@pikiloom/kernel 0.1.1 → 0.1.2
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.
- package/README.md +242 -65
- package/llms.txt +98 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,107 +1,284 @@
|
|
|
1
1
|
# @pikiloom/kernel
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
accumulating session snapshot + control
|
|
5
|
-
(IM, Web, tunnel). Environmental concerns
|
|
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
|
|
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
|
-
|
|
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
|
|
25
|
+
上层 (you write): IM bindings · Web UI · Plugins ── implement Surface / Plugin
|
|
14
26
|
▲ createLoom({ surfaces, plugins, ...ports })
|
|
15
|
-
@pikiloom/kernel:
|
|
16
|
-
(one import, runtime/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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** —
|
|
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
|
-
|
|
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 {
|
|
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()],
|
|
33
|
-
surfaces: [new WebSurface({ port: 8787 })],
|
|
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);
|
|
100
|
+
```
|
|
101
|
+
|
|
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
|
|
36
110
|
```
|
|
37
111
|
|
|
38
|
-
|
|
39
|
-
`
|
|
40
|
-
|
|
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).
|
|
41
115
|
|
|
42
|
-
|
|
116
|
+
---
|
|
43
117
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
152
|
+
Sub-shapes:
|
|
153
|
+
|
|
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
|
+
```
|
|
161
|
+
|
|
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.
|
|
57
241
|
|
|
58
|
-
|
|
242
|
+
**Ports** (side) — all optional, each with a default, so `createLoom()` runs with zero config:
|
|
59
243
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
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) |
|
|
67
252
|
|
|
68
|
-
|
|
69
|
-
+ delta `diffSnapshot`/`applySnapshotPatch` (prefix-append text, `undefined→null` field-clears
|
|
70
|
-
that survive JSON).
|
|
253
|
+
**Plugins** contribute per-session tools (`tools()`) and/or augment snapshots (`decorateSnapshot()`).
|
|
71
254
|
|
|
72
|
-
|
|
255
|
+
---
|
|
73
256
|
|
|
74
|
-
|
|
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`.
|
|
257
|
+
## Exports
|
|
78
258
|
|
|
79
|
-
|
|
259
|
+
Main entry `@pikiloom/kernel` re-exports everything. Subpaths: `@pikiloom/kernel/drivers`,
|
|
260
|
+
`@pikiloom/kernel/surfaces`, `@pikiloom/kernel/protocol`.
|
|
80
261
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
262
|
+
- Runtime: `createLoom`, `Loom`, `Hub`, `SessionRunner`, `runTurn`, `PtyBridge`, `ptyAvailable`, `attachTui`
|
|
263
|
+
- Drivers: `EchoDriver`, `ClaudeDriver`, `CodexDriver`, `GeminiDriver`, `HermesDriver`
|
|
264
|
+
- Surfaces: `WebSurface`, `CliSurface`
|
|
265
|
+
- Ports/defaults: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`, `DeferToTerminalInteractionHandler`, `NoopCatalog`, `defaultBaseDir`
|
|
266
|
+
- Protocol: `UniversalSnapshot`, `diffSnapshot`, `applySnapshotPatch`, `emptySnapshot`, `PROTOCOL_VERSION`, all wire/`Client*`/`Server*` message types
|
|
267
|
+
- Types: `AgentDriver`, `AgentTurnInput`, `DriverContext`, `DriverEvent`, `DriverResult`, `LoomIO`, `PromptInput`, `Surface`, `Plugin`, `SessionStore`, `ModelResolver`, `ToolProvider`, `SystemPromptBuilder`, `InteractionHandler`, `Catalog`, …
|
|
84
268
|
|
|
85
|
-
|
|
86
|
-
> npm installs (`posix_spawnp failed`). Fix: `chmod +x node_modules/node-pty/prebuilds/*/spawn-helper`.
|
|
269
|
+
---
|
|
87
270
|
|
|
88
|
-
##
|
|
271
|
+
## Verify
|
|
89
272
|
|
|
90
273
|
```bash
|
|
91
274
|
npm run typecheck # tsc, clean
|
|
92
|
-
npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow
|
|
275
|
+
npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow, driver parsers
|
|
93
276
|
KERNEL_E2E_REAL=1 npm test # also drives the real `claude` CLI end-to-end
|
|
94
277
|
node examples/smoke.mjs # smoke against the compiled dist
|
|
95
278
|
```
|
|
96
279
|
|
|
97
|
-
|
|
98
|
-
implemented and **E2E-verified** (hermetic + real-claude + compiled-artifact). The kernel
|
|
99
|
-
lives beside `src/` and does **not** touch the existing pikiloom app.
|
|
280
|
+
See `examples/` for a runnable web console, a Feishu/Lark terminal, and a Node smoke test.
|
|
100
281
|
|
|
101
|
-
##
|
|
282
|
+
## License
|
|
102
283
|
|
|
103
|
-
|
|
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.
|
|
284
|
+
MIT
|
package/llms.txt
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @pikiloom/kernel
|
|
2
|
+
|
|
3
|
+
> Reusable core that turns heterogeneous coding-agent CLIs (Claude Code, Codex, Gemini, ACP/Hermes)
|
|
4
|
+
> into ONE uniform, accumulating `UniversalSnapshot` + control verbs (prompt/stop/steer/interact),
|
|
5
|
+
> exposed over pluggable surfaces (IM, Web, tunnel, raw PTY). You read one snapshot shape and never
|
|
6
|
+
> parse a CLI's native output. ESM-only, Node >= 20. Runtime dep: `ws`; optional: `node-pty` (raw TUI).
|
|
7
|
+
|
|
8
|
+
This file is an LLM-oriented summary. Full prose: README.md. Exact types: the bundled `dist/**/*.d.ts`.
|
|
9
|
+
|
|
10
|
+
## Install & import
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm i @pikiloom/kernel
|
|
14
|
+
```
|
|
15
|
+
ESM only. Main export re-exports everything; subpaths: `@pikiloom/kernel/drivers`, `/surfaces`, `/protocol`.
|
|
16
|
+
|
|
17
|
+
## Three usage tiers
|
|
18
|
+
|
|
19
|
+
1) ONE turn, no persistence — `runTurn(driver, input, opts?) => Promise<{ result, snapshot }>`:
|
|
20
|
+
```ts
|
|
21
|
+
import { runTurn, ClaudeDriver } from '@pikiloom/kernel';
|
|
22
|
+
const { result, snapshot } = await runTurn(
|
|
23
|
+
new ClaudeDriver(),
|
|
24
|
+
{ prompt: 'hi', workdir: process.cwd(), effort: 'high' }, // AgentTurnInput
|
|
25
|
+
{ onSnapshot: (s) => render(s), onSteer: (steer) => {}, signal }, // RunTurnOptions (signal = AbortSignal to stop)
|
|
26
|
+
);
|
|
27
|
+
// result: { ok, text, reasoning?, error?, stopReason?, sessionId?, usage? }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2) FULL multi-session backend — `createLoom(config) => Loom`:
|
|
31
|
+
```ts
|
|
32
|
+
import { createLoom, ClaudeDriver, WebSurface } from '@pikiloom/kernel';
|
|
33
|
+
const loom = createLoom({ drivers: [new ClaudeDriver()], surfaces: [new WebSurface({ port: 8787 })], defaultAgent: 'claude' });
|
|
34
|
+
await loom.start();
|
|
35
|
+
const { sessionKey, taskId } = await loom.io.prompt({ prompt: 'hi', agent: 'claude' });
|
|
36
|
+
const unsub = loom.io.subscribe((key, snapshot, patch, seq) => render(snapshot));
|
|
37
|
+
```
|
|
38
|
+
Every port has a default (zero config). Override any: sessionStore, modelResolver, toolProvider, systemPromptBuilder, catalog, interactionHandler, plugins, serialPerSession, appNamespace, workdir, defaultAgent, log.
|
|
39
|
+
|
|
40
|
+
3) RAW TUI passthrough — `loom.runTui({ agent, workdir })` (full stdio passthrough) or `loom.openTui(...) => PtyBridge` (onData/write/resize/onExit). Needs optional `node-pty`; `ptyAvailable()` reports it.
|
|
41
|
+
|
|
42
|
+
## UniversalSnapshot — the shape you render (accumulates across a turn)
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
interface UniversalSnapshot {
|
|
46
|
+
phase: 'idle'|'queued'|'streaming'|'done';
|
|
47
|
+
taskId?; sessionId?; agent?; model?; effort?; prompt?: string|null;
|
|
48
|
+
text?: string; // accumulated assistant output (append)
|
|
49
|
+
reasoning?: string; // accumulated thinking (append; empty if the model/auth withholds it — see gotchas)
|
|
50
|
+
activity?: string; // human-readable execution trail, kernel-derived from toolCalls (one line per tool, status suffix)
|
|
51
|
+
plan?: { explanation: string|null; steps: { text: string; status: 'pending'|'inProgress'|'completed' }[] } | null;
|
|
52
|
+
toolCalls?: { id; name; summary; input?; result?; status: 'running'|'done'|'failed' }[];
|
|
53
|
+
subAgents?: { id; kind; description; model; tools[]; status }[];
|
|
54
|
+
usage?: { inputTokens; outputTokens; cachedInputTokens; contextUsedTokens?; contextPercent; providerName? } | null;
|
|
55
|
+
artifacts?: { url?; path?; fileName; mime?; kind: 'photo'|'document'; caption? }[];
|
|
56
|
+
interactions?: { promptId; kind: 'user-input'|'permission'|'confirmation'; title; questions: { id; text; type?: 'text'|'select'; choices? }[] }[];
|
|
57
|
+
queued?: { taskId; prompt }[];
|
|
58
|
+
error?: string|null; incomplete?: boolean; startedAt?; updatedAt: number;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
Wire delta helpers: `diffSnapshot(prev,next) => SnapshotPatch` (prefix-append text/reasoning; undefined→null clears) and `applySnapshotPatch(prev,patch)`. `emptySnapshot()`, `PROTOCOL_VERSION`.
|
|
62
|
+
|
|
63
|
+
## Control verbs (LoomIO, via loom.io)
|
|
64
|
+
|
|
65
|
+
- `prompt({ prompt, agent?, sessionKey?, workdir?, model?, effort?, attachments? }) => Promise<{ sessionKey, taskId }>`
|
|
66
|
+
- `stop(sessionKey) => boolean` (interrupts current turn; queued tasks stay and the next promotes)
|
|
67
|
+
- `steer(taskId, prompt, attachments?) => Promise<boolean>` (mid-turn; only drivers with capabilities.steer)
|
|
68
|
+
- `interact(promptId, 'select'|'text'|'skip'|'cancel', value?) => boolean` (answer a pending interaction)
|
|
69
|
+
- `subscribe((sessionKey, snapshot, patch, seq) => void) => unsubscribe`
|
|
70
|
+
- `getSnapshot(sessionKey)`, `getHistory(sessionKey)`, `listSessions()`, `listAgents()`, `listAgentInfo()`
|
|
71
|
+
- discovery: `listModels(agent)`, `listEffort(agent, model?)`, `listTools(agent, workdir?)`, `listSkills(agent, workdir?)`
|
|
72
|
+
Concurrent prompts to one session queue by default (serialPerSession=true).
|
|
73
|
+
|
|
74
|
+
## Drivers (pass to createLoom({drivers}) or loom.registerDriver)
|
|
75
|
+
|
|
76
|
+
- ClaudeDriver (id 'claude'): `claude` CLI stream-json, supports --effort; steer ✓, resume ✓, tui ✓
|
|
77
|
+
- CodexDriver (id 'codex'): `codex app-server` JSON-RPC; steer ✓, HITL via askUser, resume ✓, tui ✓
|
|
78
|
+
- GeminiDriver (id 'gemini'): `gemini` stream-json; resume ✓, tui ✓
|
|
79
|
+
- HermesDriver (id 'hermes'): ACP session/update; resume ✓
|
|
80
|
+
- EchoDriver (id 'echo'): hermetic in-process test driver; all caps ✓
|
|
81
|
+
|
|
82
|
+
Write a driver: implement `AgentDriver { id; capabilities?; run(input, ctx); tui?(input) }`. In run(), call `ctx.emit(event)`:
|
|
83
|
+
`DriverEvent = {type:'session',sessionId} | {type:'text',delta} | {type:'reasoning',delta} | {type:'tool',call:UniversalToolCall} | {type:'plan',plan} | {type:'subagent',subagent} | {type:'usage',usage} | {type:'artifact',artifact} | {type:'activity',line}`.
|
|
84
|
+
`ctx = { signal: AbortSignal, emit, askUser(interaction)=>Promise<answers>, registerSteer(fn) }`. Return `DriverResult { ok, text, reasoning?, error?, stopReason?, sessionId?, usage? }`. Emit a `tool` event on BOTH start (status 'running') and completion (status 'done'/'failed'); the kernel projects `activity` from these.
|
|
85
|
+
|
|
86
|
+
## Surfaces & Ports
|
|
87
|
+
|
|
88
|
+
- Surface (上层, binds LoomIO): built-in `WebSurface` (ws host, wire protocol), `CliSurface`. Implement `Surface { id; capabilities?; start(io, host?); stop() }` for an IM channel.
|
|
89
|
+
- Plugin: `{ id; tools?({agent,workdir}); decorateSnapshot?(snapshot) }`.
|
|
90
|
+
- Ports (defaults in parens): SessionStore (FsSessionStore), ModelResolver (NullModelResolver = native login; override for BYOK), ToolProvider (NoopToolProvider), SystemPromptBuilder (PassthroughSystemPromptBuilder), Catalog (NoopCatalog), InteractionHandler (DeferToTerminalInteractionHandler; AutoCancelInteractionHandler for one-shots). `defaultBaseDir(ns)`.
|
|
91
|
+
|
|
92
|
+
## Gotchas (read before using)
|
|
93
|
+
|
|
94
|
+
- ESM only; Node >= 20. `node-pty` is OPTIONAL — required only for tui()/runTui()/openTui(); a missing one degrades gracefully (`ptyAvailable()===false`), it never breaks import.
|
|
95
|
+
- `reasoning` is only populated when the agent streams plaintext thinking. Subscription/OAuth Claude withholds it (streams only an encrypted signature) → `reasoning` empty; BYOK Anthropic API and Codex DO stream it. Not a kernel bug.
|
|
96
|
+
- `activity` is derived by the kernel from structured `toolCalls`+`subAgents` (you don't build it). Rich UIs can use `toolCalls` directly; simple/text surfaces can use the `activity` string.
|
|
97
|
+
- The kernel bakes in ZERO model knowledge: model/effort/tool/skill lists come from the Catalog port (your app's SSOT). Per-agent capabilities come from the driver registry (`listAgentInfo()`).
|
|
98
|
+
- `stop` interrupts only the running turn (queued tasks promote); it is not "cancel everything".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikiloom/kernel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Heterogeneous coding agents (Claude / Codex / Gemini / ACP) -> an interaction-friendly, accumulating session snapshot + control handle, over pluggable surfaces (IM, Web, tunnel, TUI). The reusable core pikiloom itself is built on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"files": [
|
|
31
31
|
"dist/",
|
|
32
|
-
"README.md"
|
|
32
|
+
"README.md",
|
|
33
|
+
"llms.txt"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
36
|
"build": "tsc -p tsconfig.json",
|