SLOPSHOPPER

kanon-agenci

Panel „co robią agenci”: subagenci sesji ze statusem, REALNYM modelem każdego żądania (turn.step), tokenami i liczbą narzędzi; rozjazd tagu [S5.5] w opisie z…

newpanecommandstatusagents
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · kanon-agenci
│ ┃ Agenci ✕ › fix the failing auth test and add an audit log call │ ┃ [Aktywni] Wszyscy Suma Zamknij │ ┃ agenci: 0 (0 aktywnych · 0 zakończonych) · ⏺ Read(src/auth.ts) │ ┃ spawny (mod): 0 · próg hooka ⎿ Read 6 lines │ ┃ kanon-agent-limit: 10 prób Agent ⏺ Update(src/auth.ts) │ ┃ Brak agentów w tej sesji. ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /agenci │ ⎿ kanon-agenci: agenci: 0 (0 aktywnych · 0 zakończonych) · spawny │ ⎿ kanon-agenci: Brak agentów w tej sesji. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agenci
[Aktywni] Wszyscy Suma Zamknij agenci: 0 (0 aktywnych · 0 zakończonych) · spawny (mod): 0 · próg hooka kanon-agent-limit: 10 prób Agent Brak agentów w tej sesji.
README

kanon-agenci — panel „co robią agenci”

Mod Claude Code (API „function hooks”, early access; pisany pod 2.1.293, testy zielone też na CLI 2.1.289). M1 z planu modów (2026-10-10). Zamyka lukę kanonu: tag [S5.5MD] w opisie subagenta jest deklaracją, a prawdę niesie message.model w transkrypcie — tu widać ją W TRAKCIE pracy, razem z tokenami, liczbą narzędzi i licznikiem spawnów widzianych przez mod (obok progu hooka kanon-agent-limit, który pyta od 10. próby narzędzia Agent). v0.1.1 = poprawki po recenzji sceptyka [OP5.5HI] 2026-10-10 (5 🟡, 4 🟢, 0 🔴).

[Aktywni]  Wszyscy   Suma   Zamknij
agenci: 3 (2 aktywnych · 1 zakończonych) · spawny (mod): 3 · próg hooka kanon-agent-limit: 10 prób Agent
● [S5.5] czytelnik — „[S5.5MD] Rekonesans katalogu src” · running · 4 żądań · 12 narzędzi · 38k in (31k cache) · 1k out · 2 min
● [OP5.5] recenzent — „[S5.5HI] Recenzja diffu” · running · 1 żądań · 0 narzędzi · 9k in (0k cache) · 0k out · 10 s · ⚠ tag≠model (S5.5→OP5.5)
  • Źródła danych (silnik, nie deklaracje): agent.spawn → start wiersza (opis, typ, model rozstrzygnięty przez silnik); turn.step z agentId → KAŻDE żądanie agenta: model, który odpowiedział (usage.model), tokeny in/out/cache, liczba narzędzi, o które poprosiła odpowiedź (toolUses); turn.complete z agentId → koniec pętli; $.agent.list() → status na żywo przy rysowaniu (pending|running|waiting|idle|completed|failed|killed). Kroki bez agentId liczy osobno jako „główny wątek” (zakładka Suma).
  • Rozjazd tagu: kod modelu z tagu w opisie (OP5.5|OP5|OP4.8|F5.1|F5|S5.5|S5|H5.5|H4.5) porównany z kodem z id modelu z usage (rodzina + wersja Z GRANICĄ: claude-opus-5-5-20260101 → OP5.5, us.anthropic.claude-sonnet-5-5-…-v1:0 → S5.5, claude-opus-5-6 → OP5.6, nie OP5) → ⚠ tag≠model (S5.5→OP5.5) na żółto. Wiersz bez tagu, bez odpowiedzi albo z id spoza wzorca nie ostrzega.
  • Licznik spawnów to spawny, które DOTARŁY do hooka agent.spawn (także odrzucone { deny } niżej, także z wnętrza subagentów i z $.agent.spawn innych pluginów). Hook kanon-agent-limit liczy co innego: PRÓBY narzędzia Agent w transkrypcie głównej pętli (odmowy PreToolUse/uprawnień nie docierają do modu). Próg hooka (10) jest w etykiecie jako punkt odniesienia, nie jako wspólny licznik.
  • Krok bez odpowiedzi (usage: null — przerwanie, błąd API) nie liczy się jako żądanie i nie ustawia modelu; wiersz bez policzonych kroków pokazuje „bez kroków widzianych przez mod” zamiast zer, wiersz prosto z listy czas „—” (mod nie zna startu agenta).
  • Kolejność spawn/krok nie jest gwarantowana: krok, który wyprzedzi hook spawnu, czeka w buforze modułu (per id) i wchodzi do wiersza, gdy spawn nadejdzie; wiersz założony z listy dostaje od spawnu opis, typ i model, liczniki zostają.
  • Agent wznowiony (wiadomość, teammate): kolejny krok po turn.complete znów otwiera wiersz — linia statusu (▶ = bez koniec) i panel (status z listy) liczą zgodnie.
  • Czego nie widać (typy 2.1.293): agentów workflow — hook spawnu pomija je (bez wiersza, bez licznika; zdalny agent workflow nie ma lokalnej pętli ani wpisu na liście, więc wiersz wisiałby „running” na zawsze) — i forków silnika (kompaktacja, pamięć): nie ma ich na liście, każdy ich krok trafia do licznika „pominięte kroki (workflow, forki silnika)”. Mod załadowany po starcie agenta zakłada wiersz z listy przy pierwszym kroku. Po reloadzie modułu stan ($.state) zostaje, bufor nieznanych id startuje od nowa.
  • Komenda /agenci — otwiera panel (poproszony przez człowieka seatuje się przy dowolnej szerokości) i daje podsumowanie tekstowe; /agenci tekst — tylko tekst; /agenci zamknij — zamyka. Linia statusu: agenci 2▶ 1✓.
  • Zakładki: Aktywni (pending|running|waiting|idle), Wszyscy, Suma (tokeny per kod modelu + główny wątek).
  • Mod wyłącznie obserwuje: nie zmienia modelu ani promptu agenta, nie odrzuca spawnów, nie dotyka tool.check, nie pisze plików, nie używa sieci ani modelu. Hooki na zdarzeniach bramkujących mają try/catch po next — błąd modu nie dotyka tury.

