pikiloom 0.4.37 → 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.
- package/dist/agent/kernel-bridge.js +206 -0
- package/dist/agent/stream.js +19 -1
- package/dist/cli/kernel-app.js +115 -0
- package/dist/cli/main.js +7 -0
- package/package.json +4 -2
- package/packages/kernel/README.md +107 -0
- package/packages/kernel/dist/contracts/driver.d.ts +95 -0
- package/packages/kernel/dist/contracts/driver.js +1 -0
- package/packages/kernel/dist/contracts/ports.d.ts +84 -0
- package/packages/kernel/dist/contracts/ports.js +1 -0
- package/packages/kernel/dist/contracts/surface.d.ts +75 -0
- package/packages/kernel/dist/contracts/surface.js +1 -0
- package/packages/kernel/dist/drivers/claude.d.ts +23 -0
- package/packages/kernel/dist/drivers/claude.js +453 -0
- package/packages/kernel/dist/drivers/codex.d.ts +14 -0
- package/packages/kernel/dist/drivers/codex.js +346 -0
- package/packages/kernel/dist/drivers/echo.d.ts +20 -0
- package/packages/kernel/dist/drivers/echo.js +61 -0
- package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
- package/packages/kernel/dist/drivers/gemini.js +143 -0
- package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
- package/packages/kernel/dist/drivers/hermes.js +194 -0
- package/packages/kernel/dist/drivers/index.d.ts +5 -0
- package/packages/kernel/dist/drivers/index.js +5 -0
- package/packages/kernel/dist/index.d.ts +18 -0
- package/packages/kernel/dist/index.js +29 -0
- package/packages/kernel/dist/ports/defaults.d.ts +59 -0
- package/packages/kernel/dist/ports/defaults.js +137 -0
- package/packages/kernel/dist/protocol/index.d.ts +309 -0
- package/packages/kernel/dist/protocol/index.js +59 -0
- package/packages/kernel/dist/runtime/hub.d.ts +65 -0
- package/packages/kernel/dist/runtime/hub.js +260 -0
- package/packages/kernel/dist/runtime/loom.d.ts +44 -0
- package/packages/kernel/dist/runtime/loom.js +69 -0
- package/packages/kernel/dist/runtime/pty.d.ts +23 -0
- package/packages/kernel/dist/runtime/pty.js +69 -0
- package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
- package/packages/kernel/dist/runtime/session-runner.js +210 -0
- package/packages/kernel/dist/runtime/tui.d.ts +8 -0
- package/packages/kernel/dist/runtime/tui.js +35 -0
- package/packages/kernel/dist/runtime/turn.d.ts +17 -0
- package/packages/kernel/dist/runtime/turn.js +16 -0
- package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
- package/packages/kernel/dist/surfaces/cli.js +65 -0
- package/packages/kernel/dist/surfaces/index.d.ts +2 -0
- package/packages/kernel/dist/surfaces/index.js +2 -0
- package/packages/kernel/dist/surfaces/web.d.ts +39 -0
- package/packages/kernel/dist/surfaces/web.js +244 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
// Minimal ACP (Agent Client Protocol) ndjson JSON-RPC client — same wire as codex
|
|
3
|
+
// app-server. Used by the Hermes driver to drive any-model agents over ACP.
|
|
4
|
+
class AcpClient {
|
|
5
|
+
bin;
|
|
6
|
+
args;
|
|
7
|
+
env;
|
|
8
|
+
proc = null;
|
|
9
|
+
buf = '';
|
|
10
|
+
nextId = 1;
|
|
11
|
+
pending = new Map();
|
|
12
|
+
notify;
|
|
13
|
+
constructor(bin, args, env) {
|
|
14
|
+
this.bin = bin;
|
|
15
|
+
this.args = args;
|
|
16
|
+
this.env = env;
|
|
17
|
+
}
|
|
18
|
+
onNotification(cb) { this.notify = cb; }
|
|
19
|
+
start() {
|
|
20
|
+
try {
|
|
21
|
+
this.proc = spawn(this.bin, this.args, { stdio: ['pipe', 'pipe', 'pipe'], env: this.env ? { ...process.env, ...this.env } : process.env });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
this.proc.stdout.on('data', (chunk) => this.onData(chunk));
|
|
27
|
+
this.proc.on('close', () => { for (const cb of this.pending.values())
|
|
28
|
+
cb({ error: { message: 'acp exited' } }); this.pending.clear(); });
|
|
29
|
+
this.proc.on('error', () => { });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
onData(chunk) {
|
|
33
|
+
this.buf += chunk.toString('utf8');
|
|
34
|
+
const lines = this.buf.split('\n');
|
|
35
|
+
this.buf = lines.pop() || '';
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
if (!line.trim())
|
|
38
|
+
continue;
|
|
39
|
+
let m;
|
|
40
|
+
try {
|
|
41
|
+
m = JSON.parse(line);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (m.method && m.id != null) {
|
|
47
|
+
this.respond(m.id, {});
|
|
48
|
+
} // agent->client request: ack
|
|
49
|
+
else if (m.id != null) {
|
|
50
|
+
const cb = this.pending.get(m.id);
|
|
51
|
+
if (cb) {
|
|
52
|
+
this.pending.delete(m.id);
|
|
53
|
+
cb(m);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (m.method)
|
|
57
|
+
this.notify?.(m.method, m.params ?? {});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
request(method, params, timeoutMs = 60_000) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
if (!this.proc || this.proc.killed) {
|
|
63
|
+
resolve({ error: { message: 'not connected' } });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const id = this.nextId++;
|
|
67
|
+
const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `ACP '${method}' timed out` } }); }, timeoutMs);
|
|
68
|
+
this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
|
|
69
|
+
try {
|
|
70
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
this.pending.delete(id);
|
|
75
|
+
resolve({ error: { message: 'write failed' } });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
respond(id, result) { try {
|
|
80
|
+
this.proc?.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
81
|
+
}
|
|
82
|
+
catch { /* closed */ } }
|
|
83
|
+
kill() { try {
|
|
84
|
+
this.proc?.kill('SIGTERM');
|
|
85
|
+
}
|
|
86
|
+
catch { /* ignore */ } this.proc = null; }
|
|
87
|
+
}
|
|
88
|
+
export class HermesDriver {
|
|
89
|
+
bin;
|
|
90
|
+
id = 'hermes';
|
|
91
|
+
capabilities = { steer: false, interact: false, resume: true, tui: false };
|
|
92
|
+
constructor(bin = 'hermes') {
|
|
93
|
+
this.bin = bin;
|
|
94
|
+
}
|
|
95
|
+
async run(input, ctx) {
|
|
96
|
+
const client = new AcpClient(this.bin, ['acp'], input.env);
|
|
97
|
+
const s = { text: '', reasoning: '', sessionId: input.sessionId ?? null, contextWindow: null, contextUsed: null, error: null };
|
|
98
|
+
const tools = new Set();
|
|
99
|
+
if (!client.start())
|
|
100
|
+
return { ok: false, text: '', error: 'failed to start hermes acp', stopReason: 'error' };
|
|
101
|
+
const onAbort = () => { try {
|
|
102
|
+
client.request('session/cancel', { sessionId: s.sessionId });
|
|
103
|
+
}
|
|
104
|
+
catch { /* ignore */ } client.kill(); };
|
|
105
|
+
if (ctx.signal.aborted)
|
|
106
|
+
onAbort();
|
|
107
|
+
else
|
|
108
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
109
|
+
client.onNotification((method, params) => {
|
|
110
|
+
if (method !== 'session/update')
|
|
111
|
+
return;
|
|
112
|
+
applyHermesUpdate(params?.update ?? params, s, tools, ctx.emit);
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
const init = await client.request('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } } });
|
|
116
|
+
if (init.error)
|
|
117
|
+
return { ok: false, text: '', error: init.error.message || 'initialize failed', stopReason: 'error' };
|
|
118
|
+
if (!s.sessionId) {
|
|
119
|
+
const ns = await client.request('session/new', { cwd: input.workdir, mcpServers: [] });
|
|
120
|
+
if (ns.error)
|
|
121
|
+
return { ok: false, text: '', error: ns.error.message || 'session/new failed', stopReason: 'error' };
|
|
122
|
+
s.sessionId = ns.result?.sessionId || ns.result?.session_id || null;
|
|
123
|
+
if (s.sessionId)
|
|
124
|
+
ctx.emit({ type: 'session', sessionId: s.sessionId });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
await client.request('session/load', { sessionId: s.sessionId, cwd: input.workdir, mcpServers: [] }, 30_000).catch(() => ({}));
|
|
128
|
+
}
|
|
129
|
+
if (!s.sessionId)
|
|
130
|
+
return { ok: false, text: '', error: 'hermes returned no session id', stopReason: 'error' };
|
|
131
|
+
if (input.model)
|
|
132
|
+
await client.request('session/set_model', { sessionId: s.sessionId, modelId: input.model }, 15_000).catch(() => ({}));
|
|
133
|
+
const promptResp = await client.request('session/prompt', {
|
|
134
|
+
sessionId: s.sessionId,
|
|
135
|
+
prompt: [{ type: 'text', text: input.prompt }],
|
|
136
|
+
}, 7_200_000);
|
|
137
|
+
const usage = { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextUsedTokens: s.contextUsed, contextPercent: null };
|
|
138
|
+
if (ctx.signal.aborted)
|
|
139
|
+
return { ok: false, text: s.text, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: s.sessionId, usage };
|
|
140
|
+
if (promptResp.error)
|
|
141
|
+
return { ok: false, text: s.text, error: promptResp.error.message || 'session/prompt failed', stopReason: 'error', sessionId: s.sessionId, usage };
|
|
142
|
+
const stopReason = promptResp.result?.stopReason ?? 'end_turn';
|
|
143
|
+
return { ok: !s.error, text: s.text, reasoning: s.reasoning || undefined, error: s.error, stopReason, sessionId: s.sessionId, usage };
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
client.kill();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function applyHermesUpdate(u, s, tools, emit) {
|
|
151
|
+
if (!u)
|
|
152
|
+
return;
|
|
153
|
+
switch (u.sessionUpdate) {
|
|
154
|
+
case 'agent_message_chunk': {
|
|
155
|
+
const t = u.content?.text;
|
|
156
|
+
if (typeof t === 'string' && t) {
|
|
157
|
+
s.text += t;
|
|
158
|
+
emit({ type: 'text', delta: t });
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
case 'agent_thought_chunk': {
|
|
163
|
+
const t = u.content?.text;
|
|
164
|
+
if (typeof t === 'string' && t) {
|
|
165
|
+
s.reasoning += t;
|
|
166
|
+
emit({ type: 'reasoning', delta: t });
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
case 'tool_call': {
|
|
171
|
+
const id = typeof u.toolCallId === 'string' ? u.toolCallId : '';
|
|
172
|
+
const title = (typeof u.title === 'string' && u.title.trim()) || 'tool';
|
|
173
|
+
if (id && !tools.has(id)) {
|
|
174
|
+
tools.add(id);
|
|
175
|
+
emit({ type: 'tool', call: { id, name: title, summary: title, status: 'running' } });
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
case 'tool_call_update': {
|
|
180
|
+
const id = typeof u.toolCallId === 'string' ? u.toolCallId : '';
|
|
181
|
+
const title = (typeof u.title === 'string' && u.title.trim()) || 'tool';
|
|
182
|
+
if (id && (u.status === 'completed' || u.status === 'failed'))
|
|
183
|
+
emit({ type: 'tool', call: { id, name: title, summary: title, status: u.status === 'failed' ? 'failed' : 'done' } });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
case 'usage_update': {
|
|
187
|
+
if (typeof u.used === 'number') {
|
|
188
|
+
s.contextUsed = u.used;
|
|
189
|
+
emit({ type: 'usage', usage: { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextUsedTokens: u.used, contextPercent: null } });
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EchoDriver } from './echo.js';
|
|
2
|
+
export { ClaudeDriver, handleClaudeEvent, todoWriteToPlan } from './claude.js';
|
|
3
|
+
export { CodexDriver } from './codex.js';
|
|
4
|
+
export { GeminiDriver, parseGeminiEvent } from './gemini.js';
|
|
5
|
+
export { HermesDriver, applyHermesUpdate } from './hermes.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EchoDriver } from './echo.js';
|
|
2
|
+
export { ClaudeDriver, handleClaudeEvent, todoWriteToPlan } from './claude.js';
|
|
3
|
+
export { CodexDriver } from './codex.js';
|
|
4
|
+
export { GeminiDriver, parseGeminiEvent } from './gemini.js';
|
|
5
|
+
export { HermesDriver, applyHermesUpdate } from './hermes.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { createLoom, type Loom, type LoomConfig, type TuiLaunchOptions } from './runtime/loom.js';
|
|
2
|
+
export { Hub } from './runtime/hub.js';
|
|
3
|
+
export { SessionRunner } from './runtime/session-runner.js';
|
|
4
|
+
export { runTurn, type RunTurnOptions, type TurnOutcome } from './runtime/turn.js';
|
|
5
|
+
export { PtyBridge, ptyAvailable, type PtyExit, type PtyOpenOpts } from './runtime/pty.js';
|
|
6
|
+
export { attachTui, type AttachTuiOptions } from './runtime/tui.js';
|
|
7
|
+
export type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, SteerFn, McpServerSpec, TuiInput, TuiSpec, } from './contracts/driver.js';
|
|
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';
|
|
10
|
+
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
11
|
+
export { EchoDriver } from './drivers/echo.js';
|
|
12
|
+
export { ClaudeDriver } from './drivers/claude.js';
|
|
13
|
+
export { CodexDriver } from './drivers/codex.js';
|
|
14
|
+
export { GeminiDriver } from './drivers/gemini.js';
|
|
15
|
+
export { HermesDriver } from './drivers/hermes.js';
|
|
16
|
+
export { WebSurface, type WebSurfaceOptions } from './surfaces/web.js';
|
|
17
|
+
export { CliSurface } from './surfaces/cli.js';
|
|
18
|
+
export * from './protocol/index.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @pikiloom/kernel — heterogeneous coding agents -> an interaction-friendly,
|
|
2
|
+
// accumulating session snapshot + control handle, over pluggable surfaces.
|
|
3
|
+
//
|
|
4
|
+
// const loom = createLoom({
|
|
5
|
+
// drivers: [new ClaudeDriver()], // 下层 (unchanged)
|
|
6
|
+
// surfaces: [new WebSurface({ port: 8787 })], // 上层 (IM / Web / tunnel)
|
|
7
|
+
// })
|
|
8
|
+
// await loom.start()
|
|
9
|
+
//
|
|
10
|
+
// pikiloom itself is just a consumer of this package.
|
|
11
|
+
export { createLoom } from './runtime/loom.js';
|
|
12
|
+
export { Hub } from './runtime/hub.js';
|
|
13
|
+
export { SessionRunner } from './runtime/session-runner.js';
|
|
14
|
+
export { runTurn } from './runtime/turn.js';
|
|
15
|
+
export { PtyBridge, ptyAvailable } from './runtime/pty.js';
|
|
16
|
+
export { attachTui } from './runtime/tui.js';
|
|
17
|
+
// AgentInfo + Model/Effort/Tool/Skill descriptors are wire vocabulary — exported via protocol below.
|
|
18
|
+
// Default ports
|
|
19
|
+
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
20
|
+
// Drivers & surfaces (also available via subpath exports)
|
|
21
|
+
export { EchoDriver } from './drivers/echo.js';
|
|
22
|
+
export { ClaudeDriver } from './drivers/claude.js';
|
|
23
|
+
export { CodexDriver } from './drivers/codex.js';
|
|
24
|
+
export { GeminiDriver } from './drivers/gemini.js';
|
|
25
|
+
export { HermesDriver } from './drivers/hermes.js';
|
|
26
|
+
export { WebSurface } from './surfaces/web.js';
|
|
27
|
+
export { CliSurface } from './surfaces/cli.js';
|
|
28
|
+
// Protocol (the wire vocabulary; shared with transports)
|
|
29
|
+
export * from './protocol/index.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { UniversalSnapshot, ModelDescriptor, EffortOption, ToolDescriptor, SkillDescriptor } from '../protocol/index.js';
|
|
2
|
+
import type { SessionStore, CoreSessionRecord, ModelResolver, ToolProvider, SystemPromptBuilder, InteractionHandler, Catalog } from '../contracts/ports.js';
|
|
3
|
+
export declare class FsSessionStore implements SessionStore {
|
|
4
|
+
private readonly baseDir;
|
|
5
|
+
constructor(baseDir: string);
|
|
6
|
+
private dir;
|
|
7
|
+
private recordPath;
|
|
8
|
+
private turnsPath;
|
|
9
|
+
ensure(agent: string, opts: {
|
|
10
|
+
sessionId?: string | null;
|
|
11
|
+
title?: string | null;
|
|
12
|
+
workdir: string;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
sessionId: string;
|
|
15
|
+
workspacePath: string;
|
|
16
|
+
}>;
|
|
17
|
+
get(agent: string, sessionId: string): Promise<CoreSessionRecord | null>;
|
|
18
|
+
save(record: CoreSessionRecord): Promise<void>;
|
|
19
|
+
list(agent: string, opts?: {
|
|
20
|
+
limit?: number;
|
|
21
|
+
}): Promise<CoreSessionRecord[]>;
|
|
22
|
+
recordResult(agent: string, sessionId: string, result: {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
error?: string | null;
|
|
25
|
+
text?: string;
|
|
26
|
+
sessionId?: string | null;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
appendTurn(agent: string, sessionId: string, turn: UniversalSnapshot): Promise<void>;
|
|
29
|
+
history(agent: string, sessionId: string): Promise<UniversalSnapshot[]>;
|
|
30
|
+
}
|
|
31
|
+
export declare function defaultBaseDir(appNamespace: string): string;
|
|
32
|
+
export declare class NullModelResolver implements ModelResolver {
|
|
33
|
+
resolve(): Promise<null>;
|
|
34
|
+
}
|
|
35
|
+
export declare class NoopToolProvider implements ToolProvider {
|
|
36
|
+
provideForSession(): Promise<{
|
|
37
|
+
servers: [];
|
|
38
|
+
env?: Record<string, string>;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
export declare class PassthroughSystemPromptBuilder implements SystemPromptBuilder {
|
|
42
|
+
compose(opts: {
|
|
43
|
+
agent: string;
|
|
44
|
+
base?: string;
|
|
45
|
+
isFirstTurn: boolean;
|
|
46
|
+
}): string | undefined;
|
|
47
|
+
}
|
|
48
|
+
export declare class AutoCancelInteractionHandler implements InteractionHandler {
|
|
49
|
+
askUser(): Promise<Record<string, string[]>>;
|
|
50
|
+
}
|
|
51
|
+
export declare class DeferToTerminalInteractionHandler implements InteractionHandler {
|
|
52
|
+
askUser(): Promise<null>;
|
|
53
|
+
}
|
|
54
|
+
export declare class NoopCatalog implements Catalog {
|
|
55
|
+
listModels(): Promise<ModelDescriptor[]>;
|
|
56
|
+
listEffort(): Promise<EffortOption[]>;
|
|
57
|
+
listTools(): Promise<ToolDescriptor[]>;
|
|
58
|
+
listSkills(): Promise<SkillDescriptor[]>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
// ---- FsSessionStore: the default local persistence + workspace backend ----
|
|
6
|
+
export class FsSessionStore {
|
|
7
|
+
baseDir;
|
|
8
|
+
constructor(baseDir) {
|
|
9
|
+
this.baseDir = baseDir;
|
|
10
|
+
}
|
|
11
|
+
dir(agent, sessionId) {
|
|
12
|
+
return path.join(this.baseDir, agent, sessionId);
|
|
13
|
+
}
|
|
14
|
+
recordPath(agent, sessionId) {
|
|
15
|
+
return path.join(this.dir(agent, sessionId), 'record.json');
|
|
16
|
+
}
|
|
17
|
+
turnsPath(agent, sessionId) {
|
|
18
|
+
return path.join(this.dir(agent, sessionId), 'turns.jsonl');
|
|
19
|
+
}
|
|
20
|
+
async ensure(agent, opts) {
|
|
21
|
+
const sessionId = (opts.sessionId && opts.sessionId.trim()) || randomUUID();
|
|
22
|
+
const workspacePath = path.join(this.dir(agent, sessionId), 'workspace');
|
|
23
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
24
|
+
const existing = await this.get(agent, sessionId);
|
|
25
|
+
if (!existing) {
|
|
26
|
+
const now = new Date().toISOString();
|
|
27
|
+
await this.save({
|
|
28
|
+
agent, sessionId, workspacePath,
|
|
29
|
+
createdAt: now, updatedAt: now,
|
|
30
|
+
title: opts.title ?? null, runState: 'running', runDetail: null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return { sessionId, workspacePath };
|
|
34
|
+
}
|
|
35
|
+
async get(agent, sessionId) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = fs.readFileSync(this.recordPath(agent, sessionId), 'utf8');
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async save(record) {
|
|
45
|
+
const p = this.recordPath(record.agent, record.sessionId);
|
|
46
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
47
|
+
fs.writeFileSync(p, JSON.stringify({ ...record, updatedAt: new Date().toISOString() }, null, 2));
|
|
48
|
+
}
|
|
49
|
+
async list(agent, opts) {
|
|
50
|
+
const agentDir = path.join(this.baseDir, agent);
|
|
51
|
+
let ids = [];
|
|
52
|
+
try {
|
|
53
|
+
ids = fs.readdirSync(agentDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const records = [];
|
|
59
|
+
for (const id of ids) {
|
|
60
|
+
const rec = await this.get(agent, id);
|
|
61
|
+
if (rec)
|
|
62
|
+
records.push(rec);
|
|
63
|
+
}
|
|
64
|
+
records.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
|
|
65
|
+
return opts?.limit ? records.slice(0, opts.limit) : records;
|
|
66
|
+
}
|
|
67
|
+
async recordResult(agent, sessionId, result) {
|
|
68
|
+
const rec = await this.get(agent, sessionId);
|
|
69
|
+
if (!rec)
|
|
70
|
+
return;
|
|
71
|
+
rec.runState = result.ok ? 'completed' : 'incomplete';
|
|
72
|
+
rec.runDetail = result.error ?? null;
|
|
73
|
+
if (result.sessionId)
|
|
74
|
+
rec.nativeSessionId = result.sessionId;
|
|
75
|
+
if (result.text && !rec.title)
|
|
76
|
+
rec.title = result.text.slice(0, 80);
|
|
77
|
+
await this.save(rec);
|
|
78
|
+
}
|
|
79
|
+
async appendTurn(agent, sessionId, turn) {
|
|
80
|
+
const p = this.turnsPath(agent, sessionId);
|
|
81
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
82
|
+
fs.appendFileSync(p, JSON.stringify(turn) + '\n');
|
|
83
|
+
}
|
|
84
|
+
async history(agent, sessionId) {
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = fs.readFileSync(this.turnsPath(agent, sessionId), 'utf8');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const line of raw.split('\n')) {
|
|
94
|
+
const t = line.trim();
|
|
95
|
+
if (!t)
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
out.push(JSON.parse(t));
|
|
99
|
+
}
|
|
100
|
+
catch { /* skip a corrupt/partial line */ }
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function defaultBaseDir(appNamespace) {
|
|
106
|
+
return path.join(os.homedir(), `.${appNamespace}`, 'sessions');
|
|
107
|
+
}
|
|
108
|
+
// ---- Trivial defaults: native login, no tools, passthrough prompt, auto-cancel HITL ----
|
|
109
|
+
export class NullModelResolver {
|
|
110
|
+
async resolve() { return null; }
|
|
111
|
+
}
|
|
112
|
+
export class NoopToolProvider {
|
|
113
|
+
async provideForSession() { return { servers: [] }; }
|
|
114
|
+
}
|
|
115
|
+
export class PassthroughSystemPromptBuilder {
|
|
116
|
+
compose(opts) {
|
|
117
|
+
return opts.isFirstTurn ? opts.base : undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Headless HITL resolver: cancel every interaction immediately (empty answers).
|
|
121
|
+
export class AutoCancelInteractionHandler {
|
|
122
|
+
async askUser() { return {}; }
|
|
123
|
+
}
|
|
124
|
+
// Default HITL resolver: defer to a terminal. Returns null so the interaction stays
|
|
125
|
+
// pending in the snapshot until a terminal calls interact() — the right default for an
|
|
126
|
+
// IM/Web-attached app. Headless callers opt into AutoCancelInteractionHandler instead.
|
|
127
|
+
export class DeferToTerminalInteractionHandler {
|
|
128
|
+
async askUser() { return null; }
|
|
129
|
+
}
|
|
130
|
+
// Default catalog: empty. The kernel knows zero models/effort/tools/skills; an app
|
|
131
|
+
// supplies them via its own Catalog impl. Agent capabilities come from the driver registry.
|
|
132
|
+
export class NoopCatalog {
|
|
133
|
+
async listModels() { return []; }
|
|
134
|
+
async listEffort() { return []; }
|
|
135
|
+
async listTools() { return []; }
|
|
136
|
+
async listSkills() { return []; }
|
|
137
|
+
}
|