@pikiloom/kernel 0.1.2 → 0.1.4
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 +23 -2
- package/dist/contracts/surface.d.ts +17 -0
- package/dist/drivers/claude.d.ts +12 -1
- package/dist/drivers/claude.js +53 -6
- package/dist/drivers/codex.d.ts +10 -0
- package/dist/drivers/codex.js +79 -10
- package/dist/index.d.ts +1 -1
- package/dist/runtime/hub.d.ts +3 -0
- package/dist/runtime/hub.js +84 -10
- package/dist/runtime/loom.d.ts +1 -0
- package/dist/runtime/loom.js +4 -1
- package/llms.txt +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -250,7 +250,28 @@ add an IM channel or your own UI.
|
|
|
250
250
|
| `Catalog` | `NoopCatalog` | model/effort/tool/skill discovery for composers |
|
|
251
251
|
| `InteractionHandler` | `DeferToTerminalInteractionHandler` | programmatic HITL answers (`AutoCancelInteractionHandler` for one-shots) |
|
|
252
252
|
|
|
253
|
-
**Plugins**
|
|
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
|
+
```
|
|
265
|
+
|
|
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.)
|
|
254
275
|
|
|
255
276
|
---
|
|
256
277
|
|
|
@@ -264,7 +285,7 @@ Main entry `@pikiloom/kernel` re-exports everything. Subpaths: `@pikiloom/kernel
|
|
|
264
285
|
- Surfaces: `WebSurface`, `CliSurface`
|
|
265
286
|
- Ports/defaults: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`, `DeferToTerminalInteractionHandler`, `NoopCatalog`, `defaultBaseDir`
|
|
266
287
|
- 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`, …
|
|
288
|
+
- Types: `AgentDriver`, `AgentTurnInput`, `DriverContext`, `DriverEvent`, `DriverResult`, `LoomIO`, `PromptInput`, `Surface`, `Plugin`, `SpawnContribution`, `SessionStore`, `ModelResolver`, `ToolProvider`, `SystemPromptBuilder`, `InteractionHandler`, `Catalog`, …
|
|
268
289
|
|
|
269
290
|
---
|
|
270
291
|
|
|
@@ -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
|
}
|
package/dist/drivers/claude.d.ts
CHANGED
|
@@ -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;
|
package/dist/drivers/claude.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/drivers/codex.d.ts
CHANGED
|
@@ -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;
|
package/dist/drivers/codex.js
CHANGED
|
@@ -173,7 +173,7 @@ export class CodexDriver {
|
|
|
173
173
|
// to the native account.
|
|
174
174
|
const config = [...(input.configOverrides || [])];
|
|
175
175
|
const srv = new AppServer(this.bin, config, input.env);
|
|
176
|
-
const state = { text: '', reasoning: '', sessionId: input.sessionId ?? null, input: null, output: null, cached: null, status: null, error: null, turnId: null };
|
|
176
|
+
const state = { text: '', reasoning: '', sessionId: input.sessionId ?? null, input: null, output: null, cached: null, contextUsed: null, contextWindow: null, status: null, error: null, turnId: null };
|
|
177
177
|
const phases = new Map();
|
|
178
178
|
const toolSummaries = new Map();
|
|
179
179
|
let steerRegistered = false;
|
|
@@ -264,14 +264,8 @@ export class CodexDriver {
|
|
|
264
264
|
break;
|
|
265
265
|
}
|
|
266
266
|
case 'thread/tokenUsage/updated': {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
state.input = u.input_tokens ?? u.inputTokens;
|
|
270
|
-
if (u.output_tokens != null || u.outputTokens != null)
|
|
271
|
-
state.output = u.output_tokens ?? u.outputTokens;
|
|
272
|
-
if (u.cached_input_tokens != null || u.cachedInputTokens != null)
|
|
273
|
-
state.cached = u.cached_input_tokens ?? u.cachedInputTokens;
|
|
274
|
-
ctx.emit({ type: 'usage', usage: { inputTokens: state.input, outputTokens: state.output, cachedInputTokens: state.cached, contextPercent: null } });
|
|
267
|
+
applyCodexTokenUsage(state, params?.tokenUsage || params?.usage);
|
|
268
|
+
ctx.emit({ type: 'usage', usage: codexUsageOf(state) });
|
|
275
269
|
break;
|
|
276
270
|
}
|
|
277
271
|
case 'turn/completed': {
|
|
@@ -279,6 +273,8 @@ export class CodexDriver {
|
|
|
279
273
|
state.status = turn.status ?? 'completed';
|
|
280
274
|
if (turn.error)
|
|
281
275
|
state.error = turn.error.message || turn.error.code || 'turn error';
|
|
276
|
+
applyCodexTokenUsage(state, params?.tokenUsage || turn.tokenUsage || turn.usage);
|
|
277
|
+
ctx.emit({ type: 'usage', usage: codexUsageOf(state) });
|
|
282
278
|
settle();
|
|
283
279
|
break;
|
|
284
280
|
}
|
|
@@ -309,7 +305,7 @@ export class CodexDriver {
|
|
|
309
305
|
if (turnResp.error)
|
|
310
306
|
return { ok: false, text: state.text, error: turnResp.error.message || 'turn/start failed', stopReason: 'error', sessionId: state.sessionId };
|
|
311
307
|
await turnDone;
|
|
312
|
-
const usage =
|
|
308
|
+
const usage = codexUsageOf(state);
|
|
313
309
|
const ok2 = (state.status === 'completed' || state.status == null) && !state.error && !ctx.signal.aborted;
|
|
314
310
|
return {
|
|
315
311
|
ok: ok2,
|
|
@@ -344,3 +340,76 @@ function buildTurnInput(prompt, attachments) {
|
|
|
344
340
|
input.push({ type: 'text', text: prompt });
|
|
345
341
|
return input;
|
|
346
342
|
}
|
|
343
|
+
function codexNum(...vals) {
|
|
344
|
+
for (const v of vals) {
|
|
345
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
346
|
+
if (Number.isFinite(n))
|
|
347
|
+
return n;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
// Context occupancy from one usage record: prefer total_tokens, else input(+output).
|
|
352
|
+
function codexContextUsed(raw) {
|
|
353
|
+
if (!raw || typeof raw !== 'object')
|
|
354
|
+
return null;
|
|
355
|
+
const total = codexNum(raw.total_tokens, raw.totalTokens);
|
|
356
|
+
if (total != null && total >= 0)
|
|
357
|
+
return total;
|
|
358
|
+
const i = codexNum(raw.input_tokens, raw.inputTokens);
|
|
359
|
+
const o = codexNum(raw.output_tokens, raw.outputTokens);
|
|
360
|
+
if (i != null && o != null)
|
|
361
|
+
return i + o;
|
|
362
|
+
return i;
|
|
363
|
+
}
|
|
364
|
+
// Fold a codex tokenUsage payload into driver state: last-turn counts, context
|
|
365
|
+
// occupancy, and the model window. Tolerant of nested {info:{…}} and flat shapes.
|
|
366
|
+
export function applyCodexTokenUsage(s, rawUsage) {
|
|
367
|
+
if (!rawUsage || typeof rawUsage !== 'object')
|
|
368
|
+
return;
|
|
369
|
+
const info = rawUsage.info && typeof rawUsage.info === 'object' ? rawUsage.info : rawUsage;
|
|
370
|
+
const last = info.last ?? info.lastTokenUsage ?? info.last_token_usage ?? rawUsage.last;
|
|
371
|
+
const li = codexNum(last?.input_tokens, last?.inputTokens);
|
|
372
|
+
const lo = codexNum(last?.output_tokens, last?.outputTokens);
|
|
373
|
+
const lc = codexNum(last?.cached_input_tokens, last?.cachedInputTokens);
|
|
374
|
+
if (li != null)
|
|
375
|
+
s.input = li;
|
|
376
|
+
if (lo != null)
|
|
377
|
+
s.output = lo;
|
|
378
|
+
if (lc != null)
|
|
379
|
+
s.cached = lc;
|
|
380
|
+
const used = codexContextUsed(last);
|
|
381
|
+
if (used != null)
|
|
382
|
+
s.contextUsed = used;
|
|
383
|
+
// Cumulative total as fallback for the per-turn counts (the trailing `?? rawUsage`
|
|
384
|
+
// also catches the flat shape, where there is no nested `last`).
|
|
385
|
+
const total = info.total ?? info.totalTokenUsage ?? info.total_token_usage ?? rawUsage.total ?? rawUsage;
|
|
386
|
+
if (total && typeof total === 'object') {
|
|
387
|
+
const ti = codexNum(total.input_tokens, total.inputTokens);
|
|
388
|
+
const to = codexNum(total.output_tokens, total.outputTokens);
|
|
389
|
+
const tc = codexNum(total.cached_input_tokens, total.cachedInputTokens);
|
|
390
|
+
if (li == null && ti != null)
|
|
391
|
+
s.input = ti;
|
|
392
|
+
if (lo == null && to != null)
|
|
393
|
+
s.output = to;
|
|
394
|
+
if (lc == null && tc != null)
|
|
395
|
+
s.cached = tc;
|
|
396
|
+
}
|
|
397
|
+
const cw = codexNum(info.modelContextWindow, info.model_context_window, rawUsage.modelContextWindow, rawUsage.model_context_window);
|
|
398
|
+
if (cw != null && cw > 0)
|
|
399
|
+
s.contextWindow = cw;
|
|
400
|
+
}
|
|
401
|
+
export function codexUsageOf(s) {
|
|
402
|
+
const fallback = (s.input ?? 0) + (s.cached ?? 0);
|
|
403
|
+
const used = s.contextUsed ?? (fallback > 0 ? fallback : null);
|
|
404
|
+
const window = s.contextWindow ?? null;
|
|
405
|
+
const contextPercent = used != null && window ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
|
|
406
|
+
const turnOutput = s.output ?? 0;
|
|
407
|
+
return {
|
|
408
|
+
inputTokens: s.input,
|
|
409
|
+
outputTokens: s.output,
|
|
410
|
+
cachedInputTokens: s.cached,
|
|
411
|
+
contextUsedTokens: used,
|
|
412
|
+
contextPercent,
|
|
413
|
+
turnOutputTokens: turnOutput > 0 ? turnOutput : null,
|
|
414
|
+
};
|
|
415
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { PtyBridge, ptyAvailable, type PtyExit, type PtyOpenOpts } from './runti
|
|
|
6
6
|
export { attachTui, type AttachTuiOptions } from './runtime/tui.js';
|
|
7
7
|
export type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, SteerFn, McpServerSpec, TuiInput, TuiSpec, } from './contracts/driver.js';
|
|
8
8
|
export type { SessionStore, CoreSessionRecord, ModelResolver, ModelInjection, ToolProvider, SystemPromptBuilder, InteractionHandler, Catalog, } from './contracts/ports.js';
|
|
9
|
-
export type { LoomIO, PromptInput, Surface, SurfaceCapabilities, Plugin, } from './contracts/surface.js';
|
|
9
|
+
export type { LoomIO, PromptInput, Surface, SurfaceCapabilities, Plugin, SpawnContribution, } from './contracts/surface.js';
|
|
10
10
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
11
11
|
export { EchoDriver } from './drivers/echo.js';
|
|
12
12
|
export { ClaudeDriver } from './drivers/claude.js';
|
package/dist/runtime/hub.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export declare class Hub implements LoomIO {
|
|
|
49
49
|
private queueView;
|
|
50
50
|
private publishQueued;
|
|
51
51
|
private collectTools;
|
|
52
|
+
private mergeSpawn;
|
|
53
|
+
private pluginSpawn;
|
|
54
|
+
private composeSystemPrompt;
|
|
52
55
|
private onRunnerUpdate;
|
|
53
56
|
stop(sessionKey: string): boolean;
|
|
54
57
|
steer(taskId: string, prompt: string, attachments?: string[]): Promise<boolean>;
|
package/dist/runtime/hub.js
CHANGED
|
@@ -45,7 +45,11 @@ export class Hub {
|
|
|
45
45
|
throw new Error(`Driver "${agent}" does not support TUI mode`);
|
|
46
46
|
const workdir = opts.workdir || this.deps.workdir;
|
|
47
47
|
const injection = await this.deps.modelResolver.resolve(agent, { model: opts.model, profileId: null }).catch(() => null);
|
|
48
|
-
|
|
48
|
+
const model = injection?.model ?? opts.model ?? null;
|
|
49
|
+
// Same merge as run(), so the raw-PTY rail also gets plugin env/args (e.g. a hijack redirect).
|
|
50
|
+
const pluginParts = await this.pluginSpawn(agent, workdir, 'tui', opts.sessionId ?? null, model);
|
|
51
|
+
const spawn = this.mergeSpawn([{ env: injection?.env, extraArgs: injection?.extraArgs }, ...pluginParts]);
|
|
52
|
+
return driver.tui({ workdir, model, sessionId: opts.sessionId ?? null, env: spawn.env, extraArgs: spawn.extraArgs });
|
|
49
53
|
}
|
|
50
54
|
async prompt(input) {
|
|
51
55
|
const agent = (input.agent || this.deps.defaultAgent || '').trim();
|
|
@@ -98,20 +102,28 @@ export class Hub {
|
|
|
98
102
|
// prior turn's native session id and any refreshed credentials/tools.
|
|
99
103
|
const rec = await this.deps.sessionStore.get(agent, sessionId);
|
|
100
104
|
const injection = await this.deps.modelResolver.resolve(agent, { model: input.model, profileId: null }).catch(() => null);
|
|
101
|
-
const tools = await this.collectTools(agent, workdir, workspacePath);
|
|
102
105
|
const model = injection?.model ?? input.model ?? null;
|
|
103
106
|
const effort = input.effort ?? null;
|
|
104
|
-
const
|
|
107
|
+
const tools = await this.collectTools(agent, workdir, workspacePath);
|
|
108
|
+
const pluginParts = await this.pluginSpawn(agent, workdir, 'run', sessionId, model);
|
|
109
|
+
// precedence: ModelResolver (model/creds) -> ToolProvider.env -> plugins (last, so a
|
|
110
|
+
// model-traffic hijack plugin can override the resolver's base URL).
|
|
111
|
+
const spawn = this.mergeSpawn([
|
|
112
|
+
{ env: injection?.env, extraArgs: injection?.extraArgs, configOverrides: injection?.configOverrides },
|
|
113
|
+
{ env: tools.env },
|
|
114
|
+
...pluginParts,
|
|
115
|
+
]);
|
|
116
|
+
const systemPrompt = await this.composeSystemPrompt(agent, workdir, !preExisted);
|
|
105
117
|
const turnInput = {
|
|
106
118
|
prompt: input.prompt,
|
|
107
119
|
attachments: input.attachments,
|
|
108
120
|
sessionId: rec?.nativeSessionId || (input.sessionKey ? sessionId : null),
|
|
109
121
|
workdir,
|
|
110
122
|
model, effort, systemPrompt,
|
|
111
|
-
env:
|
|
112
|
-
extraArgs:
|
|
113
|
-
configOverrides:
|
|
114
|
-
extraMcpServers: tools,
|
|
123
|
+
env: spawn.env,
|
|
124
|
+
extraArgs: spawn.extraArgs,
|
|
125
|
+
configOverrides: spawn.configOverrides,
|
|
126
|
+
extraMcpServers: tools.servers,
|
|
115
127
|
};
|
|
116
128
|
runner.run(driver, turnInput, input.prompt, model, effort)
|
|
117
129
|
.then(async (result) => {
|
|
@@ -154,11 +166,16 @@ export class Hub {
|
|
|
154
166
|
if (entry?.runner)
|
|
155
167
|
this.onRunnerUpdate(sessionKey, entry.runner.snapshot, entry.runner.currentSeq);
|
|
156
168
|
}
|
|
169
|
+
// MCP servers (ToolProvider + each plugin) PLUS the ToolProvider's session env (previously
|
|
170
|
+
// dropped) — surfaced so the spawn env merge can apply it.
|
|
157
171
|
async collectTools(agent, workdir, workspacePath) {
|
|
158
|
-
const
|
|
172
|
+
const servers = [];
|
|
173
|
+
let env;
|
|
159
174
|
try {
|
|
160
175
|
const base = await this.deps.toolProvider.provideForSession({ agent, workdir, workspacePath });
|
|
161
|
-
|
|
176
|
+
servers.push(...base.servers);
|
|
177
|
+
if (base.env && Object.keys(base.env).length)
|
|
178
|
+
env = base.env;
|
|
162
179
|
}
|
|
163
180
|
catch (e) {
|
|
164
181
|
this.deps.log?.(`[hub] toolProvider failed: ${e?.message || e}`);
|
|
@@ -167,14 +184,71 @@ export class Hub {
|
|
|
167
184
|
if (!plugin.tools)
|
|
168
185
|
continue;
|
|
169
186
|
try {
|
|
170
|
-
|
|
187
|
+
servers.push(...(await plugin.tools({ agent, workdir })));
|
|
171
188
|
}
|
|
172
189
|
catch (e) {
|
|
173
190
|
this.deps.log?.(`[hub] plugin ${plugin.id} tools failed: ${e?.message || e}`);
|
|
174
191
|
}
|
|
175
192
|
}
|
|
193
|
+
return { servers, env };
|
|
194
|
+
}
|
|
195
|
+
// Merge ordered spawn contributions: env keys later-wins, extraArgs/configOverrides concatenate.
|
|
196
|
+
// Callers order parts [ModelResolver, ToolProvider.env, ...plugins] so a plugin (e.g. a model-
|
|
197
|
+
// traffic hijack) can override the resolver's base URL. Never touches global process.env.
|
|
198
|
+
mergeSpawn(parts) {
|
|
199
|
+
let env;
|
|
200
|
+
const extraArgs = [];
|
|
201
|
+
const configOverrides = [];
|
|
202
|
+
for (const p of parts) {
|
|
203
|
+
if (!p)
|
|
204
|
+
continue;
|
|
205
|
+
if (p.env && Object.keys(p.env).length)
|
|
206
|
+
env = { ...(env || {}), ...p.env };
|
|
207
|
+
if (p.extraArgs?.length)
|
|
208
|
+
extraArgs.push(...p.extraArgs);
|
|
209
|
+
if (p.configOverrides?.length)
|
|
210
|
+
configOverrides.push(...p.configOverrides);
|
|
211
|
+
}
|
|
212
|
+
return { env, extraArgs: extraArgs.length ? extraArgs : undefined, configOverrides: configOverrides.length ? configOverrides : undefined };
|
|
213
|
+
}
|
|
214
|
+
async pluginSpawn(agent, workdir, mode, sessionId, model) {
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const plugin of this.deps.plugins) {
|
|
217
|
+
if (!plugin.contributeSpawn)
|
|
218
|
+
continue;
|
|
219
|
+
try {
|
|
220
|
+
const c = await plugin.contributeSpawn({ agent, workdir, mode, sessionId, model });
|
|
221
|
+
if (c)
|
|
222
|
+
out.push(c);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
this.deps.log?.(`[hub] plugin ${plugin.id} contributeSpawn failed: ${e?.message || e}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
176
228
|
return out;
|
|
177
229
|
}
|
|
230
|
+
// Final system/developer prompt = the singular SystemPromptBuilder base + each plugin's
|
|
231
|
+
// promptFragment (in registration order), joined. Delivered via AgentTurnInput.systemPrompt,
|
|
232
|
+
// which each driver applies its own way (claude --append-system-prompt / codex / gemini).
|
|
233
|
+
async composeSystemPrompt(agent, workdir, isFirstTurn) {
|
|
234
|
+
const parts = [];
|
|
235
|
+
const base = this.deps.systemPromptBuilder.compose({ agent, base: this.deps.systemPromptBase, isFirstTurn });
|
|
236
|
+
if (base && base.trim())
|
|
237
|
+
parts.push(base);
|
|
238
|
+
for (const plugin of this.deps.plugins) {
|
|
239
|
+
if (!plugin.promptFragment)
|
|
240
|
+
continue;
|
|
241
|
+
try {
|
|
242
|
+
const f = await plugin.promptFragment({ agent, workdir, isFirstTurn });
|
|
243
|
+
if (f && f.trim())
|
|
244
|
+
parts.push(f);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
this.deps.log?.(`[hub] plugin ${plugin.id} promptFragment failed: ${e?.message || e}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return parts.length ? parts.join('\n\n') : undefined;
|
|
251
|
+
}
|
|
178
252
|
onRunnerUpdate(sessionKey, snapshot, _seq) {
|
|
179
253
|
const entry = this.sessions.get(sessionKey);
|
|
180
254
|
if (!entry)
|
package/dist/runtime/loom.d.ts
CHANGED
package/dist/runtime/loom.js
CHANGED
|
@@ -10,6 +10,8 @@ export function createLoom(config = {}) {
|
|
|
10
10
|
for (const d of config.drivers || [])
|
|
11
11
|
drivers.set(d.id, d);
|
|
12
12
|
const defaultAgent = config.defaultAgent || config.drivers?.[0]?.id || 'echo';
|
|
13
|
+
// Held as a live reference so registerPlugin() mutates the same array the Hub iterates.
|
|
14
|
+
const plugins = [...(config.plugins || [])];
|
|
13
15
|
const hub = new Hub({
|
|
14
16
|
drivers,
|
|
15
17
|
defaultAgent,
|
|
@@ -21,7 +23,7 @@ export function createLoom(config = {}) {
|
|
|
21
23
|
catalog: config.catalog || new NoopCatalog(),
|
|
22
24
|
interactionHandler: config.interactionHandler || new DeferToTerminalInteractionHandler(),
|
|
23
25
|
serialPerSession: config.serialPerSession,
|
|
24
|
-
plugins
|
|
26
|
+
plugins,
|
|
25
27
|
systemPromptBase: config.systemPromptBase,
|
|
26
28
|
log,
|
|
27
29
|
});
|
|
@@ -36,6 +38,7 @@ export function createLoom(config = {}) {
|
|
|
36
38
|
return {
|
|
37
39
|
io: hub,
|
|
38
40
|
registerDriver(driver) { drivers.set(driver.id, driver); },
|
|
41
|
+
registerPlugin(plugin) { plugins.push(plugin); },
|
|
39
42
|
async start() {
|
|
40
43
|
if (started)
|
|
41
44
|
return;
|
package/llms.txt
CHANGED
|
@@ -86,7 +86,9 @@ Write a driver: implement `AgentDriver { id; capabilities?; run(input, ctx); tui
|
|
|
86
86
|
## Surfaces & Ports
|
|
87
87
|
|
|
88
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
|
|
89
|
+
- Plugin = the registration unit for everything ONE capability adds to a session (register via `createLoom({plugins})` or `loom.registerPlugin(p)`):
|
|
90
|
+
`{ id; tools?({agent,workdir})=>McpServerSpec[]; promptFragment?({agent,workdir,isFirstTurn})=>string|null; contributeSpawn?({agent,workdir,mode:'run'|'tui',sessionId?,model?})=>SpawnContribution|null; decorateSnapshot?(snap) }`.
|
|
91
|
+
`SpawnContribution = { env?; extraArgs?; configOverrides? }`. Kernel merges per-spawn (NEVER touches global process.env), order `[ModelResolver → ToolProvider.env → plugins]` (plugin overrides resolver — e.g. a hijack redirecting `ANTHROPIC_BASE_URL` to a local proxy, applied on BOTH run() and tui()). `promptFragment`s append to the SystemPromptBuilder base. So tools + their usage-prompt + env/flags register in ONE place. The singular ModelResolver/SystemPromptBuilder remain the authoritative model-cred/base-prompt source; plugins are the composable layer on top.
|
|
90
92
|
- 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
93
|
|
|
92
94
|
## Gotchas (read before using)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikiloom/kernel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|