Linie walidatora (claude plugin validate, 2.1.289 i 2.1.293):

hooks: session.start, agent.spawn, turn.step, turn.complete, command.run{command=agenci}, ui.render{component=Pane, requestId=agenci}
calls: $.agent.list, $.clock.now, $.command.register, $.state.get, $.state.set, $.ui.close, $.ui.open, $.ui.resolve, $.ui.status

Brak $.fs, $.http, $.process, $.model, tool.call, tool.check — to kryterium „gotowe” z planu. Walidator nie dowodzi zasad sam z siebie (nie pokazuje decyzji tool.check ani ścieżek zapisu) — dowodem są testy i recenzja kodu.

Uruchomienie

Na stałe: ścieżka modu w env.CLAUDE_CODE_PLUGIN_DIRS w ~/.claude/settings.json (z backupem) — działa od NOWEJ sesji (także desktop); folder jest obserwowany, zapis pliku przeładowuje mod. Jedna sesja z terminala bez env:

claude --plugin-dir ~/Developer/claude-mods/kanon-agenci

Panel otwiera /agenci (nie timer — pane otwarty bez prośby czeka do 144 kolumn). Rysuje się w terminalu i w zakładce Code aplikacji desktop; panel czatu VS Code wykonuje tylko hooki (komenda tekst działa wszędzie).

Sprawdzenie

claude plugin validate ~/Developer/claude-mods/kanon-agenci
claude plugin test     ~/Developer/claude-mods/kanon-agenci

