pikiloom 0.4.36 → 0.4.38

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 (50) hide show
  1. package/dist/agent/drivers/codex.js +20 -3
  2. package/dist/agent/index.js +2 -2
  3. package/dist/agent/kernel-bridge.js +206 -0
  4. package/dist/agent/stream.js +60 -2
  5. package/dist/cli/kernel-app.js +115 -0
  6. package/dist/cli/main.js +7 -0
  7. package/package.json +4 -2
  8. package/packages/kernel/README.md +107 -0
  9. package/packages/kernel/dist/contracts/driver.d.ts +95 -0
  10. package/packages/kernel/dist/contracts/driver.js +1 -0
  11. package/packages/kernel/dist/contracts/ports.d.ts +84 -0
  12. package/packages/kernel/dist/contracts/ports.js +1 -0
  13. package/packages/kernel/dist/contracts/surface.d.ts +75 -0
  14. package/packages/kernel/dist/contracts/surface.js +1 -0
  15. package/packages/kernel/dist/drivers/claude.d.ts +23 -0
  16. package/packages/kernel/dist/drivers/claude.js +453 -0
  17. package/packages/kernel/dist/drivers/codex.d.ts +14 -0
  18. package/packages/kernel/dist/drivers/codex.js +346 -0
  19. package/packages/kernel/dist/drivers/echo.d.ts +20 -0
  20. package/packages/kernel/dist/drivers/echo.js +61 -0
  21. package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
  22. package/packages/kernel/dist/drivers/gemini.js +143 -0
  23. package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
  24. package/packages/kernel/dist/drivers/hermes.js +194 -0
  25. package/packages/kernel/dist/drivers/index.d.ts +5 -0
  26. package/packages/kernel/dist/drivers/index.js +5 -0
  27. package/packages/kernel/dist/index.d.ts +18 -0
  28. package/packages/kernel/dist/index.js +29 -0
  29. package/packages/kernel/dist/ports/defaults.d.ts +59 -0
  30. package/packages/kernel/dist/ports/defaults.js +137 -0
  31. package/packages/kernel/dist/protocol/index.d.ts +309 -0
  32. package/packages/kernel/dist/protocol/index.js +59 -0
  33. package/packages/kernel/dist/runtime/hub.d.ts +65 -0
  34. package/packages/kernel/dist/runtime/hub.js +260 -0
  35. package/packages/kernel/dist/runtime/loom.d.ts +44 -0
  36. package/packages/kernel/dist/runtime/loom.js +69 -0
  37. package/packages/kernel/dist/runtime/pty.d.ts +23 -0
  38. package/packages/kernel/dist/runtime/pty.js +69 -0
  39. package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
  40. package/packages/kernel/dist/runtime/session-runner.js +210 -0
  41. package/packages/kernel/dist/runtime/tui.d.ts +8 -0
  42. package/packages/kernel/dist/runtime/tui.js +35 -0
  43. package/packages/kernel/dist/runtime/turn.d.ts +17 -0
  44. package/packages/kernel/dist/runtime/turn.js +16 -0
  45. package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
  46. package/packages/kernel/dist/surfaces/cli.js +65 -0
  47. package/packages/kernel/dist/surfaces/index.d.ts +2 -0
  48. package/packages/kernel/dist/surfaces/index.js +2 -0
  49. package/packages/kernel/dist/surfaces/web.d.ts +39 -0
  50. package/packages/kernel/dist/surfaces/web.js +244 -0
