pikiloom 0.4.38 → 0.4.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dashboard/dist/assets/{AgentTab-DbFzaIyZ.js → AgentTab-Ds8D6yrl.js} +1 -1
  2. package/dashboard/dist/assets/{ConnectionModal-QNYOWHCU.js → ConnectionModal-Buj2d5L5.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-BGaKHjFT.js → DirBrowser-BTv7rH8E.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-D4Gv9b8z.js → ExtensionsTab-BnGwxS9U.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-DxxoZd84.js → IMAccessTab-DjQnpOmF.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-Nm2e93s5.js → Modal-DpuinsE3.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-BwxZmU0U.js → Modals-Bs842H3n.js} +1 -1
  8. package/dashboard/dist/assets/{Select-SGlII0yx.js → Select-Dvia29HZ.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-CVItEgcH.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-BF6lIAYM.js → SystemTab-BzPtwDxq.js} +1 -1
  11. package/dashboard/dist/assets/index-Bthwt6K_.css +1 -0
  12. package/dashboard/dist/assets/{index-DZiAiRNt.js → index-S0NmlDEH.js} +14 -14
  13. package/dashboard/dist/assets/{index-BP8R_bLT.js → index-yZ-iG1qk.js} +2 -2
  14. package/dashboard/dist/assets/{shared-S0kcs5yP.js → shared-p3kZpiD4.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/kernel-bridge.js +1 -0
  17. package/dist/agent/mcp/capabilities.js +39 -0
  18. package/dist/bot/bot.js +4 -13
  19. package/package.json +1 -1
  20. package/packages/kernel/README.md +263 -65
  21. package/packages/kernel/dist/contracts/surface.d.ts +17 -0
  22. package/packages/kernel/dist/drivers/claude.d.ts +12 -1
  23. package/packages/kernel/dist/drivers/claude.js +53 -6
  24. package/packages/kernel/dist/drivers/codex.d.ts +10 -0
  25. package/packages/kernel/dist/drivers/codex.js +79 -10
  26. package/packages/kernel/dist/index.d.ts +1 -1
  27. package/packages/kernel/dist/runtime/hub.d.ts +3 -0
  28. package/packages/kernel/dist/runtime/hub.js +84 -10
  29. package/packages/kernel/dist/runtime/loom.d.ts +1 -0
  30. package/packages/kernel/dist/runtime/loom.js +4 -1
  31. package/dashboard/dist/assets/SessionPanel-DYfSlreh.js +0 -1
  32. package/dashboard/dist/assets/index-CtS48Jn-.css +0 -1
@@ -0,0 +1,39 @@
1
+ // Session tool capabilities: each co-locates a session MCP tool group (defined under
2
+ // ./tools/) with the prompt fragment that teaches the agent to use it. This is the local,
3
+ // decoupled analog of a @pikiloom/kernel Plugin (a capability = its tools + its usage
4
+ // prompt, as one unit). pikiloom composes its own system prompt app-side and feeds the
5
+ // kernel a finished string, so this module intentionally does NOT import the kernel — that
6
+ // is what keeps pikiloom fully decoupled from the kernel's Hub/plugin runtime.
7
+ const ARTIFACT_RETURN = [
8
+ '[Artifact Return]',
9
+ 'To hand a file to the user — a screenshot, report, archive, generated asset, anything they asked you to "send" — call the `im_send_file` tool with the file path and a short caption. It is delivered through whatever terminal the user is on (an IM chat or the web dashboard) and stays retrievable even when they are connected remotely. Do NOT just print a local filesystem path: a remote user cannot open paths on this machine.',
10
+ ].join('\n');
11
+ const ASK_USER = [
12
+ '[Asking the user]',
13
+ 'The built-in `AskUserQuestion` tool is disabled here and will fail. If you would otherwise call it, call `mcp__pikiloom__im_ask_user` instead — same intent (a question plus optional choices), it blocks until the user replies via the IM/dashboard channel. Default behaviour is unchanged: infer obvious decisions yourself and only ask when you genuinely cannot proceed.',
14
+ ].join('\n');
15
+ export const SESSION_TOOL_CAPABILITIES = [
16
+ {
17
+ id: 'artifact-delivery',
18
+ tools: ['im_send_file', 'im_list_files'],
19
+ promptFragment: () => ARTIFACT_RETURN,
20
+ },
21
+ {
22
+ id: 'ask-user',
23
+ tools: ['im_ask_user'],
24
+ // Only when the HITL path is wired and the agent is claude (codex/gemini handle asks natively).
25
+ promptFragment: ({ agent, onInteraction }) => (onInteraction && agent === 'claude') ? ASK_USER : null,
26
+ },
27
+ ];
28
+ // Compose the session tool prompt = each capability's applicable fragment, in registration
29
+ // order, joined by a blank line. Byte-identical to the prior inline assembly in bot.ts.
30
+ export function composeSessionToolPrompt(ctx) {
31
+ const parts = [];
32
+ for (const cap of SESSION_TOOL_CAPABILITIES) {
33
+ const fragment = cap.promptFragment(ctx);
34
+ const trimmed = String(fragment || '').trim();
35
+ if (trimmed)
36
+ parts.push(trimmed);
37
+ }
38
+ return parts.join('\n\n');
39
+ }
package/dist/bot/bot.js CHANGED
@@ -8,6 +8,7 @@ import { getActiveProfileId, getActiveProfile, setActiveProfile } from '../model
8
8
  import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
9
9
  import { getDriver, hasDriver, allDriverIds, getDriverCapabilities } from '../agent/driver.js';
10
10
  import { resolveGuiIntegrationConfig } from '../agent/mcp/bridge.js';
11
+ import { composeSessionToolPrompt } from '../agent/mcp/capabilities.js';
11
12
  import { terminateProcessTree } from '../core/process-control.js';
12
13
  import { expandTilde } from '../core/platform.js';
13
14
  import { VERSION } from '../core/version.js';
@@ -48,18 +49,6 @@ function appendExtraPrompt(base, extra) {
48
49
  return lhs;
49
50
  return `${lhs}\n\n${rhs}`;
50
51
  }
51
- function buildMcpDeliveryPrompt() {
52
- return [
53
- '[Artifact Return]',
54
- 'To hand a file to the user — a screenshot, report, archive, generated asset, anything they asked you to "send" — call the `im_send_file` tool with the file path and a short caption. It is delivered through whatever terminal the user is on (an IM chat or the web dashboard) and stays retrievable even when they are connected remotely. Do NOT just print a local filesystem path: a remote user cannot open paths on this machine.',
55
- ].join('\n');
56
- }
57
- function buildClaudeAskUserPrompt() {
58
- return [
59
- '[Asking the user]',
60
- 'The built-in `AskUserQuestion` tool is disabled here and will fail. If you would otherwise call it, call `mcp__pikiloom__im_ask_user` instead — same intent (a question plus optional choices), it blocks until the user replies via the IM/dashboard channel. Default behaviour is unchanged: infer obvious decisions yourself and only ask when you genuinely cannot proceed.',
61
- ].join('\n');
62
- }
63
52
  function buildBrowserAutomationPrompt(browserEnabled) {
64
53
  if (!browserEnabled) {
65
54
  return [
@@ -2022,7 +2011,9 @@ export class Bot {
2022
2011
  const workflowEnabled = cs.agent === 'claude' && (extras?.workflowEnabled ?? this.claudeWorkflowEnabled);
2023
2012
  const deliverySessionKey = ('key' in cs && typeof cs.key === 'string') ? cs.key : null;
2024
2013
  const wrappedSendFile = this.buildArtifactSendFile(cs.agent, deliverySessionKey, cs, mcpSendFile);
2025
- const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(appendExtraPrompt(buildMcpDeliveryPrompt(), onInteraction && cs.agent === 'claude' ? buildClaudeAskUserPrompt() : ''), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
2014
+ // Session tool capabilities (im_send_file / im_ask_user) bundle their usage prompt with
2015
+ // the tool itself — see agent/mcp/capabilities.ts. Browser/workflow remain session policy.
2016
+ const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(composeSessionToolPrompt({ agent: cs.agent, onInteraction: !!onInteraction }), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
2026
2017
  const effectiveSystemPrompt = isFirstTurnOfSession
2027
2018
  ? appendExtraPrompt(systemPrompt, mcpSystemPrompt)
2028
2019
  : (mcpSystemPrompt || undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.38",
3
+ "version": "0.4.39",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,107 +1,305 @@
1
1
  # @pikiloom/kernel
2
2
 
3
- Heterogeneous coding agents (Claude / Codex / Gemini / ACP) an **interaction-friendly,
4
- accumulating session snapshot + control handle**, exposed over **pluggable surfaces**
5
- (IM, Web, tunnel). Environmental concerns are **injected ports** with working defaults.
3
+ Turn heterogeneous coding agents (**Claude Code · Codex · Gemini · ACP/Hermes**) into one
4
+ **uniform, accumulating session snapshot + a small set of control verbs**, exposed over
5
+ **pluggable surfaces** (IM, Web, tunnel, raw terminal). Environmental concerns (storage,
6
+ credentials, tools, prompts) are **injected ports** with working defaults.
6
7
 
7
- This is the reusable core **pikiloom itself is meant to be built on**. Drop it into any
8
- project and stand up a "pikiloom-like" backend in a few lines.
8
+ This is the reusable core that [pikiloom](https://github.com/xiaotonng/pikiloom) itself is
9
+ built on. Drop it into any project and stand up a "pikiloom-like" backend in a few lines
10
+ **you never parse a CLI's output or learn each agent's wire format**; you read one
11
+ `UniversalSnapshot` and call `prompt / stop / steer / interact`.
9
12
 
10
- ## The model (one package, three rings)
13
+ ```bash
14
+ npm i @pikiloom/kernel
15
+ ```
16
+
17
+ - Runtime dep: `ws`. Optional: `node-pty` (only for raw-TUI passthrough). Node ≥ 20, ESM-only.
18
+ - TypeScript types ship in the package (`dist/**/*.d.ts`). For an LLM-oriented summary, see [`llms.txt`](./llms.txt).
19
+
20
+ ---
21
+
22
+ ## Mental model
11
23
 
12
24
  ```
13
- 上层 (you write): IM bindings · Web UI/skin · Plugins ── implement Surface / Plugin
25
+ 上层 (you write): IM bindings · Web UI · Plugins ── implement Surface / Plugin
14
26
  ▲ createLoom({ surfaces, plugins, ...ports })
15
- @pikiloom/kernel: terminal/ Surface contract + LoomIO + built-in WebSurface
16
- (one import, runtime/ SessionRunner · Hub (multi-session) · snapshot accumulation · control verbs
17
- internally agent/ driver registry + Claude/Echo drivers + spawn/parse Driver
18
- modular) protocol/ UniversalSnapshot + diff (the wire vocabulary) Channel
19
- ports/ SessionStore · ModelResolver · ToolProvider · ... (+ defaults)
20
- spawns external CLIs (unchanged)
21
- 下层 (unchanged): the `claude` / `codex` / binaries, native protocols
27
+ @pikiloom/kernel: contracts/ Driver · Surface · LoomIO · Ports
28
+ (one import, runtime/ SessionRunner · Hub (multi-session) · snapshot accumulation · control verbs
29
+ modular inside) drivers/ Claude / Codex / Gemini / Hermes / Echo the agent axis (下层)
30
+ surfaces/ WebSurface (ws host) · CliSurface
31
+ protocol/ UniversalSnapshot + diff ← the wire vocabulary
32
+ ports/ SessionStore · ModelResolver · ToolProvider · ... (+ defaults)
33
+ spawns the external `claude` / `codex` / `gemini` CLIs (unchanged)
34
+ 下层 (unchanged): the agent binaries, native protocols
22
35
  ```
23
36
 
24
- **IM and Web are not two systems** — they are both just `Surface`s over the same `LoomIO`.
37
+ **IM and Web are not two systems** — both are just `Surface`s over the same `LoomIO`.
38
+
39
+ ### Two rails, same driver registry
40
+
41
+ | Rail | Driver method | Output | Use for |
42
+ |------|---------------|--------|---------|
43
+ | **Structured** | `driver.run(input, ctx)` | streamed `UniversalSnapshot` (text, reasoning, tool activity, plan, usage) | IM, Web dashboards, any UI that renders structured turns |
44
+ | **Raw PTY** | `driver.tui(input)` | a real full-screen interactive process passed through a PTY | a local terminal app (`pikiloom code`-style); needs `node-pty` |
25
45
 
26
- ## Quickstart
46
+ ---
47
+
48
+ ## Quick start
49
+
50
+ ### 1. One turn through one agent — `runTurn` (the bridge primitive)
51
+
52
+ The smallest unit. No persistence, no multi-session — just run a turn and get a streamed
53
+ snapshot + final result. This is exactly what an existing app maps onto its own UI.
27
54
 
28
55
  ```ts
29
- import { createLoom, ClaudeDriver, WebSurface } from '@pikiloom/kernel';
56
+ import { runTurn, ClaudeDriver } from '@pikiloom/kernel';
57
+
58
+ const { result, snapshot } = await runTurn(
59
+ new ClaudeDriver(),
60
+ { prompt: 'Summarize package.json in one line', workdir: process.cwd(), effort: 'high' },
61
+ {
62
+ onSnapshot: (s) => {
63
+ // fires on every event — render live:
64
+ // s.text accumulated assistant output
65
+ // s.reasoning accumulated thinking (when the model/auth exposes it)
66
+ // s.activity human-readable execution trail ("Read foo.ts", "Run shell: npm test")
67
+ // s.toolCalls structured tool calls [{ name, summary, status }]
68
+ // s.plan, s.usage, s.artifacts, s.interactions
69
+ process.stdout.write(`\r${s.activity?.split('\n').at(-1) ?? ''}`);
70
+ },
71
+ onSteer: (steer) => { /* call steer('extra prompt') mid-turn */ },
72
+ signal: undefined, // pass an AbortSignal to stop the turn
73
+ },
74
+ );
75
+ console.log(result.ok, result.text, result.sessionId);
76
+ ```
77
+
78
+ ### 2. A full multi-session backend — `createLoom`
79
+
80
+ Adds persistence, a session hub, per-session queueing, discovery, and surfaces. Every port
81
+ has a default, so this runs with zero wiring.
82
+
83
+ ```ts
84
+ import { createLoom, ClaudeDriver, CodexDriver, WebSurface } from '@pikiloom/kernel';
30
85
 
31
86
  const loom = createLoom({
32
- drivers: [new ClaudeDriver()], // 下层 (unchanged)
33
- surfaces: [new WebSurface({ port: 8787 })], // 上层 (IM / Web / tunnel)
87
+ drivers: [new ClaudeDriver(), new CodexDriver()], // 下层 (unchanged binaries)
88
+ surfaces: [new WebSurface({ port: 8787 })], // 上层 (Web/tunnel; add your IM Surface here)
89
+ defaultAgent: 'claude',
90
+ // optional ports — override any one to swap storage / credentials / tools / prompts:
91
+ // sessionStore, modelResolver, toolProvider, systemPromptBuilder, catalog, interactionHandler
34
92
  });
35
93
  await loom.start();
94
+
95
+ // Drive it from anywhere via loom.io (LoomIO):
96
+ const { sessionKey, taskId } = await loom.io.prompt({ prompt: 'hello', agent: 'claude' });
97
+ const unsub = loom.io.subscribe((key, snapshot, patch, seq) => { /* render */ });
98
+ loom.io.steer(taskId, 'actually, do X instead');
99
+ loom.io.stop(sessionKey);
36
100
  ```
37
101
 
38
- Add an IM terminal by implementing `Surface.start(io)`: route inbound messages to
39
- `io.prompt(...)` and render `io.subscribe(...)` snapshots back. Everything else (storage,
40
- credentials, tools, prompts) has a default and is overridable via ports.
102
+ To add an **IM channel**, implement `Surface.start(io)`: route inbound messages to
103
+ `io.prompt(...)` and render `io.subscribe(...)` snapshots back out. Nothing else changes.
104
+
105
+ ### 3. Raw TUI passthrough — `runTui` / `openTui`
106
+
107
+ ```ts
108
+ const loom = createLoom({ drivers: [new ClaudeDriver()], defaultAgent: 'claude' });
109
+ await loom.runTui({ agent: 'claude', workdir: process.cwd() }); // you're now in the real Claude TUI
110
+ ```
41
111
 
42
- ### TUI passthrough (`pikiloom code` Claude/Codex TUI)
112
+ `runTui` does full stdin/stdout raw passthrough on the current terminal. `openTui` returns a
113
+ `PtyBridge` (`onData` / `write` / `resize` / `onExit`) so you can drive or tee it yourself.
114
+ Requires the optional `node-pty` dependency (`ptyAvailable()` reports availability).
43
115
 
44
- Two orthogonal driver outputs off the same registry: `run()` yields a structured
45
- `UniversalSnapshot` (Web/IM); `tui()` yields a raw, full-screen interactive process that
46
- the kernel spawns in a **PTY** and passes through transparently (with optional tee/mirror).
116
+ ---
117
+
118
+ ## The data you read: `UniversalSnapshot`
119
+
120
+ One driver-agnostic shape for every agent. A surface renders *this* and never touches a
121
+ CLI's native format. It **accumulates** across a turn (text/reasoning append; structured
122
+ fields replace).
47
123
 
48
124
  ```ts
49
- import { createLoom, ClaudeDriver, CodexDriver } from '@pikiloom/kernel';
50
- const loom = createLoom({ drivers: [new ClaudeDriver(), new CodexDriver()], defaultAgent: 'claude' });
51
- await loom.runTui({ agent: 'claude', workdir: process.cwd() }); // you're now in the real Claude TUI, launched by the kernel
125
+ interface UniversalSnapshot {
126
+ phase: 'idle' | 'queued' | 'streaming' | 'done';
127
+ taskId?: string | null;
128
+ sessionId?: string | null;
129
+ agent?: string | null;
130
+ model?: string | null;
131
+ effort?: string | null;
132
+ prompt?: string | null;
133
+
134
+ text?: string; // accumulated assistant output
135
+ reasoning?: string; // accumulated thinking (only when the model/auth streams it; see note)
136
+ activity?: string; // human-readable execution trail, one line per tool/subagent (kernel-derived)
137
+ plan?: UniversalPlan | null; // { explanation, steps:[{ text, status }] }
138
+ toolCalls?: UniversalToolCall[]; // structured: { id, name, summary, input?, result?, status }
139
+ subAgents?: UniversalSubAgent[]; // spawned sub-agents and their tools
140
+ usage?: UniversalUsage | null; // { inputTokens, outputTokens, cachedInputTokens, contextPercent, ... }
141
+ artifacts?: UniversalArtifact[]; // generated files/images { url|path, fileName, mime, kind }
142
+ interactions?: UniversalInteraction[]; // pending human-in-the-loop questions (answer via interact())
143
+ queued?: UniversalQueuedTask[]; // prompts waiting behind the running turn
144
+
145
+ error?: string | null;
146
+ incomplete?: boolean; // true if the turn was interrupted / errored
147
+ startedAt?: number;
148
+ updatedAt: number;
149
+ }
52
150
  ```
53
151
 
54
- `runTui` does full stdin/stdout raw passthrough on the current terminal; `openTui` returns
55
- a `PtyBridge` (`onData`/`write`/`resize`/`onExit`) so you can drive or tee it yourself.
56
- Requires the optional `node-pty` dependency.
152
+ Sub-shapes:
57
153
 
58
- ## Contracts
154
+ ```ts
155
+ interface UniversalToolCall { id: string; name: string; summary: string; input?: string | null; result?: string | null; status: 'running' | 'done' | 'failed'; }
156
+ interface UniversalPlan { explanation: string | null; steps: { text: string; status: 'pending' | 'inProgress' | 'completed' }[]; }
157
+ interface UniversalUsage { inputTokens: number | null; outputTokens: number | null; cachedInputTokens: number | null; contextUsedTokens?: number | null; contextPercent: number | null; providerName?: string | null; }
158
+ interface UniversalArtifact { url?: string; path?: string; fileName: string; mime?: string; kind: 'photo' | 'document'; caption?: string; }
159
+ interface UniversalInteraction { promptId: string; kind: 'user-input' | 'permission' | 'confirmation'; title: string; questions: { id: string; text: string; type?: 'text' | 'select'; choices?: { label: string; value?: string }[] }[]; }
160
+ ```
59
161
 
60
- | Seam | Interface | Direction |
61
- |------|-----------|-----------|
62
- | Agent (下层) | `AgentDriver.run(input, ctx)` emits `DriverEvent`s | down, bundled (+ `registerDriver` escape hatch) |
63
- | Surface (上层) | `Surface.start(io: LoomIO)` | up, app-provided (WebSurface built-in) |
64
- | Session API | `LoomIO` = prompt/stop/steer/interact + subscribe/getSnapshot/listSessions | the meeting point |
65
- | Plugin | `Plugin.tools()` / `decorateSnapshot()` | up, app-provided |
66
- | Ports | `SessionStore` · `ModelResolver` · `ToolProvider` · `SystemPromptBuilder` · `InteractionHandler` | side, defaults included |
162
+ **Activity projection (kernel-owned).** Every driver emits *structured* tool calls; the
163
+ kernel's `SessionRunner` derives `snapshot.activity` from `toolCalls` + `subAgents` centrally
164
+ (`projectActivity`): one line per call with a status suffix `summary` while running,
165
+ `summary done` / `summary -> detail` on success, `summary failed: detail` on error. So every
166
+ surface gets a readable execution trail for free, and rich UIs can still use `toolCalls`
167
+ directly. You implement this **nowhere** it's a property of the snapshot.
168
+
169
+ **Reasoning note.** Plaintext thinking only appears when the agent actually streams it
170
+ (e.g. BYOK Anthropic API keys, Codex `item/reasoning`). Subscription/OAuth Claude withholds
171
+ plaintext extended-thinking (streams only an encrypted signature), so `reasoning` is empty
172
+ there — a platform behavior, not a kernel limitation. The Claude driver also captures
173
+ reasoning delivered as a complete block, not only as streamed deltas.
174
+
175
+ ### Streaming on the wire — `diffSnapshot` / `applySnapshotPatch`
176
+
177
+ `diffSnapshot(prev, next)` produces a compact `SnapshotPatch` (prefix-append for `text` /
178
+ `reasoning`; field replacement otherwise, with `undefined → null` so field-clears survive
179
+ `JSON.stringify`). `applySnapshotPatch(prev, patch)` reassembles it on the client. `WebSurface`
180
+ uses these; reuse them for your own transport.
181
+
182
+ ---
183
+
184
+ ## Control verbs (`LoomIO`)
185
+
186
+ ```ts
187
+ io.prompt({ prompt, agent?, sessionKey?, workdir?, model?, effort?, attachments? }) // → { sessionKey, taskId }
188
+ io.stop(sessionKey) // interrupt the running turn (queued tasks stay & promote)
189
+ io.steer(taskId, prompt, attachments?) // inject a message mid-turn (drivers with capabilities.steer)
190
+ io.interact(promptId, action, value?) // answer a pending interaction: 'select' | 'text' | 'skip' | 'cancel'
191
+ io.subscribe((sessionKey, snapshot, patch, seq) => …) // live snapshots
192
+ io.getSnapshot(sessionKey) · io.getHistory(sessionKey) · io.listSessions()
193
+ io.listAgentInfo() · io.listModels(agent) · io.listEffort(agent, model?) · io.listTools(agent, workdir?) · io.listSkills(agent, workdir?)
194
+ ```
195
+
196
+ Concurrent prompts to one session **queue** by default (`serialPerSession`, no clobber); `stop`
197
+ interrupts only the current turn and the next queued task promotes.
198
+
199
+ ---
200
+
201
+ ## Drivers (the agent axis)
202
+
203
+ | Driver | id | transport | steer | interact | resume | tui |
204
+ |--------|----|-----------|:-----:|:--------:|:------:|:---:|
205
+ | `ClaudeDriver` | `claude` | `claude` CLI, stream-json (+ `--effort`, partial messages) | ✓ | — | ✓ | ✓ |
206
+ | `CodexDriver` | `codex` | `codex app-server` JSON-RPC (HITL via `requestUserInput`) | ✓ | via askUser | ✓ | ✓ |
207
+ | `GeminiDriver` | `gemini` | `gemini --output-format stream-json` | — | — | ✓ | ✓ |
208
+ | `HermesDriver` | `hermes` | ACP `session/update` | — | — | ✓ | — |
209
+ | `EchoDriver` | `echo` | none (hermetic, in-process) | ✓ | ✓ | ✓ | ✓ |
210
+
211
+ Write your own by implementing `AgentDriver` and passing it to `createLoom({ drivers })` (or
212
+ `loom.registerDriver(...)`). A driver normalizes its agent into `DriverEvent`s:
213
+
214
+ ```ts
215
+ type DriverEvent =
216
+ | { type: 'session'; sessionId: string }
217
+ | { type: 'text'; delta: string }
218
+ | { type: 'reasoning'; delta: string }
219
+ | { type: 'tool'; call: UniversalToolCall } // emit on tool start AND completion (status update)
220
+ | { type: 'plan'; plan: UniversalPlan }
221
+ | { type: 'subagent'; subagent: UniversalSubAgent }
222
+ | { type: 'usage'; usage: Partial<UniversalUsage> }
223
+ | { type: 'artifact'; artifact: UniversalArtifact }
224
+ | { type: 'activity'; line: string }; // explicit activity line (drivers that don't emit tool events)
225
+
226
+ interface AgentDriver {
227
+ readonly id: string;
228
+ readonly capabilities?: { steer?: boolean; interact?: boolean; resume?: boolean; tui?: boolean };
229
+ run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>; // ctx: { signal, emit, askUser, registerSteer }
230
+ tui?(input: TuiInput): TuiSpec;
231
+ }
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Surfaces & Ports
237
+
238
+ **Surfaces** (上层) bind to `LoomIO`. Built in: `WebSurface` (a WebSocket host speaking the
239
+ wire protocol — any pikichannel-style client connects), `CliSurface`. Implement `Surface` to
240
+ add an IM channel or your own UI.
241
+
242
+ **Ports** (side) — all optional, each with a default, so `createLoom()` runs with zero config:
243
+
244
+ | Port | Default | Swap to… |
245
+ |------|---------|----------|
246
+ | `SessionStore` | `FsSessionStore` (`~/.<ns>/sessions`) | your DB / transcript store |
247
+ | `ModelResolver` | `NullModelResolver` (native login) | BYOK credential/provider injection |
248
+ | `ToolProvider` | `NoopToolProvider` | per-session MCP servers |
249
+ | `SystemPromptBuilder` | `PassthroughSystemPromptBuilder` | your system/developer prompt |
250
+ | `Catalog` | `NoopCatalog` | model/effort/tool/skill discovery for composers |
251
+ | `InteractionHandler` | `DeferToTerminalInteractionHandler` | programmatic HITL answers (`AutoCancelInteractionHandler` for one-shots) |
252
+
253
+ **Plugins** are the registration unit for everything ONE capability adds to a session —
254
+ register many, composed deterministically (and dynamically via `loom.registerPlugin(...)`):
255
+
256
+ ```ts
257
+ interface Plugin {
258
+ id: string;
259
+ tools?(opts: { agent; workdir }): McpServerSpec[]; // MCP servers
260
+ promptFragment?(opts: { agent; workdir; isFirstTurn }): string | null; // how-to-use / behavior prompt
261
+ contributeSpawn?(opts: { agent; workdir; mode: 'run'|'tui'; sessionId?; model? }): SpawnContribution | null; // { env?, extraArgs?, configOverrides? }
262
+ decorateSnapshot?(snapshot): UniversalSnapshot;
263
+ }
264
+ ```
67
265
 
68
- Wire shape: `UniversalSnapshot` (phase/text/reasoning/plan/toolCalls/usage/interactions/artifacts)
69
- + delta `diffSnapshot`/`applySnapshotPatch` (prefix-append text, `undefined→null` field-clears
70
- that survive JSON).
266
+ The kernel merges contributions per spawn — **never mutating global `process.env`** — in order
267
+ `[ModelResolver ToolProvider.env → plugins (registration order)]`, so a plugin can override
268
+ the resolver (e.g. point an agent's `ANTHROPIC_BASE_URL` at a local proxy). `promptFragment`s are
269
+ appended to the `SystemPromptBuilder` base and delivered via each agent's native mechanism. This
270
+ is how a capability registers its tools **and** their usage prompt **and** any env/flags in one
271
+ place — and how a model-traffic interceptor injects a redirect on both the `run()` and `tui()`
272
+ rails without the kernel knowing anything about it. (The singular `ModelResolver` /
273
+ `SystemPromptBuilder` ports remain the one authoritative model-credential / base-prompt source;
274
+ plugins are the composable per-capability layer on top.)
71
275
 
72
- ## What's included
276
+ ---
73
277
 
74
- - Drivers: `EchoDriver` (hermetic), `ClaudeDriver` (real `claude` CLI, stream-json + TUI), `CodexDriver` (app-server JSON-RPC + TUI), `GeminiDriver` (stream-json), `HermesDriver` (ACP).
75
- - Terminals: `WebSurface` (ws host speaking the wire protocol — any pikichannel client connects), `CliSurface`.
76
- - TUI: `PtyBridge` + `attachTui` + `Loom.runTui/openTui` (raw PTY passthrough with tee).
77
- - Ports: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`.
278
+ ## Exports
78
279
 
79
- ## Publishing / CI
280
+ Main entry `@pikiloom/kernel` re-exports everything. Subpaths: `@pikiloom/kernel/drivers`,
281
+ `@pikiloom/kernel/surfaces`, `@pikiloom/kernel/protocol`.
80
282
 
81
- `npm publish ./packages/kernel --access public` (scoped-public). Each pikiloom release bumps
82
- + builds the kernel (`scripts/release.sh`) and the Release workflow publishes it alongside
83
- pikiloom needs the `@pikiloom` npm scope to exist and `NPM_TOKEN` to have publish access.
283
+ - Runtime: `createLoom`, `Loom`, `Hub`, `SessionRunner`, `runTurn`, `PtyBridge`, `ptyAvailable`, `attachTui`
284
+ - Drivers: `EchoDriver`, `ClaudeDriver`, `CodexDriver`, `GeminiDriver`, `HermesDriver`
285
+ - Surfaces: `WebSurface`, `CliSurface`
286
+ - Ports/defaults: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`, `DeferToTerminalInteractionHandler`, `NoopCatalog`, `defaultBaseDir`
287
+ - Protocol: `UniversalSnapshot`, `diffSnapshot`, `applySnapshotPatch`, `emptySnapshot`, `PROTOCOL_VERSION`, all wire/`Client*`/`Server*` message types
288
+ - Types: `AgentDriver`, `AgentTurnInput`, `DriverContext`, `DriverEvent`, `DriverResult`, `LoomIO`, `PromptInput`, `Surface`, `Plugin`, `SpawnContribution`, `SessionStore`, `ModelResolver`, `ToolProvider`, `SystemPromptBuilder`, `InteractionHandler`, `Catalog`, …
84
289
 
85
- > Known rough edge: node-pty's prebuilt `spawn-helper` can lose its executable bit on some
86
- > npm installs (`posix_spawnp failed`). Fix: `chmod +x node_modules/node-pty/prebuilds/*/spawn-helper`.
290
+ ---
87
291
 
88
- ## Verification
292
+ ## Verify
89
293
 
90
294
  ```bash
91
295
  npm run typecheck # tsc, clean
92
- npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow
296
+ npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow, driver parsers
93
297
  KERNEL_E2E_REAL=1 npm test # also drives the real `claude` CLI end-to-end
94
298
  node examples/smoke.mjs # smoke against the compiled dist
95
299
  ```
96
300
 
97
- Status: contracts + runtime + Echo/Claude drivers + Web terminal + default ports are
98
- implemented and **E2E-verified** (hermetic + real-claude + compiled-artifact). The kernel
99
- lives beside `src/` and does **not** touch the existing pikiloom app.
301
+ See `examples/` for a runnable web console, a Feishu/Lark terminal, and a Node smoke test.
100
302
 
101
- ## Roadmap (gated cutover)
303
+ ## License
102
304
 
103
- 1. Port the remaining drivers (Codex/Gemini/Hermes-ACP) behind the same `AgentDriver` contract.
104
- 2. Promote pikiloom's concrete IM channels to reference `Surface` adapters (`@pikiloom/kernel/surfaces/*`).
105
- 3. Add the full pikichannel transport (WebRTC + rendezvous + TURN + `/api` tunnel) as a `WebSurface` option.
106
- 4. Re-point the pikiloom app's `main.ts` at `createLoom({...})`; keep multi-session queue / promotion / goal
107
- auto-continuation in the app (not the kernel). Switch over only after parity is proven on DEV.
305
+ MIT
@@ -65,11 +65,28 @@ export interface Surface {
65
65
  start(io: LoomIO, host?: TuiHost): Promise<void>;
66
66
  stop(): Promise<void>;
67
67
  }
68
+ export interface SpawnContribution {
69
+ env?: Record<string, string>;
70
+ extraArgs?: string[];
71
+ configOverrides?: string[];
72
+ }
68
73
  export interface Plugin {
69
74
  readonly id: string;
70
75
  tools?(opts: {
71
76
  agent: string;
72
77
  workdir: string;
73
78
  }): McpServerSpec[] | Promise<McpServerSpec[]>;
79
+ promptFragment?(opts: {
80
+ agent: string;
81
+ workdir: string;
82
+ isFirstTurn: boolean;
83
+ }): string | null | undefined | Promise<string | null | undefined>;
84
+ contributeSpawn?(opts: {
85
+ agent: string;
86
+ workdir: string;
87
+ mode: 'run' | 'tui';
88
+ sessionId?: string | null;
89
+ model?: string | null;
90
+ }): SpawnContribution | null | undefined | Promise<SpawnContribution | null | undefined>;
74
91
  decorateSnapshot?(snapshot: UniversalSnapshot): UniversalSnapshot;
75
92
  }
@@ -1,5 +1,5 @@
1
1
  import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec } from '../contracts/driver.js';
2
- import type { UniversalPlan } from '../protocol/index.js';
2
+ import type { UniversalUsage, UniversalPlan } from '../protocol/index.js';
3
3
  export declare class ClaudeDriver implements AgentDriver {
4
4
  private readonly bin;
5
5
  readonly id = "claude";
@@ -14,6 +14,17 @@ export declare class ClaudeDriver implements AgentDriver {
14
14
  run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
15
15
  private usage;
16
16
  }
17
+ export interface ClaudeUsageState {
18
+ input: number | null;
19
+ output: number | null;
20
+ cached: number | null;
21
+ cacheCreation?: number | null;
22
+ contextWindow?: number | null;
23
+ turnOutputTokensBase?: number | null;
24
+ }
25
+ export declare function claudeUsageOf(s: ClaudeUsageState): UniversalUsage;
26
+ export declare function claudeContextWindowFromModel(model: unknown): number | null;
27
+ export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
17
28
  export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
18
29
  export declare function claudeUserMessage(text: string): string;
19
30
  export declare function emitClaudeImages(blocks: any[], s: any, emit: (e: DriverEvent) => void): void;
@@ -46,6 +46,8 @@ export class ClaudeDriver {
46
46
  sessionId: null, model: null,
47
47
  stopReason: null, error: null,
48
48
  input: null, output: null, cached: null,
49
+ cacheCreation: null,
50
+ contextWindow: null, turnOutputTokensBase: 0,
49
51
  subAgents: new Map(),
50
52
  tools: new Map(),
51
53
  };
@@ -141,9 +143,45 @@ export class ClaudeDriver {
141
143
  });
142
144
  }
143
145
  usage(s) {
144
- return { inputTokens: s.input, outputTokens: s.output, cachedInputTokens: s.cached, contextPercent: null };
146
+ return claudeUsageOf(s);
145
147
  }
146
148
  }
149
+ export function claudeUsageOf(s) {
150
+ const used = (s.input ?? 0) + (s.cached ?? 0) + (s.cacheCreation ?? 0) + (s.output ?? 0);
151
+ const window = s.contextWindow ?? null;
152
+ const contextPercent = window && used > 0 ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
153
+ const turnOutput = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
154
+ return {
155
+ inputTokens: s.input,
156
+ outputTokens: s.output,
157
+ cachedInputTokens: s.cached,
158
+ contextUsedTokens: used > 0 ? used : null,
159
+ contextPercent,
160
+ turnOutputTokens: turnOutput > 0 ? turnOutput : null,
161
+ };
162
+ }
163
+ // Advertised context window by Claude model id (best-effort; unknown -> null so the
164
+ // percent simply stays absent rather than wrong). Anchor-free so vendor-prefixed ids
165
+ // (us.anthropic.claude-…) still match.
166
+ export function claudeContextWindowFromModel(model) {
167
+ const id = String(model ?? '').trim().toLowerCase();
168
+ if (!id)
169
+ return null;
170
+ if (id === 'haiku' || /claude-haiku-/.test(id))
171
+ return 200_000;
172
+ if (id === 'opus' || id === 'sonnet' || id === 'fable')
173
+ return 1_000_000;
174
+ if (/claude-(opus|sonnet)-/.test(id) || /claude-fable-/.test(id))
175
+ return 1_000_000;
176
+ return null;
177
+ }
178
+ // Usable window = advertised minus Claude's max-output (20k) + autocompact (13k) reserve.
179
+ const CLAUDE_USABLE_WINDOW_RESERVE = 33_000;
180
+ export function claudeEffectiveContextWindow(advertised) {
181
+ if (advertised == null)
182
+ return null;
183
+ return advertised <= CLAUDE_USABLE_WINDOW_RESERVE ? advertised : advertised - CLAUDE_USABLE_WINDOW_RESERVE;
184
+ }
147
185
  // Parse one claude stream-json event into kernel DriverEvents (pure + exported for
148
186
  // hermetic testing). Faithful to pikiloom's claudeParse shapes.
149
187
  export function handleClaudeEvent(ev, s, emit) {
@@ -178,16 +216,21 @@ export function handleClaudeEvent(ev, s, emit) {
178
216
  emit({ type: 'session', sessionId: ev.session_id });
179
217
  }
180
218
  s.model = ev.model ?? s.model;
219
+ s.contextWindow = claudeEffectiveContextWindow(claudeContextWindowFromModel(s.model)) ?? s.contextWindow;
181
220
  return;
182
221
  }
183
222
  if (t === 'stream_event') {
184
223
  const inner = ev.event || {};
185
224
  if (inner.type === 'message_start') {
186
225
  const u = inner.message?.usage;
187
- if (u) {
188
- s.input = u.input_tokens ?? s.input;
189
- s.cached = u.cache_read_input_tokens ?? s.cached;
190
- }
226
+ // Claude emits one message per tool-use round within a single turn. Carry the prior
227
+ // message's output into the per-turn base, then reset to the new message's prompt size
228
+ // so contextUsedTokens tracks current occupancy while turnOutputTokens sums the turn.
229
+ s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
230
+ s.input = u?.input_tokens ?? 0;
231
+ s.cached = u?.cache_read_input_tokens ?? 0;
232
+ s.cacheCreation = u?.cache_creation_input_tokens ?? 0;
233
+ s.output = 0;
191
234
  }
192
235
  else if (inner.type === 'content_block_delta') {
193
236
  const d = inner.delta || {};
@@ -211,7 +254,9 @@ export function handleClaudeEvent(ev, s, emit) {
211
254
  s.input = u.input_tokens;
212
255
  if (u.cache_read_input_tokens != null)
213
256
  s.cached = u.cache_read_input_tokens;
214
- emit({ type: 'usage', usage: { inputTokens: s.input, outputTokens: s.output, cachedInputTokens: s.cached, contextPercent: null } });
257
+ if (u.cache_creation_input_tokens != null)
258
+ s.cacheCreation = u.cache_creation_input_tokens;
259
+ emit({ type: 'usage', usage: claudeUsageOf(s) });
215
260
  }
216
261
  if (inner.delta?.stop_reason)
217
262
  s.stopReason = inner.delta.stop_reason;
@@ -312,6 +357,8 @@ export function handleClaudeEvent(ev, s, emit) {
312
357
  const cached = u.cache_read_input_tokens ?? u.cached_input_tokens;
313
358
  if (s.cached == null && cached != null)
314
359
  s.cached = cached;
360
+ if (s.cacheCreation == null && u.cache_creation_input_tokens != null)
361
+ s.cacheCreation = u.cache_creation_input_tokens;
315
362
  }
316
363
  return;
317
364
  }
@@ -1,4 +1,5 @@
1
1
  import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, TuiInput, TuiSpec } from '../contracts/driver.js';
2
+ import type { UniversalUsage } from '../protocol/index.js';
2
3
  export declare class CodexDriver implements AgentDriver {
3
4
  private readonly bin;
4
5
  readonly id = "codex";
@@ -12,3 +13,12 @@ export declare class CodexDriver implements AgentDriver {
12
13
  run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
13
14
  tui(input: TuiInput): TuiSpec;
14
15
  }
16
+ export interface CodexUsageState {
17
+ input: number | null;
18
+ output: number | null;
19
+ cached: number | null;
20
+ contextUsed?: number | null;
21
+ contextWindow?: number | null;
22
+ }
23
+ export declare function applyCodexTokenUsage(s: CodexUsageState, rawUsage: any): void;
24
+ export declare function codexUsageOf(s: CodexUsageState): UniversalUsage;