16 testów (tests/agenci.test.ts): wiersz po spawn + 2 krokach (terminal i desktop), rozjazd tag≠model, kod modelu z granicą (data, prefiks dostawcy, opus-5-6 → OP5.6, id spoza wzorca bez ⚠), krok bez odpowiedzi → nie liczony, bez ⚠, krok forka poza listą → każdy krok pominięty i jedno pytanie listy na id, krok agenta z listy bez spawnu → wiersz, krok wyprzedza spawn (lista pusta → bufor → wiersz; lista zna → scalenie danych spawnu), agent z listy bez kroków widoczny (status z listy wygrywa; czas „—”; zakładki), turn.complete → completed + linia statusu, agent wznowiony po turn.complete → panel i linia statusu zgodne, Suma per model + główny wątek, /agenci open/close (isPlaced:false → powód w tekście), pusty stan, spawn odrzucony ({ deny }) → licznik rośnie, wiersza brak. Bez testu (driver $.agent.spawn kitu nie niesie workflow): pominięcie agentów workflow — wyjątek jest w kodzie. Cisza na powierzchniach bez panelu: linia walidatora hooks: nie zawiera AbovePrompt ani StatusLine — mod rysuje tylko własny Pane i wpis $.ui.status. Kryteria planu (c)(d)(e) — ręczny przegląd w terminalu i desktopie ze zrzutem — czekają na właściciela.

Lekcje (2026-10-10): hook turn.step to generator — const r = yield* next(e) i return r; w teście hook dna też jest generatorem (async function* (_$, e) { return wynik }), a driver $.turn.step(input) trzeba skonsumować (for await) przed await s.result; on(...) testu nie przyjmuje drugiej rejestracji tego samego zdarzenia („registered twice”) — warianty dna przez parametr; driver $.agent.spawn nie niesie workflow, więc wyjątek dla agentów workflow jest w kodzie, ale bez testu; krok „z dna” spawnu ($.turn.step wywołany w hooku dna agent.spawn, zanim odda wynik) odgrywa wyścig spawn/krok; update($, …) zwraca zapisaną wartość — linia statusu liczy z niej, nie z ponownego read (d.ts: „Every get of one dispatch reads one moment”).

Rollback

git revert commitu modu, usunięcie ścieżki z CLAUDE_CODE_PLUGIN_DIRS (albo cp backupu ~/.claude/settings.json.bak-<ts>), rm -rf ~/.claude/dev-mods/<session_id>/kanon-agenci.