@@ -0,0 +1,309 @@
1
+ export declare const PROTOCOL_VERSION: 1;
2
+ export type SessionPhase = 'idle' | 'queued' | 'streaming' | 'done';
3
+ export interface UniversalPlanStep {
4
+ text: string;
5
+ status: 'pending' | 'inProgress' | 'completed';
6
+ }
7
+ export interface UniversalPlan {
8
+ explanation: string | null;
9
+ steps: UniversalPlanStep[];
10
+ }
11
+ export interface UniversalToolCall {
12
+ id: string;
13
+ name: string;
14
+ summary: string;
15
+ input?: string | null;
16
+ result?: string | null;
17
+ status: 'running' | 'done' | 'failed';
18
+ }
19
+ export interface UniversalSubAgent {
20
+ id: string;
21
+ kind: string | null;
22
+ description: string | null;
23
+ model: string | null;
24
+ tools: Array<{
25
+ id: string;
26
+ name: string;
27
+ summary: string;
28
+ }>;
29
+ status: 'running' | 'done' | 'failed';
30
+ }
31
+ export interface UniversalUsage {
32
+ inputTokens: number | null;
33
+ outputTokens: number | null;
34
+ cachedInputTokens: number | null;
35
+ contextUsedTokens?: number | null;
36
+ contextPercent: number | null;
37
+ turnOutputTokens?: number | null;
38
+ providerName?: string | null;
39
+ }
40
+ export interface UniversalQueuedTask {
41
+ taskId: string;
42
+ prompt: string;
43
+ }
44
+ export interface UniversalArtifact {
45
+ url?: string;
46
+ path?: string;
47
+ fileName: string;
48
+ fileSize?: number;
49
+ mime?: string;
50
+ kind: 'photo' | 'document';
51
+ caption?: string;
52
+ }
53
+ export interface UniversalInteractionQuestion {
54
+ id: string;
55
+ header?: string;
56
+ text: string;
57
+ type?: 'text' | 'select' | string;
58
+ choices?: Array<{
59
+ label: string;
60
+ description?: string;
61
+ value?: string;
62
+ }>;
63
+ allowFreeform?: boolean;
64
+ allowEmpty?: boolean;
65
+ }
66
+ export interface UniversalInteraction {
67
+ promptId: string;
68
+ kind: 'user-input' | 'permission' | 'confirmation';
69
+ title: string;
70
+ hint?: string | null;
71
+ questions: UniversalInteractionQuestion[];
72
+ currentIndex?: number;
73
+ }
74
+ export interface UniversalSnapshot {
75
+ phase: SessionPhase;
76
+ taskId?: string | null;
77
+ sessionId?: string | null;
78
+ agent?: string | null;
79
+ model?: string | null;
80
+ effort?: string | null;
81
+ prompt?: string | null;
82
+ text?: string;
83
+ reasoning?: string;
84
+ activity?: string;
85
+ plan?: UniversalPlan | null;
86
+ toolCalls?: UniversalToolCall[];
87
+ subAgents?: UniversalSubAgent[];
88
+ usage?: UniversalUsage | null;
89
+ artifacts?: UniversalArtifact[];
90
+ interactions?: UniversalInteraction[];
91
+ queued?: UniversalQueuedTask[];
92
+ error?: string | null;
93
+ incomplete?: boolean;
94
+ startedAt?: number;
95
+ updatedAt: number;
96
+ }
97
+ export interface SessionMeta {
98
+ sessionKey: string;
99
+ agent?: string | null;
100
+ title?: string | null;
101
+ phase?: SessionPhase;
102
+ updatedAt?: number;
103
+ }
104
+ export interface SnapshotPatch {
105
+ full?: UniversalSnapshot;
106
+ appendText?: string;
107
+ appendReasoning?: string;
108
+ set?: Partial<UniversalSnapshot>;
109
+ }
110
+ export declare function emptySnapshot(): UniversalSnapshot;
111
+ export declare function diffSnapshot(prev: UniversalSnapshot, next: UniversalSnapshot): SnapshotPatch;
112
+ export declare function applySnapshotPatch(prev: UniversalSnapshot | null, patch: SnapshotPatch): UniversalSnapshot;
113
+ export interface AgentInfo {
114
+ id: string;
115
+ capabilities?: {
116
+ steer?: boolean;
117
+ interact?: boolean;
118
+ resume?: boolean;
119
+ tui?: boolean;
120
+ };
121
+ }
122
+ export interface ModelDescriptor {
123
+ id: string;
124
+ label?: string;
125
+ providerName?: string | null;
126
+ contextWindow?: number | null;
127
+ }
128
+ export interface EffortOption {
129
+ id: string;
130
+ label?: string;
131
+ }
132
+ export interface ToolDescriptor {
133
+ id: string;
134
+ name: string;
135
+ description?: string;
136
+ enabled?: boolean;
137
+ }
138
+ export interface SkillDescriptor {
139
+ id: string;
140
+ name: string;
141
+ description?: string;
142
+ }
143
+ export type TransportKind = 'websocket' | 'webrtc' | 'cli' | string;
144
+ export type HostCapability = 'prompt' | 'stop' | 'steer' | 'interact' | 'subscribe-all' | 'artifacts' | 'history' | 'catalog' | 'tui';
145
+ export interface HostInfo {
146
+ name: string;
147
+ version: string;
148
+ transport: TransportKind;
149
+ capabilities: HostCapability[];
150
+ authRequired?: boolean;
151
+ }
152
+ export interface ClientHello {
153
+ type: 'hello';
154
+ v: number;
155
+ client?: {
156
+ name?: string;
157
+ platform?: string;
158
+ };
159
+ token?: string;
160
+ }
161
+ export interface ClientSubscribe {
162
+ type: 'subscribe';
163
+ sessionKey: string;
164
+ }
165
+ export interface ClientUnsubscribe {
166
+ type: 'unsubscribe';
167
+ sessionKey: string;
168
+ }
169
+ export interface ClientPrompt {
170
+ type: 'prompt';
171
+ sessionKey?: string;
172
+ prompt: string;
173
+ agent?: string;
174
+ workdir?: string;
175
+ model?: string | null;
176
+ effort?: string | null;
177
+ attachments?: string[];
178
+ clientRef?: string;
179
+ }
180
+ export interface ClientStop {
181
+ type: 'stop';
182
+ sessionKey: string;
183
+ }
184
+ export interface ClientSteer {
185
+ type: 'steer';
186
+ taskId: string;
187
+ prompt: string;
188
+ }
189
+ export interface ClientInteract {
190
+ type: 'interact';
191
+ promptId: string;
192
+ action: 'select' | 'text' | 'skip' | 'cancel';
193
+ value?: string;
194
+ }
195
+ export interface ClientGetSnapshot {
196
+ type: 'getSnapshot';
197
+ sessionKey: string;
198
+ }
199
+ export interface ClientListSessions {
200
+ type: 'listSessions';
201
+ }
202
+ export interface ClientGetHistory {
203
+ type: 'getHistory';
204
+ sessionKey: string;
205
+ ref?: string;
206
+ }
207
+ export interface ClientGetCatalog {
208
+ type: 'getCatalog';
209
+ agent?: string;
210
+ model?: string | null;
211
+ workdir?: string;
212
+ ref?: string;
213
+ }
214
+ export interface ClientPing {
215
+ type: 'ping';
216
+ t?: number;
217
+ }
218
+ export interface ClientOpenTui {
219
+ type: 'openTui';
220
+ agent?: string;
221
+ workdir?: string;
222
+ model?: string | null;
223
+ sessionId?: string | null;
224
+ cols?: number;
225
+ rows?: number;
226
+ ref?: string;
227
+ }
228
+ export interface ClientTuiInput {
229
+ type: 'tuiInput';
230
+ tuiId: string;
231
+ data: string;
232
+ }
233
+ export interface ClientTuiResize {
234
+ type: 'tuiResize';
235
+ tuiId: string;
236
+ cols: number;
237
+ rows: number;
238
+ }
239
+ export interface ClientTuiClose {
240
+ type: 'tuiClose';
241
+ tuiId: string;
242
+ }
243
+ export type ClientMessage = ClientHello | ClientSubscribe | ClientUnsubscribe | ClientPrompt | ClientStop | ClientSteer | ClientInteract | ClientGetSnapshot | ClientListSessions | ClientGetHistory | ClientGetCatalog | ClientOpenTui | ClientTuiInput | ClientTuiResize | ClientTuiClose | ClientPing;
244
+ export interface ServerWelcome {
245
+ type: 'welcome';
246
+ v: number;
247
+ host: HostInfo;
248
+ sessions: SessionMeta[];
249
+ }
250
+ export interface ServerSession {
251
+ type: 'session';
252
+ sessionKey: string;
253
+ seq: number;
254
+ patch: SnapshotPatch;
255
+ }
256
+ export interface ServerSessions {
257
+ type: 'sessions';
258
+ sessions: SessionMeta[];
259
+ }
260
+ export interface ServerHistory {
261
+ type: 'history';
262
+ sessionKey: string;
263
+ turns: UniversalSnapshot[];
264
+ ref?: string;
265
+ }
266
+ export interface ServerCatalog {
267
+ type: 'catalog';
268
+ agents: AgentInfo[];
269
+ agent?: string;
270
+ models: ModelDescriptor[];
271
+ effort: EffortOption[];
272
+ tools: ToolDescriptor[];
273
+ skills: SkillDescriptor[];
274
+ ref?: string;
275
+ }
276
+ export interface ServerAccepted {
277
+ type: 'accepted';
278
+ sessionKey: string;
279
+ taskId: string;
280
+ clientRef?: string;
281
+ }
282
+ export interface ServerError {
283
+ type: 'error';
284
+ message: string;
285
+ code?: string;
286
+ clientRef?: string;
287
+ }
288
+ export interface ServerPong {
289
+ type: 'pong';
290
+ t?: number;
291
+ }
292
+ export interface ServerTuiOpened {
293
+ type: 'tuiOpened';
294
+ tuiId: string;
295
+ ref?: string;
296
+ }
297
+ export interface ServerTuiData {
298
+ type: 'tuiData';
299
+ tuiId: string;
300
+ data: string;
301
+ }
302
+ export interface ServerTuiExit {
303
+ type: 'tuiExit';
304
+ tuiId: string;
305
+ exitCode: number;
306
+ signal?: number;
307
+ }
308
+ export type ServerMessage = ServerWelcome | ServerSession | ServerSessions | ServerHistory | ServerCatalog | ServerTuiOpened | ServerTuiData | ServerTuiExit | ServerAccepted | ServerError | ServerPong;
309
+ export declare function isClientMessage(value: unknown): value is ClientMessage;
@@ -0,0 +1,59 @@
1
+ // The wire vocabulary shared by the kernel runtime and every transport/terminal.
2
+ // This is the SSOT shape: a driver-agnostic, accumulating snapshot of one session,
3
+ // plus a small set of control verbs. Ported from pikiloom's pikichannel protocol.
4
+ export const PROTOCOL_VERSION = 1;
5
+ export function emptySnapshot() {
6
+ return { phase: 'idle', updatedAt: 0 };
7
+ }
8
+ const APPEND_FIELDS = ['text', 'reasoning'];
9
+ const STRUCT_FIELDS = ['plan', 'toolCalls', 'subAgents', 'usage', 'artifacts', 'interactions', 'queued'];
10
+ const SCALAR_FIELDS = ['phase', 'taskId', 'sessionId', 'agent', 'model', 'effort', 'prompt', 'activity', 'error', 'incomplete', 'startedAt', 'updatedAt'];
11
+ export function diffSnapshot(prev, next) {
12
+ const patch = {};
13
+ let set;
14
+ // Coerce undefined -> null so a field-clear survives JSON serialization on the wire
15
+ // (JSON.stringify drops undefined-valued keys, which would otherwise leave the
16
+ // receiver's cumulative snapshot holding the previous turn's value).
17
+ const put = (k, v) => { (set ||= {})[k] = v === undefined ? null : v; };
18
+ for (const f of APPEND_FIELDS) {
19
+ const a = prev[f] ?? '';
20
+ const b = next[f] ?? '';
21
+ if (a === b)
22
+ continue;
23
+ if (typeof b === 'string' && typeof a === 'string' && b.startsWith(a)) {
24
+ if (f === 'text')
25
+ patch.appendText = b.slice(a.length);
26
+ else
27
+ patch.appendReasoning = b.slice(a.length);
28
+ }
29
+ else {
30
+ put(f, b);
31
+ }
32
+ }
33
+ for (const f of SCALAR_FIELDS) {
34
+ if (prev[f] !== next[f])
35
+ put(f, next[f]);
36
+ }
37
+ for (const f of STRUCT_FIELDS) {
38
+ if (JSON.stringify(prev[f]) !== JSON.stringify(next[f]))
39
+ put(f, next[f]);
40
+ }
41
+ if (set)
42
+ patch.set = set;
43
+ return patch;
44
+ }
45
+ export function applySnapshotPatch(prev, patch) {
46
+ if (patch.full)
47
+ return patch.full;
48
+ const next = prev ? { ...prev } : emptySnapshot();
49
+ if (patch.appendText)
50
+ next.text = (next.text || '') + patch.appendText;
51
+ if (patch.appendReasoning)
52
+ next.reasoning = (next.reasoning || '') + patch.appendReasoning;
53
+ if (patch.set)
54
+ Object.assign(next, patch.set);
55
+ return next;
56
+ }
57
+ export function isClientMessage(value) {
58
+ return !!value && typeof value === 'object' && typeof value.type === 'string';
59
+ }
@@ -0,0 +1,65 @@
1
+ import { type UniversalSnapshot, type SnapshotPatch, type SessionMeta, type AgentInfo, type ModelDescriptor, type EffortOption, type ToolDescriptor, type SkillDescriptor } from '../protocol/index.js';
2
+ import type { AgentDriver, TuiSpec } from '../contracts/driver.js';
3
+ import type { SessionStore, ModelResolver, ToolProvider, SystemPromptBuilder, InteractionHandler, Catalog } from '../contracts/ports.js';
4
+ import type { LoomIO, PromptInput, Plugin } from '../contracts/surface.js';
5
+ export interface HubDeps {
6
+ drivers: Map<string, AgentDriver>;
7
+ defaultAgent: string;
8
+ workdir: string;
9
+ sessionStore: SessionStore;
10
+ modelResolver: ModelResolver;
11
+ toolProvider: ToolProvider;
12
+ systemPromptBuilder: SystemPromptBuilder;
13
+ catalog: Catalog;
14
+ interactionHandler: InteractionHandler;
15
+ plugins: Plugin[];
16
+ serialPerSession?: boolean;
17
+ systemPromptBase?: string;
18
+ log?: (msg: string) => void;
19
+ }
20
+ export declare class Hub implements LoomIO {
21
+ private readonly deps;
22
+ private readonly sessions;
23
+ private readonly runnersByTask;
24
+ private readonly active;
25
+ private readonly waiting;
26
+ private readonly seqByKey;
27
+ private readonly updateSubs;
28
+ private readonly sessionsSubs;
29
+ constructor(deps: HubDeps);
30
+ listAgents(): string[];
31
+ listAgentInfo(): AgentInfo[];
32
+ listModels(agent: string): Promise<ModelDescriptor[]>;
33
+ listEffort(agent: string, model?: string | null): Promise<EffortOption[]>;
34
+ listTools(agent: string, workdir?: string): Promise<ToolDescriptor[]>;
35
+ listSkills(agent: string, workdir?: string): Promise<SkillDescriptor[]>;
36
+ resolveTui(opts: {
37
+ agent?: string;
38
+ workdir?: string;
39
+ model?: string | null;
40
+ sessionId?: string | null;
41
+ }): Promise<TuiSpec>;
42
+ prompt(input: PromptInput): Promise<{
43
+ sessionKey: string;
44
+ taskId: string;
45
+ }>;
46
+ private enqueue;
47
+ private runNow;
48
+ private promoteNext;
49
+ private queueView;
50
+ private publishQueued;
51
+ private collectTools;
52
+ private onRunnerUpdate;
53
+ stop(sessionKey: string): boolean;
54
+ steer(taskId: string, prompt: string, attachments?: string[]): Promise<boolean>;
55
+ interact(promptId: string, action: 'select' | 'text' | 'skip' | 'cancel', value?: string): boolean;
56
+ subscribe(cb: (k: string, s: UniversalSnapshot, p: SnapshotPatch, seq: number) => void): () => void;
57
+ onSessionsChanged(cb: (s: SessionMeta[]) => void): () => void;
58
+ private emitSessionsChanged;
59
+ listSessions(): SessionMeta[];
60
+ getSnapshot(sessionKey: string): {
61
+ snapshot: UniversalSnapshot;
62
+ seq: number;
63
+ } | null;
64
+ getHistory(sessionKey: string): Promise<UniversalSnapshot[]>;
65
+ }
@@ -0,0 +1,260 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { diffSnapshot, emptySnapshot, } from '../protocol/index.js';
3
+ import { SessionRunner } from './session-runner.js';
4
+ function splitKey(sessionKey) {
5
+ const i = sessionKey.indexOf(':');
6
+ return i < 0 ? { agent: '', sessionId: sessionKey } : { agent: sessionKey.slice(0, i), sessionId: sessionKey.slice(i + 1) };
7
+ }
8
+ export class Hub {
9
+ deps;
10
+ sessions = new Map();
11
+ runnersByTask = new Map();
12
+ active = new Map(); // sessionKey -> currently running turn
13
+ waiting = new Map(); // sessionKey -> FIFO of queued turns
14
+ seqByKey = new Map(); // monotonic publish seq per session (survives turn changes)
15
+ updateSubs = new Set();
16
+ sessionsSubs = new Set();
17
+ constructor(deps) {
18
+ this.deps = deps;
19
+ }
20
+ listAgents() { return [...this.deps.drivers.keys()]; }
21
+ // ---- discovery (capabilities from drivers; models/effort/tools/skills from Catalog) ----
22
+ listAgentInfo() {
23
+ return [...this.deps.drivers.values()].map(d => ({ id: d.id, capabilities: d.capabilities }));
24
+ }
25
+ listModels(agent) {
26
+ return this.deps.catalog.listModels({ agent }).catch((e) => { this.deps.log?.(`[hub] listModels failed: ${e?.message || e}`); return []; });
27
+ }
28
+ listEffort(agent, model) {
29
+ return this.deps.catalog.listEffort({ agent, model: model ?? null }).catch((e) => { this.deps.log?.(`[hub] listEffort failed: ${e?.message || e}`); return []; });
30
+ }
31
+ listTools(agent, workdir) {
32
+ return this.deps.catalog.listTools({ agent, workdir: workdir || this.deps.workdir }).catch((e) => { this.deps.log?.(`[hub] listTools failed: ${e?.message || e}`); return []; });
33
+ }
34
+ listSkills(agent, workdir) {
35
+ return this.deps.catalog.listSkills({ agent, workdir: workdir || this.deps.workdir }).catch((e) => { this.deps.log?.(`[hub] listSkills failed: ${e?.message || e}`); return []; });
36
+ }
37
+ // Resolve how to launch an agent's interactive TUI (with model injection applied),
38
+ // without spawning it — the caller (launcher / Loom.runTui) owns the PTY.
39
+ async resolveTui(opts) {
40
+ const agent = (opts.agent || this.deps.defaultAgent || '').trim();
41
+ const driver = this.deps.drivers.get(agent);
42
+ if (!driver)
43
+ throw new Error(`No driver registered for agent "${agent}"`);
44
+ if (!driver.tui)
45
+ throw new Error(`Driver "${agent}" does not support TUI mode`);
46
+ const workdir = opts.workdir || this.deps.workdir;
47
+ const injection = await this.deps.modelResolver.resolve(agent, { model: opts.model, profileId: null }).catch(() => null);
48
+ return driver.tui({ workdir, model: injection?.model ?? opts.model ?? null, sessionId: opts.sessionId ?? null, env: injection?.env });
49
+ }
50
+ async prompt(input) {
51
+ const agent = (input.agent || this.deps.defaultAgent || '').trim();
52
+ const driver = this.deps.drivers.get(agent);
53
+ if (!driver)
54
+ throw new Error(`No driver registered for agent "${agent}"`);
55
+ const workdir = input.workdir || this.deps.workdir;
56
+ const resumeId = input.sessionKey ? splitKey(input.sessionKey).sessionId : null;
57
+ const preExisted = resumeId ? !!(await this.deps.sessionStore.get(agent, resumeId)) : false;
58
+ const { sessionId, workspacePath } = await this.deps.sessionStore.ensure(agent, {
59
+ sessionId: resumeId, workdir, title: input.prompt.slice(0, 80),
60
+ });
61
+ const sessionKey = `${agent}:${sessionId}`;
62
+ const taskId = randomUUID();
63
+ const runner = new SessionRunner(sessionKey, agent, taskId, (snap, seq) => this.onRunnerUpdate(sessionKey, snap, seq), this.deps.interactionHandler);
64
+ this.runnersByTask.set(taskId, runner);
65
+ const item = { runner, taskId, input, driver, agent, sessionId, sessionKey, workdir, workspacePath, preExisted };
66
+ // Per-session serial orchestration (default): one turn per session at a time. A prompt
67
+ // for a busy session queues (surfaced in the active snapshot's `queued`) and promotes
68
+ // when the running turn finishes — never clobbering the in-flight turn's snapshot.
69
+ if (this.deps.serialPerSession !== false && this.active.has(sessionKey)) {
70
+ this.enqueue(item);
71
+ }
72
+ else {
73
+ void this.runNow(item);
74
+ }
75
+ return { sessionKey, taskId };
76
+ }
77
+ enqueue(item) {
78
+ const q = this.waiting.get(item.sessionKey) ?? [];
79
+ q.push(item);
80
+ this.waiting.set(item.sessionKey, q);
81
+ this.publishQueued(item.sessionKey);
82
+ }
83
+ // Build and run one turn. The sync prefix (mark active, install the session entry) runs
84
+ // before the first await, so getSnapshot() is valid the instant prompt() returns.
85
+ async runNow(item) {
86
+ const { runner, taskId, input, driver, agent, sessionId, sessionKey, workdir, workspacePath, preExisted } = item;
87
+ this.active.set(sessionKey, runner);
88
+ const prev = this.sessions.get(sessionKey);
89
+ const entry = {
90
+ meta: { sessionKey, agent, title: input.prompt.slice(0, 80), phase: 'streaming', updatedAt: Date.now() },
91
+ // Inherit the prior turn's published state so this turn's FIRST diff correctly resets
92
+ // text/tools/usage/queued on the client — no cross-turn bleed on the same session.
93
+ snapshot: runner.snapshot, lastPublished: prev?.lastPublished ?? emptySnapshot(), seq: prev?.seq ?? 0, runner,
94
+ };
95
+ this.sessions.set(sessionKey, entry);
96
+ this.emitSessionsChanged();
97
+ // Resolve injection / tools / resume-target at run time so a promoted turn sees the
98
+ // prior turn's native session id and any refreshed credentials/tools.
99
+ const rec = await this.deps.sessionStore.get(agent, sessionId);
100
+ 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
+ const model = injection?.model ?? input.model ?? null;
103
+ const effort = input.effort ?? null;
104
+ const systemPrompt = this.deps.systemPromptBuilder.compose({ agent, base: this.deps.systemPromptBase, isFirstTurn: !preExisted });
105
+ const turnInput = {
106
+ prompt: input.prompt,
107
+ attachments: input.attachments,
108
+ sessionId: rec?.nativeSessionId || (input.sessionKey ? sessionId : null),
109
+ workdir,
110
+ model, effort, systemPrompt,
111
+ env: injection?.env,
112
+ extraArgs: injection?.extraArgs, // BYOK flags from the ModelResolver
113
+ configOverrides: injection?.configOverrides, // codex BYOK -c routing
114
+ extraMcpServers: tools,
115
+ };
116
+ runner.run(driver, turnInput, input.prompt, model, effort)
117
+ .then(async (result) => {
118
+ await this.deps.sessionStore.recordResult(agent, sessionId, result);
119
+ // Persist the completed turn's final snapshot as a transcript entry. The runner's
120
+ // snapshot is stable once done (a follow-up prompt spawns a fresh runner+snapshot).
121
+ try {
122
+ await this.deps.sessionStore.appendTurn?.(agent, sessionId, runner.snapshot);
123
+ }
124
+ catch (e) {
125
+ this.deps.log?.(`[hub] appendTurn failed ${sessionKey}: ${e?.message || e}`);
126
+ }
127
+ })
128
+ .catch((err) => this.deps.log?.(`[hub] run error ${sessionKey}: ${err?.message || err}`))
129
+ .finally(() => {
130
+ this.runnersByTask.delete(taskId);
131
+ if (this.active.get(sessionKey) === runner)
132
+ this.active.delete(sessionKey);
133
+ this.promoteNext(sessionKey); // serial: start the next queued turn (queue survives stop)
134
+ this.emitSessionsChanged();
135
+ });
136
+ }
137
+ promoteNext(sessionKey) {
138
+ const q = this.waiting.get(sessionKey);
139
+ if (!q || q.length === 0) {
140
+ this.waiting.delete(sessionKey);
141
+ return;
142
+ }
143
+ const next = q.shift();
144
+ if (q.length === 0)
145
+ this.waiting.delete(sessionKey);
146
+ void this.runNow(next);
147
+ }
148
+ queueView(sessionKey) {
149
+ return (this.waiting.get(sessionKey) || []).map(it => ({ taskId: it.taskId, prompt: it.input.prompt }));
150
+ }
151
+ // Re-publish the active turn's snapshot so a queue change (enqueue/drain) reaches subscribers.
152
+ publishQueued(sessionKey) {
153
+ const entry = this.sessions.get(sessionKey);
154
+ if (entry?.runner)
155
+ this.onRunnerUpdate(sessionKey, entry.runner.snapshot, entry.runner.currentSeq);
156
+ }
157
+ async collectTools(agent, workdir, workspacePath) {
158
+ const out = [];
159
+ try {
160
+ const base = await this.deps.toolProvider.provideForSession({ agent, workdir, workspacePath });
161
+ out.push(...base.servers);
162
+ }
163
+ catch (e) {
164
+ this.deps.log?.(`[hub] toolProvider failed: ${e?.message || e}`);
165
+ }
166
+ for (const plugin of this.deps.plugins) {
167
+ if (!plugin.tools)
168
+ continue;
169
+ try {
170
+ out.push(...(await plugin.tools({ agent, workdir })));
171
+ }
172
+ catch (e) {
173
+ this.deps.log?.(`[hub] plugin ${plugin.id} tools failed: ${e?.message || e}`);
174
+ }
175
+ }
176
+ return out;
177
+ }
178
+ onRunnerUpdate(sessionKey, snapshot, _seq) {
179
+ const entry = this.sessions.get(sessionKey);
180
+ if (!entry)
181
+ return;
182
+ let decorated = snapshot;
183
+ for (const plugin of this.deps.plugins) {
184
+ if (plugin.decorateSnapshot)
185
+ decorated = plugin.decorateSnapshot(decorated);
186
+ }
187
+ const queued = this.queueView(sessionKey);
188
+ decorated = { ...decorated, queued: queued.length ? queued : undefined };
189
+ entry.snapshot = decorated;
190
+ entry.seq = (this.seqByKey.get(sessionKey) ?? entry.seq ?? 0) + 1; // monotonic per session across turns
191
+ this.seqByKey.set(sessionKey, entry.seq);
192
+ entry.meta = { ...entry.meta, phase: decorated.phase, updatedAt: decorated.updatedAt, title: entry.meta.title || decorated.prompt?.slice(0, 80) || null };
193
+ const patch = diffSnapshot(entry.lastPublished, decorated);
194
+ entry.lastPublished = JSON.parse(JSON.stringify(decorated));
195
+ for (const cb of this.updateSubs) {
196
+ try {
197
+ cb(sessionKey, decorated, patch, entry.seq);
198
+ }
199
+ catch { /* isolate subscriber */ }
200
+ }
201
+ if (decorated.phase === 'done')
202
+ this.emitSessionsChanged();
203
+ }
204
+ // ---- control verbs ----
205
+ stop(sessionKey) {
206
+ const r = this.sessions.get(sessionKey)?.runner;
207
+ if (!r)
208
+ return false;
209
+ r.stop();
210
+ return true;
211
+ }
212
+ async steer(taskId, prompt, attachments) {
213
+ const r = this.runnersByTask.get(taskId);
214
+ return r ? r.steer(prompt, attachments) : false;
215
+ }
216
+ interact(promptId, action, value) {
217
+ for (const r of this.runnersByTask.values()) {
218
+ if (r.interact(promptId, action, value))
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+ // ---- subscriptions & queries ----
224
+ subscribe(cb) {
225
+ this.updateSubs.add(cb);
226
+ return () => this.updateSubs.delete(cb);
227
+ }
228
+ onSessionsChanged(cb) {
229
+ this.sessionsSubs.add(cb);
230
+ return () => this.sessionsSubs.delete(cb);
231
+ }
232
+ emitSessionsChanged() {
233
+ const list = this.listSessions();
234
+ for (const cb of this.sessionsSubs) {
235
+ try {
236
+ cb(list);
237
+ }
238
+ catch { /* isolate */ }
239
+ }
240
+ }
241
+ listSessions() {
242
+ return [...this.sessions.values()].map(e => e.meta).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
243
+ }
244
+ getSnapshot(sessionKey) {
245
+ const e = this.sessions.get(sessionKey);
246
+ return e ? { snapshot: e.snapshot, seq: e.seq } : null;
247
+ }
248
+ async getHistory(sessionKey) {
249
+ const { agent, sessionId } = splitKey(sessionKey);
250
+ if (!agent || !this.deps.sessionStore.history)
251
+ return [];
252
+ try {
253
+ return await this.deps.sessionStore.history(agent, sessionId);
254
+ }
255
+ catch (e) {
256
+ this.deps.log?.(`[hub] history failed ${sessionKey}: ${e?.message || e}`);
257
+ return [];
258
+ }
259
+ }
260
+ }