Source 2 files
hooks/register.tsx 319 lines
1// kanon-agenci — panel „co robią agenci” (mod Claude Code, API function hooks 2.1.293; early access). v0.1.1 po recenzji sceptyka.
2// Dane z silnika, nie z deklaracji: `agent.spawn` (start, opis, typ, model rozstrzygnięty), `turn.step` (KAŻDE żądanie do modelu
3// z `agentId`, model który odpowiedział i tokeny z `usage`), `turn.complete` (koniec pętli agenta), `$.agent.list()` (status na żywo
4// przy rysowaniu). Zamyka lukę kanonu: tag `[S5.5MD]` w opisie subagenta jest deklaracją, a prawdę niesie `message.model`
5// transkryptu — tu widać ją w trakcie pracy (wiersz z `⚠ tag≠model`).
6// Licznik „spawny (mod)” to spawny, które DOTARŁY do hooka `agent.spawn` (także odrzucone przez `{ deny }` niżej, także z wnętrza
7// subagentów i z `$.agent.spawn` innych pluginów). To NIE jest licznik hooka `kanon-agent-limit`, który liczy PRÓBY użycia narzędzia
8// Agent w transkrypcie głównej pętli (odmowy PreToolUse/uprawnień nie docierają do modu) — próg hooka (10) pokazujemy jako punkt odniesienia.
9// Mod wyłącznie obserwuje: nie zmienia modelu ani promptu agentów, nie wstrzymuje spawnów, nie pisze plików, nie używa sieci,
10// nie wywołuje modelu, nie zatwierdza niczego. Hooki na zdarzeniach bramkujących mają try/catch po `next` i `.catch` → `next(e)`.
11// Czego NIE widać (typy 2.1.293): agentów workflow (bez wiersza i bez licznika — także zdalnych, bez lokalnej pętli) i forków silnika
12// (kompaktacja, pamięć): ich kroki liczy `pominiete`; teammate’a w osobnym panelu terminala. Krok bez odpowiedzi (`usage: null`:
13// przerwanie, błąd API) nie liczy się jako żądanie i nie ustawia modelu. Panel otwiera `/agenci` albo przycisk — nie timer.
14import { atom, read, update } from 'claude-code'
15import type { AgentInfo, EngineInterface, Register, TurnStepResult } from 'claude-code'
16
17import type { Glowny, Wiersz, Zakladka } from '../types'
18
19const PANE = 'agenci'
20const LIMIT_AGENT = 10 // próg hooka kanon-agent-limit (PreToolUse Agent) — punkt odniesienia, nie ten sam licznik
21
22// Skaner walidatora wymaga literałów plugin/key w każdym atomie — stąd powtórzona nazwa.
23const wiersze = atom({ plugin: 'kanon-agenci', key: 'wiersze' } as const, [] as Wiersz[])
24const glowny = atom({ plugin: 'kanon-agenci', key: 'glowny' } as const, {
25  kroki: 0,
26  narzedzia: 0,
27  wejscie: 0,
28  wyjscie: 0,
29  cache: 0,
30  model: null,
31} as Glowny)
32const spawny = atom({ plugin: 'kanon-agenci', key: 'spawny' } as const, 0)
33const pominiete = atom({ plugin: 'kanon-agenci', key: 'pominiete' } as const, 0)
34const zakladka = atom({ plugin: 'kanon-agenci', key: 'zakladka' } as const, 'aktywni' as Zakladka)
35
36// Kod modelu jak tag kanonu `[<MODEL><EFFORT>]` (kanon-tag.sh): rodzina + wersja z id modelu, Z GRANICĄ — dozwolona data `-YYYYMMDD`
37// i sufiks dostawcy `-v1:0`, prefiks dostawcy (`us.anthropic.`) ignorowany. `claude-opus-5-6` → OP5.6 (nie OP5), id spoza wzorca → null.
38const ID_MODELU = /claude-(opus|sonnet|haiku|fable)-(\d{1,2})(?:-(\d{1,2}))?(?:-\d{8})?(?:-v\d+(?::\d+)?)?$/
39const RODZINA: Record<string, string> = { opus: 'OP', sonnet: 'S', haiku: 'H', fable: 'F' }
40const TAG = /\[(OP5\.5|OP5|OP4\.8|F5\.1|F5|S5\.5|S5|H5\.5|H4\.5)(LO|MD|HI|XH|MX)?\]/
41const NAZWY: Record<Zakladka, string> = { aktywni: 'Aktywni', wszyscy: 'Wszyscy', suma: 'Suma' }
42const AKTYWNE = new Set(['pending', 'running', 'waiting', 'idle'])
43
44const kod = (model: string | null): string | null => {
45  const m = model ? ID_MODELU.exec(model) : null
46  return m ? `${RODZINA[m[1]]}${m[2]}${m[3] ? `.${m[3]}` : ''}` : null
47}
48const kodLubId = (model: string | null): string => (model ? (kod(model) ?? model) : '?')
49const tagZ = (opis: string): string | null => TAG.exec(opis)?.[1] ?? null
50const k = (n: number) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}k`)
51const czas = (ms: number) => (ms < 60_000 ? `${Math.max(0, Math.round(ms / 1000))} s` : `${Math.round(ms / 60_000)} min`)
52const skroc = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s)
53
54type Liczniki = { model: string | null; kroki: number; narzedzia: number; wejscie: number; wyjscie: number; cache: number }
55type Krok = { model: string; narzedzia: number; wejscie: number; wyjscie: number; cache: number }
56
57const PUSTE: Liczniki = { model: null, kroki: 0, narzedzia: 0, wejscie: 0, wyjscie: 0, cache: 0 }
58
59/** Liczby z jednego kroku; null, gdy żadna odpowiedź nie przyszła (`usage: null`) — takiego kroku nie liczymy. */
60const krokZ = (r: TurnStepResult): Krok | null => {
61  const u = r.usage
62  if (!u) return null
63  return {
64    model: u.model,
65    narzedzia: r.toolUses.length,
66    wejscie: u.input_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens,
67    wyjscie: u.output_tokens,
68    cache: u.cache_read_input_tokens,
69  }
70}
71const dolicz = <T extends Liczniki>(w: T, s: Krok): T => ({
72  ...w,
73  model: s.model,
74  kroki: w.kroki + 1,
75  narzedzia: w.narzedzia + s.narzedzia,
76  wejscie: w.wejscie + s.wejscie,
77  wyjscie: w.wyjscie + s.wyjscie,
78  cache: w.cache + s.cache,
79})
80
81const nowyWiersz = (id: string, opis: string, typ: string, start: number | null, zrodlo: Wiersz['zrodlo'], modelZadany: string | null): Wiersz => ({
82  id,
83  opis,
84  typ,
85  tag: tagZ(opis),
86  modelZadany,
87  ...PUSTE,
88  start,
89  koniec: null,
90  zrodlo,
91})
92
93const statusWiersza = (w: Wiersz, zListy: AgentInfo | undefined): string => zListy?.status ?? (w.koniec !== null ? 'completed' : 'running')
94
95const rozjazd = (w: Wiersz): string | null => {
96  if (!w.tag || !w.model) return null
97  const realny = kod(w.model)
98  if (!realny) return null // id spoza wzorca — nie zgadujemy
99  return realny === w.tag ? null : `${w.tag}→${realny}`
100}
101
102const liniaAgenta = (w: Wiersz, status: string, teraz: number): string => {
103  const r = rozjazd(w)
104  const liczniki = w.kroki
105    ? `${w.kroki} żądań · ${w.narzedzia} narzędzi · ${k(w.wejscie)} in (${k(w.cache)} cache) · ${k(w.wyjscie)} out`
106    : 'bez kroków widzianych przez mod'
107  const trwanie = w.start === null ? '—' : czas((w.koniec ?? teraz) - w.start)
108  return `[${kodLubId(w.model ?? w.modelZadany)}] ${w.typ} — „${skroc(w.opis, 44)}” · ${status} · ${liczniki} · ${trwanie}` + (r ? ` · ⚠ tag≠model (${r})` : '')
109}
110const liniaGlownego = (g: Glowny): string =>
111  `główny wątek [${kodLubId(g.model)}]: ${g.kroki} żądań · ${g.narzedzia} narzędzi · ${k(g.wejscie)} in (${k(g.cache)} cache) · ${k(g.wyjscie)} out`
112const licznikSpawnow = (n: number) => `spawny (mod): ${n} · próg hooka kanon-agent-limit: ${LIMIT_AGENT} prób Agent`
113
114// Kroki z id bez wiersza i bez wpisu na liście, zebrane per id (lista pytana raz na id). Zwykle forki silnika i agenci workflow —
115// liczone w `pominiete`. Gdy jednak nadejdzie `agent.spawn` tego id (kolejność spawn/krok nie jest gwarantowana), bufor wchodzi
116// do wiersza, a `pominiete` oddaje te kroki. Pamięć modułu: po reloadzie startuje od nowa.
117const nieznane = new Map<string, Liczniki>()
118
119/** Wiersze do pokazania: stan modu + agenci z listy, których spawnu mod nie widział (np. załadowany po ich starcie). */
120async function zebrane($: EngineInterface): Promise<{ wiersze: Wiersz[]; statusy: Map<string, AgentInfo>; teraz: number }> {
121  const zapisane = await read($, wiersze)
122  let lista: AgentInfo[] = []
123  try {
124    lista = await $.agent.list()
125  } catch {
126    lista = []
127  }
128  const teraz = await $.clock.now()
129  const statusy = new Map(lista.map(a => [a.id, a] as const))
130  const znane = new Set(zapisane.map(w => w.id))
131  const zListy = lista.filter(a => !znane.has(a.id)).map(a => nowyWiersz(a.id, a.description, a.type, null, 'lista', null))
132  return { wiersze: [...zapisane, ...zListy], statusy, teraz }
133}
134
135async function zapiszKrok($: EngineInterface, e: { agentId?: string }, r: TurnStepResult): Promise<void> {
136  const s = krokZ(r)
137  if (!s) return
138
139  if (!e.agentId) {
140    await update($, glowny, g => dolicz(g, s))
141    return
142  }
143
144  const id = e.agentId
145  let nowy: Wiersz | null = null
146  if (!(await read($, wiersze)).some(w => w.id === id)) {
147    const buf = nieznane.get(id)
148    if (buf) {
149      nieznane.set(id, dolicz(buf, s))
150      await update($, pominiete, n => n + 1)
151      return
152    }
153    const info = (await $.agent.list()).find(a => a.id === id)
154    if (!info) {
155      nieznane.set(id, dolicz(PUSTE, s))
156      await update($, pominiete, n => n + 1)
157      return
158    }
159    nowy = nowyWiersz(id, info.description, info.type, await $.clock.now(), 'lista', null) // start = pierwszy widziany krok
160  }
161  // Jedno `update`: dopisanie wiersza (jeśli spawn go tymczasem nie założył) i doliczenie kroku. Krok po `turn.complete`
162  // (agent wznowiony wiadomością, teammate) znów otwiera wiersz — `koniec: null`.
163  const l = await update($, wiersze, l => {
164    const baza = l.some(w => w.id === id) ? l : nowy ? [...l, nowy] : l
165    return baza.map(w => (w.id === id ? { ...dolicz(w, s), koniec: null } : w))
166  })
167  pasekStanu($, l)
168}
169
170/** Wpis w linii statusu: `agenci 2▶ 1✓` (▶ = wiersz bez `koniec`); znika, gdy nie ma żadnego. Liczy z wartości, którą właśnie zapisano. */
171function pasekStanu($: EngineInterface, l: Wiersz[]): void {
172  if (l.length === 0) {
173    void $.ui.status(undefined)
174    return
175  }
176  const biegna = l.filter(w => w.koniec === null).length
177  void $.ui.status(`agenci ${biegna}▶ ${l.length - biegna}✓`)
178}
179
180async function podsumowanie($: EngineInterface): Promise<string> {
181  const { wiersze: l, statusy, teraz } = await zebrane($)
182  const [n, pom, g] = await Promise.all([read($, spawny), read($, pominiete), read($, glowny)])
183  const ze = l.map(w => ({ w, status: statusWiersza(w, statusy.get(w.id)) }))
184  const biegna = ze.filter(x => AKTYWNE.has(x.status)).length
185  const naglowek =
186    `agenci: ${l.length} (${biegna} aktywnych · ${l.length - biegna} zakończonych) · ${licznikSpawnow(n)}` +
187    (pom ? ` · pominięte kroki (workflow, forki silnika): ${pom}` : '')
188  const linie = ze.map(x => `- ${liniaAgenta(x.w, x.status, teraz)}`)
189  return [naglowek, ...(l.length ? linie : ['Brak agentów w tej sesji.']), ...(g.kroki ? [liniaGlownego(g)] : [])].join('\n')
190}
191
192export const register: Register = on => {
193  on('session.start', async ($, e, next) => {
194    await $.command.register({
195      name: 'agenci',
196      description: 'Panel „co robią agenci”: status, realny model, tokeny, narzędzia; „tekst” = tylko podsumowanie, „zamknij” = zamknij panel',
197      argumentHint: '[tekst|zamknij]',
198    })
199    return next(e)
200  })
201
202  on('agent.spawn', async ($, e, next) => {
203    const r = await next(e)
204    try {
205      // Agenci workflow: bez wiersza i bez licznika (zdalny nie ma lokalnej pętli ani wpisu na liście — wiersz wisiałby „running” na zawsze);
206      // ich kroki, jeśli przyjdą, trafiają do `pominiete`.
207      if (!e.workflow) {
208        await update($, spawny, n => n + 1)
209        if (!('deny' in r && r.deny) && 'agentId' in r && r.agentId) {
210          const id = r.agentId
211          const teraz = await $.clock.now()
212          const modelZadany = 'model' in r && typeof r.model === 'string' ? r.model : null
213          const buf = nieznane.get(id) // kroki, które wyprzedziły ten hook
214          if (buf) {
215            nieznane.delete(id)
216            await update($, pominiete, n => Math.max(0, n - buf.kroki))
217          }
218          const l = await update($, wiersze, l => {
219            const i = l.findIndex(w => w.id === id)
220            if (i >= 0) {
221              // wiersz założony z listy przez krok, który wyprzedził spawn — dane spawnu dochodzą, liczniki zostają
222              const w = { ...l[i], opis: e.description, typ: e.subagentType, tag: tagZ(e.description), modelZadany, zrodlo: 'spawn' as const }
223              return l.map((x, j) => (j === i ? w : x))
224            }
225            const w = nowyWiersz(id, e.description, e.subagentType, teraz, 'spawn', modelZadany)
226            return [...l, buf ? { ...w, ...buf } : w]
227          })
228          pasekStanu($, l)
229        }
230      }
231    } catch {
232      // obserwator: błąd zapisu nie dotyka spawnu
233    }
234    return r
235  }).catch(($, e, next) => next(e))
236
237  on('turn.step', async function* ($, e, next) {
238    const r = yield* next(e)
239    try {
240      await zapiszKrok($, e, r)
241    } catch {
242      // obserwator: błąd zapisu nie dotyka kroku
243    }
244    return r
245  })
246
247  on('turn.complete', async ($, e, next) => {
248    const r = await next(e)
249    try {
250      if (e.agentId) {
251        const id = e.agentId
252        const teraz = await $.clock.now()
253        const l = await update($, wiersze, l => l.map(w => (w.id === id ? { ...w, koniec: teraz, model: e.usage?.model ?? w.model } : w)))
254        pasekStanu($, l)
255      }
256    } catch {
257      // obserwator
258    }
259    return r
260  }).catch(($, e, next) => next(e))
261
262  on('command.run', { command: 'agenci' }, async ($, e) => {
263    const arg = e.args.trim().toLowerCase()
264    if (arg === 'zamknij') {
265      await $.ui.close({ id: PANE })
266      return { text: 'Panel agentów zamknięty.' }
267    }
268    const tekst = await podsumowanie($)
269    if (arg === 'tekst') return { text: tekst }
270    const o = await $.ui.open({ id: PANE, title: 'Agenci', closeOnEscape: true })
271    return { text: o.isPlaced ? tekst : `${tekst}\nPanel czeka na szersze okno: ${o.reason}` }
272  })
273
274  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
275    const { Box, Button, Text } = $.ui.resolve(e)
276    const { wiersze: l, statusy, teraz } = await zebrane($)
277    const [n, pom, g, zak] = await Promise.all([read($, spawny), read($, pominiete), read($, glowny), read($, zakladka)])
278    const ze = l.map(w => ({ w, status: statusWiersza(w, statusy.get(w.id)) }))
279    const biegna = ze.filter(x => AKTYWNE.has(x.status)).length
280    const widoczne = zak === 'aktywni' ? ze.filter(x => AKTYWNE.has(x.status)) : ze
281
282    const kolor = (status: string) => (status === 'running' ? 'success' : status === 'failed' || status === 'killed' ? 'error' : 'warning')
283
284    const suma = new Map<string, { agentow: number; wejscie: number; wyjscie: number; cache: number }>()
285    for (const { w } of ze) {
286      const kk = kodLubId(w.model ?? w.modelZadany)
287      const s = suma.get(kk) ?? { agentow: 0, wejscie: 0, wyjscie: 0, cache: 0 }
288      suma.set(kk, { agentow: s.agentow + 1, wejscie: s.wejscie + w.wejscie, wyjscie: s.wyjscie + w.wyjscie, cache: s.cache + w.cache })
289    }
290
291    return (
292      <Box flexDirection="column">
293        <Box gap={1}>
294          {(['aktywni', 'wszyscy', 'suma'] as const).map(z => (
295            <Button key={`zak-${z}`} plain label={z === zak ? `[${NAZWY[z]}]` : ` ${NAZWY[z]} `} onPress={() => update($, zakladka, () => z)} />
296          ))}
297          <Button key="zamknij" plain dimColor label="Zamknij" onPress={() => $.ui.close({ id: PANE })} />
298        </Box>
299        <Text bold={n >= LIMIT_AGENT - 1} color={n >= LIMIT_AGENT ? 'warning' : undefined}>
300          {`agenci: ${l.length} (${biegna} aktywnych · ${l.length - biegna} zakończonych) · ${licznikSpawnow(n)}`}
301        </Text>
302        {pom > 0 && <Text dimColor>{`pominięte kroki (workflow, forki silnika — kompaktacja, pamięć): ${pom}`}</Text>}
303        {zak !== 'suma' && widoczne.length === 0 && <Text dimColor>{l.length === 0 ? 'Brak agentów w tej sesji.' : 'Żaden agent nie biegnie.'}</Text>}
304        {zak !== 'suma' &&
305          widoczne.map(({ w, status }) => (
306            <Text key={w.id} wrap="truncate-end" color={rozjazd(w) ? 'warning' : kolor(status)} dimColor={!AKTYWNE.has(status) && !rozjazd(w)}>
307              {`${AKTYWNE.has(status) ? '●' : '○'} ${liniaAgenta(w, status, teraz)}`}
308            </Text>
309          ))}
310        {zak === 'suma' &&
311          [...suma.entries()].map(([kk, s]) => (
312            <Text key={`suma-${kk}`}>{`${kk}: ${s.agentow} agentów · ${k(s.wejscie)} in (${k(s.cache)} cache) · ${k(s.wyjscie)} out`}</Text>
313          ))}
314        {zak === 'suma' && <Text dimColor>{g.kroki ? liniaGlownego(g) : 'główny wątek: brak kroków od załadowania modu'}</Text>}
315      </Box>
316    )
317  })
318}
319
types/index.d.ts 56 lines
1// Kontrakt stanu modu kanon-agenci ($.state sesji; po reloadzie modułu wartości zostają, zmienne modułu startują od nowa).
2export type Wiersz = {
3  /** Id pętli agenta: `agent.spawn` → `agentId`, to samo niosą `turn.step`/`tool.call`/`turn.complete` i `$.agent.list()`. */
4  id: string
5  /** Opis zadania z wywołania Agent (`description`) — tu siedzi deklarowany tag `[S5.5MD]`. Z listy, póki spawn nie dopisze swojego. */
6  opis: string
7  /** Typ agenta (`subagentType`): czytelnik, sceptyk, general-purpose, Explore… */
8  typ: string
9  /** Kod modelu z tagu w opisie (S5.5, OP5.5, F5.1…) albo null, gdy opis tagu nie ma. */
10  tag: string | null
11  /** Model, jaki silnik rozstrzygnął przy starcie (`AgentSpawnResult.model`); null, póki spawnu mod nie widział. */
12  modelZadany: string | null
13  /** Model, który ODPOWIEDZIAŁ w ostatnim policzonym kroku (`TurnStepResult.usage.model`); null bez żadnej odpowiedzi. */
14  model: string | null
15  /** Liczba żądań do modelu widzianych przez mod — tylko kroki z odpowiedzią (`usage` nie-null). */
16  kroki: number
17  /** Liczba wywołań narzędzi, o które poprosiły odpowiedzi (`toolUses.length` policzonych kroków). */
18  narzedzia: number
19  /** Tokeny wejścia razem: niecache + zapis cache + odczyt cache. */
20  wejscie: number
21  /** Tokeny wyjścia. */
22  wyjscie: number
23  /** Tokeny wejścia podane z cache (`cache_read_input_tokens`). */
24  cache: number
25  /**
26   * `$.clock.now()` przy `agent.spawn` (zrodlo `spawn`) albo przy PIERWSZYM widzianym kroku (zrodlo `lista`) — nie start agenta;
27   * null dla wiersza rysowanego prosto z listy (czas pokazuje się jako „—”).
28   */
29  start: number | null
30  /** `$.clock.now()` przy `turn.complete` pętli agenta; null, póki biegnie — kolejny krok (agent wznowiony, teammate) znów zeruje. */
31  koniec: number | null
32  /** Skąd wiersz: `spawn` = hook `agent.spawn` (także dopisany później do wiersza z listy), `lista` = pierwszy krok agenta, którego spawnu mod nie widział. */
33  zrodlo: 'spawn' | 'lista'
34}
35
36export type Glowny = { kroki: number; narzedzia: number; wejscie: number; wyjscie: number; cache: number; model: string | null }
37
38export type Zakladka = 'aktywni' | 'wszyscy' | 'suma'
39
40declare module 'claude-code' {
41  interface PluginState {
42    'kanon-agenci': {
43      wiersze: Wiersz[]
44      glowny: Glowny
45      /**
46       * Spawny, które dotarły do hooka `agent.spawn` (bez workflow; z odmowami `{ deny }` niżej). Inny licznik niż hook
47       * `kanon-agent-limit` (próby narzędzia Agent w transkrypcie głównej pętli) — próg hooka (10) służy za punkt odniesienia.
48       */
49      spawny: number
50      /** Kroki z `agentId`, którego nie zna ani spawn, ani `$.agent.list()` (workflow, forki silnika: kompaktacja, pamięć) — liczone, nie rysowane. */
51      pominiete: number
52      zakladka: Zakladka
53    }
54  }
55}
56