@tea-agent/loop-agent 0.23.1 → 0.24.1
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/CHANGELOG.md +61 -1
- package/README.md +1 -1
- package/bin/agent-worker.js +0 -0
- package/dist/commands/init.js +2 -2
- package/dist/executors/shell-executor.js +20 -7
- package/dist/shared/operator/capabilities.js +475 -2
- package/dist/task/dag-source-paths.js +50 -0
- package/dist/task/frontend-project-capability.js +316 -0
- package/dist/task/task-demand-routing.js +432 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/artifact-card.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +495 -0
- package/dist/worker/console/chat/chat-ui-policy.js +25 -0
- package/dist/worker/console/chat/compaction-errors.js +64 -0
- package/dist/worker/console/chat/composer-draft-store.js +45 -0
- package/dist/worker/console/chat/context-panel.js +54 -0
- package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
- package/dist/worker/console/chat/explore-tools.js +299 -0
- package/dist/worker/console/chat/human-gate-card.js +37 -0
- package/dist/worker/console/chat/interview-adapter.js +149 -0
- package/dist/worker/console/chat/operation-card.js +23 -0
- package/dist/worker/console/chat/pi-console-config.js +158 -0
- package/dist/worker/console/chat/pi-runtime.js +581 -43
- package/dist/worker/console/chat/repo-browser.js +140 -0
- package/dist/worker/console/chat/repo-walk.js +116 -0
- package/dist/worker/console/chat/resource-loader.js +18 -17
- package/dist/worker/console/chat/routes.js +1359 -65
- package/dist/worker/console/chat/runtime-context.js +24 -0
- package/dist/worker/console/chat/runtime-selection.js +37 -0
- package/dist/worker/console/chat/session-store.js +210 -11
- package/dist/worker/console/chat/shortcuts.js +15 -0
- package/dist/worker/console/chat/tool-adapter.js +81 -194
- package/dist/worker/console/chat/tools.js +72 -48
- package/dist/worker/console/chat/usage.js +37 -0
- package/dist/worker/console/chat/workspace-landing.js +56 -0
- package/dist/worker/console/dag-confirmation.js +42 -8
- package/dist/worker/console/human-gate-token.js +130 -0
- package/dist/worker/console/interview/grill-me.js +28 -5
- package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
- package/dist/worker/console/operation-runner.js +6 -2
- package/dist/worker/console/operation-sse.js +26 -0
- package/dist/worker/console/operator-actions.js +420 -7
- package/dist/worker/console/server.js +14 -2
- package/dist/worker/console/static/assets/index-Cv68Ge5n.js +27 -0
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +8 -4
- package/dist/workflows/dag/backend-test-intake-context.js +10 -12
- package/dist/workflows/dag/backend-test-markdown-workflow.js +232 -47
- package/dist/workflows/dag/backend-test-result-contract.js +233 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
- package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
- package/dist/workflows/dag/frontend-project-capability.js +1 -316
- package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
- package/dist/workflows/dag/init-hybrid.js +46 -43
- package/dist/workflows/dag/rerun-plan.js +6 -10
- package/dist/workflows/dag/task-demand-routing.js +1 -383
- package/docs/README.md +1 -1
- package/docs/architecture/README.md +5 -5
- package/docs/architecture/evolution.md +4 -4
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/frontend-implementation-contract.schema.json +1 -1
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-implementation/SKILL.md +7 -0
- package/skills/frontend-implementation/references/node-contracts.md +1 -1
- package/skills/frontend-verification/SKILL.md +4 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-DVl7Jxt5.js +0 -25
- package/dist/worker/console/static/assets/index-lVcIr9Ju.css +0 -1
|
@@ -24,12 +24,15 @@
|
|
|
24
24
|
* `createDefaultPiSdkBindings()`.
|
|
25
25
|
*/
|
|
26
26
|
import { randomBytes } from "node:crypto";
|
|
27
|
+
import { projectCompactSnapshot } from "./chat-event-store.js";
|
|
27
28
|
import path from "node:path";
|
|
29
|
+
import { extractUsageSample } from "./usage.js";
|
|
28
30
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
29
31
|
import { createOperatorChatResourceLoader, } from "./resource-loader.js";
|
|
30
|
-
import { loadOperatorChatInstructionSkills, composeInstructionSkillsPrompt, } from "./instruction-skills.js";
|
|
31
|
-
import {
|
|
32
|
+
import { loadOperatorChatInstructionSkills, composeInstructionSkillsPrompt, OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS, } from "./instruction-skills.js";
|
|
33
|
+
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
32
34
|
import { resolveDefaultChatModel, } from "./model-resolver.js";
|
|
35
|
+
import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
33
36
|
/**
|
|
34
37
|
* Base system prompt fragment every General Operator Chat session receives.
|
|
35
38
|
*
|
|
@@ -47,34 +50,47 @@ import { resolveDefaultChatModel, } from "./model-resolver.js";
|
|
|
47
50
|
const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
48
51
|
"You are the General Operator Chat for loop-agent / agent-worker.",
|
|
49
52
|
"You are an OPERATOR: you orchestrate and inspect via the registered operator_* tools. You are NOT an implementer.",
|
|
50
|
-
"Tool surface: you have
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
53
|
+
"Tool surface: you have safe read-only explore tools (safe-read, safe-grep, git-status, git-diff, find, ls) to probe the repo, AND the model-callable operator_* tools. bash is NOT available (it was removed to close a write-via-redirect escape); use safe-read / safe-grep / git-status / git-diff instead.",
|
|
54
|
+
"safe-read / safe-grep enforce a repo-relative path boundary, a sensitive-file denylist (.env* / *.key / *.pem / .git/** / auth.json / sessions/**) and secret scrubbing. Do not attempt to read those files via any other path.",
|
|
55
|
+
"You do NOT have edit / write / apply_patch / full-tools / shell / coding-chat — file-WRITING is gated behind the DAG path. All canonical writes still go through DAG implement-pi / repair-pi, never via this chat.",
|
|
56
|
+
"High-risk mutations are not model-executable tools. Prepare contract via interview then apply via contractApply, prepare DAG runs via prepareDagConfirmation, prepare other mutations via prepareMutationGate, then confirm in the browser Human Gate. When a high-risk action (contractApply / runDag / dagRerun / etc.) fails on missing prepared state, use read-only tools (status, doctor, dagReport, inspect, contractShow, safe-read, safe-grep) to diagnose.",
|
|
57
|
+
"You cannot self-confirm a DAG run: confirmDagConfirmation is a human-only action (executed by the browser with a server-signed confirmation token). You may only prepare it via prepareDagConfirmation; the user must confirm in the UI.",
|
|
58
|
+
"Prefer read-only tools (status, doctor, dagReport, inspect, contractShow, safe-read, safe-grep, git-status) to diagnose before acting.",
|
|
54
59
|
].join("\n");
|
|
55
60
|
/**
|
|
56
61
|
* Built-in SDK read/explore tool names registered for every Chat session.
|
|
57
62
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
63
|
+
* M0-A (roadmap §14 工作块 A): bash is REMOVED — it could write files via
|
|
64
|
+
* `echo >` / `tee` / `sed -i` / `node -e writeFile`, bypassing the DAG write
|
|
65
|
+
* boundary. The residual read-only repo probing is served by the custom
|
|
66
|
+
* safe-read / safe-grep / find / ls tools (see buildExploreCustomTools) which
|
|
67
|
+
* enforce a repo-relative path boundary, a sensitive-file denylist, and
|
|
68
|
+
* secret scrubbing on the returned content. edit/write/... stay excluded.
|
|
62
69
|
*/
|
|
63
70
|
export const OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS = Object.freeze([
|
|
64
|
-
"bash",
|
|
65
|
-
"read",
|
|
66
|
-
"grep",
|
|
67
71
|
"find",
|
|
68
72
|
"ls",
|
|
69
73
|
]);
|
|
70
74
|
/**
|
|
71
75
|
* Canonical active tool-set handed to setActiveToolsByName at every gate:
|
|
72
|
-
* every operator action (dynamic, from the registry) PLUS the
|
|
73
|
-
*
|
|
74
|
-
*
|
|
76
|
+
* every model-callable operator action (dynamic, from the registry) PLUS the
|
|
77
|
+
* built-in explore tools (find/ls) PLUS the safe explore custom tools
|
|
78
|
+
* (safe-read / safe-grep / git-status / git-diff). bash is NOT here (M0-A).
|
|
79
|
+
* File-WRITING coding tools (edit/write/...) are never here — they are
|
|
80
|
+
* excluded at the registry level and thus cannot be activated.
|
|
75
81
|
*/
|
|
82
|
+
export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
|
|
83
|
+
"safe-read",
|
|
84
|
+
"safe-grep",
|
|
85
|
+
"git-status",
|
|
86
|
+
"git-diff",
|
|
87
|
+
]);
|
|
76
88
|
export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
|
|
77
|
-
return [
|
|
89
|
+
return [
|
|
90
|
+
...OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
91
|
+
...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
|
|
92
|
+
...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
93
|
+
];
|
|
78
94
|
}
|
|
79
95
|
/**
|
|
80
96
|
* Build Pi custom ToolDefinition objects for every whitelisted operator action.
|
|
@@ -88,7 +104,7 @@ async function buildCustomOperatorTools(options) {
|
|
|
88
104
|
import("typebox"),
|
|
89
105
|
import("@earendil-works/pi-coding-agent"),
|
|
90
106
|
]);
|
|
91
|
-
const schemas =
|
|
107
|
+
const schemas = buildModelCallableToolSchemas();
|
|
92
108
|
const ctx = options.actionContext;
|
|
93
109
|
return schemas.map((schema) => {
|
|
94
110
|
// TypeBox schema is built dynamically; cast through unknown to the builder
|
|
@@ -146,6 +162,185 @@ async function buildCustomOperatorTools(options) {
|
|
|
146
162
|
});
|
|
147
163
|
});
|
|
148
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
|
|
167
|
+
* safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
|
|
168
|
+
* the model can probe the repo WITHOUT write escapes (echo redirect, tee,
|
|
169
|
+
* sed -i, node -e) and WITHOUT reading sensitive files (env files, key/pem,
|
|
170
|
+
* auth.json, sessions dir) — and every returned byte is secret-scrubbed
|
|
171
|
+
* before reaching the model context.
|
|
172
|
+
*
|
|
173
|
+
* gitStatus/gitDiff use a FIXED argv (typed, no shell) so they cannot be
|
|
174
|
+
* turned into an arbitrary-shell vector.
|
|
175
|
+
*/
|
|
176
|
+
async function buildExploreCustomTools(options) {
|
|
177
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
178
|
+
import("typebox"),
|
|
179
|
+
import("@earendil-works/pi-coding-agent"),
|
|
180
|
+
]);
|
|
181
|
+
const { execFileSync } = await import("node:child_process");
|
|
182
|
+
const repoRoot = options.repoRoot;
|
|
183
|
+
const { safeReadFile, safeGrep, scrubSecrets } = await import("./explore-tools.js");
|
|
184
|
+
function safeGit(args) {
|
|
185
|
+
try {
|
|
186
|
+
const stdout = execFileSync("git", args, {
|
|
187
|
+
cwd: repoRoot,
|
|
188
|
+
encoding: "utf8",
|
|
189
|
+
// Fixed argv, no shell, bounded output.
|
|
190
|
+
maxBuffer: 512 * 1024,
|
|
191
|
+
timeout: 10_000,
|
|
192
|
+
windowsHide: true,
|
|
193
|
+
});
|
|
194
|
+
return { ok: true, stdout };
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
code: "GIT_FAILED",
|
|
200
|
+
message: e instanceof Error ? e.message : String(e),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const safeReadTool = defineTool({
|
|
205
|
+
name: "safe-read",
|
|
206
|
+
label: "safe-read",
|
|
207
|
+
description: "Read a repo-relative file with sensitive-file denylist (.env*/*.key/*.pem/.git/**/auth.json/sessions/**) and secret scrubbing. Absolute paths and .. are rejected.",
|
|
208
|
+
promptSnippet: "safe-read: read a repo file (safe; secrets scrubbed)",
|
|
209
|
+
parameters: Type.Object({
|
|
210
|
+
path: Type.String({ description: "repo-relative file path" }),
|
|
211
|
+
}, { additionalProperties: false }),
|
|
212
|
+
async execute(_toolCallId, params) {
|
|
213
|
+
const raw = String(params.path ?? "");
|
|
214
|
+
try {
|
|
215
|
+
const result = await safeReadFile(repoRoot, raw);
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
218
|
+
details: result,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
const err = e;
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: "text",
|
|
227
|
+
text: JSON.stringify({
|
|
228
|
+
error: err.message ?? String(e),
|
|
229
|
+
code: err.code ?? "READ_FAILED",
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
details: { error: err.message ?? String(e), code: err.code ?? "READ_FAILED" },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const safeGrepTool = defineTool({
|
|
239
|
+
name: "safe-grep",
|
|
240
|
+
label: "safe-grep",
|
|
241
|
+
description: "Line search across the repo working tree with sensitive-file denylist and secret scrubbing. pattern is a JS regex source; optional glob filters paths.",
|
|
242
|
+
promptSnippet: "safe-grep: search repo files (safe; secrets scrubbed)",
|
|
243
|
+
parameters: Type.Object({
|
|
244
|
+
pattern: Type.String({ description: "regex source (no shell)" }),
|
|
245
|
+
caseInsensitive: Type.Optional(Type.Boolean()),
|
|
246
|
+
glob: Type.Optional(Type.String({ description: "optional glob filter e.g. *.ts" })),
|
|
247
|
+
}, { additionalProperties: false }),
|
|
248
|
+
async execute(_toolCallId, params) {
|
|
249
|
+
try {
|
|
250
|
+
const result = await safeGrep(repoRoot, String(params.pattern ?? ""), {
|
|
251
|
+
caseInsensitive: Boolean(params.caseInsensitive),
|
|
252
|
+
glob: params.glob ? String(params.glob) : undefined,
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
256
|
+
details: result,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
const err = e;
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: "text",
|
|
265
|
+
text: JSON.stringify({
|
|
266
|
+
error: err.message ?? String(e),
|
|
267
|
+
code: err.code ?? "GREP_FAILED",
|
|
268
|
+
}),
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
details: { error: err.message ?? String(e), code: err.code ?? "GREP_FAILED" },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const gitStatusTool = defineTool({
|
|
277
|
+
name: "git-status",
|
|
278
|
+
label: "git-status",
|
|
279
|
+
description: "Typed `git status --porcelain` (fixed argv, no shell). Returns scrubbed working-tree status.",
|
|
280
|
+
promptSnippet: "git-status: working tree status (read-only)",
|
|
281
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
282
|
+
async execute() {
|
|
283
|
+
const r = safeGit(["status", "--porcelain"]);
|
|
284
|
+
if (!r.ok) {
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: "text", text: JSON.stringify({ error: r.message, code: r.code }) }],
|
|
287
|
+
details: { error: r.message, code: r.code },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const { scrubbed, redactions } = scrubSecrets(r.stdout);
|
|
291
|
+
const payload = { status: scrubbed, redactions };
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
294
|
+
details: payload,
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
const gitDiffTool = defineTool({
|
|
299
|
+
name: "git-diff",
|
|
300
|
+
label: "git-diff",
|
|
301
|
+
description: "Typed `git diff` (fixed argv: --stat or a single -- path; no shell). Returns scrubbed diff. path must be repo-relative and not sensitive.",
|
|
302
|
+
promptSnippet: "git-diff: diff (read-only, scrubbed)",
|
|
303
|
+
parameters: Type.Object({
|
|
304
|
+
path: Type.Optional(Type.String({ description: "optional repo-relative path to diff (default: whole tree --stat)" })),
|
|
305
|
+
}, { additionalProperties: false }),
|
|
306
|
+
async execute(_toolCallId, params) {
|
|
307
|
+
let rawPath = params.path ? String(params.path) : undefined;
|
|
308
|
+
// If a path is supplied, validate it via the read-path guard so git diff
|
|
309
|
+
// cannot be aimed at sensitive files (.env* / auth.json / sessions).
|
|
310
|
+
if (rawPath) {
|
|
311
|
+
const { guardReadPath } = await import("./explore-tools.js");
|
|
312
|
+
const g = await guardReadPath(repoRoot, rawPath);
|
|
313
|
+
if (!g.ok) {
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{ type: "text", text: JSON.stringify({ error: g.message, code: g.code }) },
|
|
317
|
+
],
|
|
318
|
+
details: { error: g.message, code: g.code },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
rawPath = g.repoRelative;
|
|
322
|
+
}
|
|
323
|
+
const args = rawPath
|
|
324
|
+
? ["diff", "--", rawPath]
|
|
325
|
+
: ["diff", "--stat"];
|
|
326
|
+
const r = safeGit(args);
|
|
327
|
+
if (!r.ok) {
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: "text", text: JSON.stringify({ error: r.message, code: r.code }) }],
|
|
330
|
+
details: { error: r.message, code: r.code },
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const { scrubbed, redactions } = scrubSecrets(r.stdout);
|
|
334
|
+
const payload = { diff: scrubbed, redactions };
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
337
|
+
details: payload,
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
return [safeReadTool, safeGrepTool, gitStatusTool, gitDiffTool];
|
|
342
|
+
}
|
|
343
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
149
344
|
/**
|
|
150
345
|
* Extract the latest assistant text fragment from an SDK message object.
|
|
151
346
|
* Tolerant of multiple message shapes the SDK emits.
|
|
@@ -176,7 +371,11 @@ export class ConsolePiRuntime {
|
|
|
176
371
|
options;
|
|
177
372
|
loader;
|
|
178
373
|
sessions = new Map();
|
|
374
|
+
modelRuntimes = new Map();
|
|
375
|
+
sessionManagers = new Map();
|
|
376
|
+
mainlineLeaves = new Map();
|
|
179
377
|
bindings;
|
|
378
|
+
disabledInstructionSkills = new Set();
|
|
180
379
|
constructor(options) {
|
|
181
380
|
this.options = options;
|
|
182
381
|
this.loader = createOperatorChatResourceLoader();
|
|
@@ -186,6 +385,17 @@ export class ConsolePiRuntime {
|
|
|
186
385
|
get sessionDir() {
|
|
187
386
|
return this.options.sessionDir;
|
|
188
387
|
}
|
|
388
|
+
/** Resolve the Pi agent directory for bounded Console configuration APIs. */
|
|
389
|
+
async getAgentDir() {
|
|
390
|
+
return safeGetAgentDir(this.bindings);
|
|
391
|
+
}
|
|
392
|
+
async testConfiguredModel(model) {
|
|
393
|
+
const { services } = await this.bindings.createServices({ cwd: this.options.cwd, agentDir: await this.getAgentDir() });
|
|
394
|
+
return { available: Boolean(this.bindings.resolveModel({ modelRuntime: services.modelRuntime, provider: model.provider, modelId: model.modelId })) };
|
|
395
|
+
}
|
|
396
|
+
setDisabledInstructionSkills(names) {
|
|
397
|
+
this.disabledInstructionSkills = new Set(names);
|
|
398
|
+
}
|
|
189
399
|
/** Operator action context used by custom-tool dispatchers (Gate 3). */
|
|
190
400
|
get actionContext() {
|
|
191
401
|
return this.options.actionContext;
|
|
@@ -227,7 +437,7 @@ export class ConsolePiRuntime {
|
|
|
227
437
|
*/
|
|
228
438
|
async composeSystemPromptSkills() {
|
|
229
439
|
try {
|
|
230
|
-
const result = await loadOperatorChatInstructionSkills(this.options.skillsDir);
|
|
440
|
+
const result = await loadOperatorChatInstructionSkills(this.options.skillsDir, OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS.filter((name) => !this.disabledInstructionSkills.has(name)));
|
|
231
441
|
return composeInstructionSkillsPrompt(result.loaded);
|
|
232
442
|
}
|
|
233
443
|
catch (error) {
|
|
@@ -297,10 +507,10 @@ export class ConsolePiRuntime {
|
|
|
297
507
|
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
298
508
|
// Build custom Pi tools that route whitelisted operator actions into
|
|
299
509
|
// the dispatcher (Gate 3 lives inside each tool's execute()).
|
|
300
|
-
let
|
|
510
|
+
let operatorCustomTools = [];
|
|
301
511
|
if (this.options.actionContext) {
|
|
302
512
|
try {
|
|
303
|
-
|
|
513
|
+
operatorCustomTools = await buildCustomOperatorTools({
|
|
304
514
|
actionContext: this.options.actionContext,
|
|
305
515
|
});
|
|
306
516
|
}
|
|
@@ -311,26 +521,37 @@ export class ConsolePiRuntime {
|
|
|
311
521
|
process.stderr.write(`[console] chat custom-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
312
522
|
}
|
|
313
523
|
}
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
524
|
+
// M0-A: also register the safe explore tools (safe-read / safe-grep /
|
|
525
|
+
// git-status / git-diff) so the model can probe the repo without bash.
|
|
526
|
+
let exploreCustomTools = [];
|
|
527
|
+
try {
|
|
528
|
+
exploreCustomTools = await buildExploreCustomTools({
|
|
529
|
+
repoRoot: this.options.cwd,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
process.stderr.write(`[console] chat explore-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
534
|
+
}
|
|
535
|
+
const customTools = [...operatorCustomTools, ...exploreCustomTools];
|
|
536
|
+
// Tool surface (roadmap M0):
|
|
537
|
+
// - ALL model-callable operator actions are registered as customTools.
|
|
538
|
+
// - Safe explore tools (safe-read / safe-grep / git-status / git-diff)
|
|
539
|
+
// are also registered as customTools (bash is GONE — M0-A removes
|
|
540
|
+
// the write-via-redirect escape).
|
|
541
|
+
// - Built-in find/ls are registered by passing `tools:` (Gate 1). The
|
|
542
|
+
// default builtin read/bash/edit/write are EXCLUDED: read/bash/grep
|
|
543
|
+
// are replaced by the safe custom versions above; edit/write stay
|
|
544
|
+
// excluded (file-writing never activated).
|
|
318
545
|
// by passing `tools: OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS` (Gate 1).
|
|
319
546
|
// This is REQUIRED: the SDK's default builtin set is only
|
|
320
547
|
// `read, bash, edit, write` (grep/find/ls are NOT default builtins —
|
|
321
548
|
// see SDK CreateAgentSessionOptions.tools docs). Without `tools`, the
|
|
322
|
-
// later setActiveToolsByName(["grep","find","ls"]) would silently
|
|
323
|
-
// no-op because those tools would not exist in the registry.
|
|
324
|
-
// - We then exclude edit/write so the only file-WRITING path is gone.
|
|
325
|
-
// - bash is bare (no command filter); the residual write-via-redirect
|
|
326
|
-
// risk is accepted (exec-plan Q9).
|
|
327
549
|
const { session } = await this.bindings.createSessionFromServices({
|
|
328
550
|
services,
|
|
329
551
|
sessionManager,
|
|
330
|
-
// Explicitly register the explore-tool builtin set (Gate 1).
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// the effective registry = explore tools + customTools (operator actions).
|
|
552
|
+
// Explicitly register the explore-tool builtin set (Gate 1): find/ls.
|
|
553
|
+
// The default builtin read/bash/edit/write are EXCLUDED below; read/
|
|
554
|
+
// grep/bash are replaced by the safe customTools (M0-A).
|
|
334
555
|
tools: [...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS],
|
|
335
556
|
excludeTools: [
|
|
336
557
|
"edit",
|
|
@@ -342,6 +563,14 @@ export class ConsolePiRuntime {
|
|
|
342
563
|
"coding-chat",
|
|
343
564
|
"coding_chat",
|
|
344
565
|
"shell",
|
|
566
|
+
// M0-A: bash is removed entirely (write-via-redirect escape). The
|
|
567
|
+
// model gets safe-read / safe-grep custom tools instead.
|
|
568
|
+
"bash",
|
|
569
|
+
// M0-A: built-in read/grep are replaced by custom safe-read/safe-grep
|
|
570
|
+
// that enforce a repo-relative boundary, a sensitive-file denylist,
|
|
571
|
+
// and secret scrubbing (roadmap G16).
|
|
572
|
+
"read",
|
|
573
|
+
"grep",
|
|
345
574
|
],
|
|
346
575
|
...(customTools && customTools.length > 0
|
|
347
576
|
? { customTools }
|
|
@@ -353,6 +582,8 @@ export class ConsolePiRuntime {
|
|
|
353
582
|
// File-writing tools are excluded above and thus cannot appear here.
|
|
354
583
|
session.setActiveToolsByName(OPERATOR_CHAT_ACTIVE_TOOL_NAMES());
|
|
355
584
|
this.sessions.set(sessionId, session);
|
|
585
|
+
this.sessionManagers.set(sessionId, sessionManager);
|
|
586
|
+
this.modelRuntimes.set(sessionId, modelRuntime);
|
|
356
587
|
return {
|
|
357
588
|
sessionId,
|
|
358
589
|
sessionFile: session.sessionFile,
|
|
@@ -361,6 +592,268 @@ export class ConsolePiRuntime {
|
|
|
361
592
|
activeTools: session.getActiveToolNames(),
|
|
362
593
|
};
|
|
363
594
|
}
|
|
595
|
+
async forkSession(init) {
|
|
596
|
+
if (!this.bindings.forkSessionManager)
|
|
597
|
+
throw new Error("SESSION_FORK_UNSUPPORTED");
|
|
598
|
+
const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
|
|
599
|
+
const sessionManager = await this.bindings.forkSessionManager({
|
|
600
|
+
sourcePath: init.sourceSessionFile,
|
|
601
|
+
targetCwd: this.options.cwd,
|
|
602
|
+
targetSessionDir: this.options.sessionDir,
|
|
603
|
+
targetSessionId: sessionId,
|
|
604
|
+
});
|
|
605
|
+
const built = await this.buildSessionWithServices({ sessionManager, model: init.model });
|
|
606
|
+
built.session.setActiveToolsByName(OPERATOR_CHAT_ACTIVE_TOOL_NAMES());
|
|
607
|
+
this.sessions.set(sessionId, built.session);
|
|
608
|
+
this.sessionManagers.set(sessionId, sessionManager);
|
|
609
|
+
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
610
|
+
return { sessionId, sessionFile: built.session.sessionFile, createdAt: new Date().toISOString(), model: init.model, activeTools: built.session.getActiveToolNames() };
|
|
611
|
+
}
|
|
612
|
+
async branchSession(input) {
|
|
613
|
+
const session = this.sessions.get(input.sessionId);
|
|
614
|
+
const manager = this.sessionManagers.get(input.sessionId);
|
|
615
|
+
if (!session || !manager)
|
|
616
|
+
throw new Error(`chat session not active: ${input.sessionId}`);
|
|
617
|
+
if (session.isStreaming === true || session.isIdle === false)
|
|
618
|
+
throw new Error("CHAT_TURN_ACTIVE");
|
|
619
|
+
if (!manager.getEntry?.(input.branchFromEntryId))
|
|
620
|
+
throw new Error("BRANCH_ENTRY_NOT_FOUND");
|
|
621
|
+
if (!this.mainlineLeaves.has(input.sessionId))
|
|
622
|
+
this.mainlineLeaves.set(input.sessionId, manager.getLeafId?.());
|
|
623
|
+
const entry = input.summary
|
|
624
|
+
? manager.branchWithSummary?.(input.branchFromEntryId, input.summary)
|
|
625
|
+
: (manager.branch(input.branchFromEntryId), undefined);
|
|
626
|
+
return { branchedFromEntryId: input.branchFromEntryId, ...(entry?.id ? { branchSummaryEntryId: entry.id } : {}) };
|
|
627
|
+
}
|
|
628
|
+
switchToMainline(sessionId) {
|
|
629
|
+
const manager = this.sessionManagers.get(sessionId);
|
|
630
|
+
if (!manager)
|
|
631
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
632
|
+
manager.branch(this.mainlineLeaves.get(sessionId));
|
|
633
|
+
this.mainlineLeaves.delete(sessionId);
|
|
634
|
+
}
|
|
635
|
+
getBranchContext(sessionId) {
|
|
636
|
+
const manager = this.sessionManagers.get(sessionId);
|
|
637
|
+
if (!manager)
|
|
638
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
639
|
+
return { leafId: manager.getLeafId?.() ?? null, entries: manager.getBranch?.() ?? [] };
|
|
640
|
+
}
|
|
641
|
+
async listModels(sessionId) {
|
|
642
|
+
const modelRuntime = this.modelRuntimes.get(sessionId);
|
|
643
|
+
if (!this.sessions.has(sessionId) || !modelRuntime)
|
|
644
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
645
|
+
return this.bindings.listAvailableModels({ modelRuntime });
|
|
646
|
+
}
|
|
647
|
+
async applyModel(sessionId, model) {
|
|
648
|
+
const session = this.sessions.get(sessionId);
|
|
649
|
+
const modelRuntime = this.modelRuntimes.get(sessionId);
|
|
650
|
+
if (!session || !modelRuntime)
|
|
651
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
652
|
+
if (!session.setModel)
|
|
653
|
+
throw new Error("MODEL_SWITCH_UNSUPPORTED");
|
|
654
|
+
const resolved = this.bindings.resolveModel({ modelRuntime, provider: model.provider, modelId: model.modelId });
|
|
655
|
+
if (!resolved)
|
|
656
|
+
throw new Error("MODEL_NOT_AVAILABLE");
|
|
657
|
+
await session.setModel(resolved);
|
|
658
|
+
return { provider: session.model?.provider ?? model.provider, modelId: session.model?.id ?? session.model?.modelId ?? model.modelId };
|
|
659
|
+
}
|
|
660
|
+
applyThinkingLevel(sessionId, level) {
|
|
661
|
+
const session = this.sessions.get(sessionId);
|
|
662
|
+
if (!session)
|
|
663
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
664
|
+
if (!THINKING_LEVELS.includes(level) || !session.setThinkingLevel)
|
|
665
|
+
throw new Error("THINKING_LEVEL_UNSUPPORTED");
|
|
666
|
+
session.setThinkingLevel(level);
|
|
667
|
+
return session.thinkingLevel ?? level;
|
|
668
|
+
}
|
|
669
|
+
getRuntimeSelection(sessionId) {
|
|
670
|
+
const session = this.sessions.get(sessionId);
|
|
671
|
+
if (!session)
|
|
672
|
+
return { activeTools: [] };
|
|
673
|
+
const provider = session.model?.provider;
|
|
674
|
+
const modelId = session.model?.id ?? session.model?.modelId;
|
|
675
|
+
return { ...(provider && modelId ? { model: { provider, modelId } } : {}), ...(session.thinkingLevel ? { thinkingLevel: session.thinkingLevel } : {}), activeTools: session.getActiveToolNames() };
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Re-open a previously persisted Chat session after Console restart or
|
|
679
|
+
* browser refresh (roadmap M1-S04 / S05).
|
|
680
|
+
*
|
|
681
|
+
* Unlike `createSession`, this reuses the EXISTING Pi JSONL session file so
|
|
682
|
+
* the conversation history is preserved. The SDK's `SessionManager.open(path)`
|
|
683
|
+
* loads the file; we then rebuild the same services + pinned tool surface so
|
|
684
|
+
* the operator-chat three-gate policy still applies after a restart.
|
|
685
|
+
*
|
|
686
|
+
* Fail-closed contract: if `sessionFile` is missing, or the SDK cannot open
|
|
687
|
+
* it (file deleted/corrupt), returns an `{ ok: false, code }` result rather
|
|
688
|
+
* than silently creating a fresh empty session and losing prior context.
|
|
689
|
+
*/
|
|
690
|
+
async reopenSession(init) {
|
|
691
|
+
const sessionId = init.sessionId;
|
|
692
|
+
// Already active in-memory: idempotent reopen is a no-op success.
|
|
693
|
+
if (this.sessions.has(sessionId)) {
|
|
694
|
+
return { ok: true, handle: await this.snapshotActive(sessionId) };
|
|
695
|
+
}
|
|
696
|
+
if (!init.sessionFile) {
|
|
697
|
+
return {
|
|
698
|
+
ok: false,
|
|
699
|
+
code: "NO_SESSION_FILE",
|
|
700
|
+
message: `cannot reopen chat session ${sessionId}: no persisted Pi session file`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
let sessionManager;
|
|
704
|
+
try {
|
|
705
|
+
sessionManager = await this.bindings.openSessionManager({
|
|
706
|
+
sessionFile: init.sessionFile,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
return {
|
|
711
|
+
ok: false,
|
|
712
|
+
code: "REOPEN_FAILED",
|
|
713
|
+
message: error instanceof Error ? error.message : String(error),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (!sessionManager) {
|
|
717
|
+
return {
|
|
718
|
+
ok: false,
|
|
719
|
+
code: "REOPEN_FAILED",
|
|
720
|
+
message: `Pi session file not openable: ${init.sessionFile}`,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const built = await this.buildSessionWithServices({
|
|
725
|
+
sessionManager,
|
|
726
|
+
model: init.model,
|
|
727
|
+
systemPromptSuffix: init.systemPromptSuffix,
|
|
728
|
+
});
|
|
729
|
+
const session = built.session;
|
|
730
|
+
// The reopened session MUST report the SAME sessionId as the durable
|
|
731
|
+
// record, otherwise we'd be serving a different conversation under a
|
|
732
|
+
// stale id. Mismatch is a fail-closed reopen failure.
|
|
733
|
+
if (session.sessionId !== sessionId) {
|
|
734
|
+
try {
|
|
735
|
+
session.dispose();
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// ignore
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
ok: false,
|
|
742
|
+
code: "REOPEN_FAILED",
|
|
743
|
+
message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${session.sessionId}`,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
session.setActiveToolsByName(OPERATOR_CHAT_ACTIVE_TOOL_NAMES());
|
|
747
|
+
this.sessions.set(sessionId, session);
|
|
748
|
+
this.sessionManagers.set(sessionId, sessionManager);
|
|
749
|
+
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
750
|
+
return {
|
|
751
|
+
ok: true,
|
|
752
|
+
handle: {
|
|
753
|
+
sessionId,
|
|
754
|
+
sessionFile: session.sessionFile ?? init.sessionFile,
|
|
755
|
+
createdAt: new Date().toISOString(),
|
|
756
|
+
model: init.model,
|
|
757
|
+
activeTools: session.getActiveToolNames(),
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
return {
|
|
763
|
+
ok: false,
|
|
764
|
+
code: "REOPEN_FAILED",
|
|
765
|
+
message: error instanceof Error ? error.message : String(error),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/** Build a handle for an already-active session (idempotent reopen path). */
|
|
770
|
+
async snapshotActive(sessionId) {
|
|
771
|
+
const session = this.sessions.get(sessionId);
|
|
772
|
+
return {
|
|
773
|
+
sessionId,
|
|
774
|
+
sessionFile: session.sessionFile,
|
|
775
|
+
createdAt: new Date().toISOString(),
|
|
776
|
+
model: undefined,
|
|
777
|
+
activeTools: session.getActiveToolNames(),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Shared services + pinned-tool materialization for both createSession and
|
|
782
|
+
* reopenSession. Loads closed-surface services, resolves the model, builds
|
|
783
|
+
* the operator + safe explore customTools, and calls the SDK with the same
|
|
784
|
+
* excludeTools / tools policy (M0-A three-gate).
|
|
785
|
+
*/
|
|
786
|
+
async buildSessionWithServices(init) {
|
|
787
|
+
const agentDir = await safeGetAgentDir(this.bindings);
|
|
788
|
+
const skillFragment = await this.composeSystemPromptSkills();
|
|
789
|
+
const appendSystemPrompt = [
|
|
790
|
+
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
791
|
+
...(skillFragment ? [skillFragment] : []),
|
|
792
|
+
...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
|
|
793
|
+
];
|
|
794
|
+
const { services } = await this.bindings.createServices({
|
|
795
|
+
cwd: this.options.cwd,
|
|
796
|
+
agentDir,
|
|
797
|
+
resourceLoaderOptions: {
|
|
798
|
+
noContextFiles: true,
|
|
799
|
+
noSkills: true,
|
|
800
|
+
noExtensions: true,
|
|
801
|
+
appendSystemPrompt,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
const modelRuntime = services.modelRuntime;
|
|
805
|
+
const resolvedModel = init.model
|
|
806
|
+
? (this.bindings.resolveModel({
|
|
807
|
+
modelRuntime,
|
|
808
|
+
provider: init.model.provider,
|
|
809
|
+
modelId: init.model.modelId,
|
|
810
|
+
}) ?? undefined)
|
|
811
|
+
: undefined;
|
|
812
|
+
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
813
|
+
let operatorCustomTools = [];
|
|
814
|
+
if (this.options.actionContext) {
|
|
815
|
+
try {
|
|
816
|
+
operatorCustomTools = await buildCustomOperatorTools({
|
|
817
|
+
actionContext: this.options.actionContext,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
process.stderr.write(`[console] chat custom-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
let exploreCustomTools = [];
|
|
825
|
+
try {
|
|
826
|
+
exploreCustomTools = await buildExploreCustomTools({
|
|
827
|
+
repoRoot: this.options.cwd,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
process.stderr.write(`[console] chat explore-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
832
|
+
}
|
|
833
|
+
const customTools = [...operatorCustomTools, ...exploreCustomTools];
|
|
834
|
+
const { session } = await this.bindings.createSessionFromServices({
|
|
835
|
+
services,
|
|
836
|
+
sessionManager: init.sessionManager,
|
|
837
|
+
tools: [...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS],
|
|
838
|
+
excludeTools: [
|
|
839
|
+
"edit",
|
|
840
|
+
"write",
|
|
841
|
+
"apply_patch",
|
|
842
|
+
"apply-patch",
|
|
843
|
+
"full-tools",
|
|
844
|
+
"full_tools",
|
|
845
|
+
"coding-chat",
|
|
846
|
+
"coding_chat",
|
|
847
|
+
"shell",
|
|
848
|
+
"bash",
|
|
849
|
+
"read",
|
|
850
|
+
"grep",
|
|
851
|
+
],
|
|
852
|
+
...(customTools && customTools.length > 0 ? { customTools } : {}),
|
|
853
|
+
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
854
|
+
});
|
|
855
|
+
return { session, modelRuntime };
|
|
856
|
+
}
|
|
364
857
|
/** Re-open a previously created session (cross-request). */
|
|
365
858
|
open(sessionId) {
|
|
366
859
|
return this.sessions.get(sessionId);
|
|
@@ -378,16 +871,32 @@ export class ConsolePiRuntime {
|
|
|
378
871
|
if (!session) {
|
|
379
872
|
throw new Error(`chat session not found: ${sessionId}`);
|
|
380
873
|
}
|
|
381
|
-
// Gate 2 (re-pin before each turn):
|
|
382
|
-
|
|
874
|
+
// Gate 2 (re-pin before each turn): interview turns pass through the
|
|
875
|
+
// existing closed interview allow/deny registry. The next ordinary turn
|
|
876
|
+
// explicitly restores the normal Operator Chat set.
|
|
877
|
+
const requestedTools = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
|
|
878
|
+
const activeTools = options?.mode === "requirement-interview"
|
|
879
|
+
? filterActiveInterviewTools(requestedTools).allowed
|
|
880
|
+
: requestedTools;
|
|
881
|
+
session.setActiveToolsByName(activeTools);
|
|
383
882
|
const unsub = session.subscribe((event) => {
|
|
384
883
|
const mapped = mapSdkEvent(sessionId, event);
|
|
385
884
|
if (mapped)
|
|
386
885
|
onEvent(mapped);
|
|
387
886
|
});
|
|
388
887
|
const onAbort = () => {
|
|
888
|
+
// M1 T08: Stop must abort only the current turn, NOT dispose the
|
|
889
|
+
// session. SDK AgentSession.abort() aborts the current operation and
|
|
890
|
+
// waits for the agent to become idle, keeping the session reusable for
|
|
891
|
+
// the next turn. Only fall back to dispose if the session has no abort()
|
|
892
|
+
// (e.g. a minimal stub), since a stuck turn should not leak forever.
|
|
389
893
|
try {
|
|
390
|
-
session.
|
|
894
|
+
if (typeof session.abort === "function") {
|
|
895
|
+
void session.abort();
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
session.dispose();
|
|
899
|
+
}
|
|
391
900
|
}
|
|
392
901
|
catch {
|
|
393
902
|
// ignore
|
|
@@ -395,7 +904,7 @@ export class ConsolePiRuntime {
|
|
|
395
904
|
};
|
|
396
905
|
options?.signal?.addEventListener("abort", onAbort);
|
|
397
906
|
try {
|
|
398
|
-
await session.prompt(text);
|
|
907
|
+
await session.prompt(text, options?.images?.length ? { images: options.images } : undefined);
|
|
399
908
|
// Wait until the session reports idle / not streaming. The SDK prompt()
|
|
400
909
|
// resolves when the agent turn completes, but post-turn continuation
|
|
401
910
|
// (auto-compaction / follow-up) may still be in flight. We poll isIdle.
|
|
@@ -417,6 +926,17 @@ export class ConsolePiRuntime {
|
|
|
417
926
|
unsub();
|
|
418
927
|
}
|
|
419
928
|
}
|
|
929
|
+
async compact(sessionId, customInstructions) {
|
|
930
|
+
const session = this.sessions.get(sessionId);
|
|
931
|
+
if (!session)
|
|
932
|
+
throw new Error(`chat session not found: ${sessionId}`);
|
|
933
|
+
if (session.isStreaming === true || session.isIdle === false)
|
|
934
|
+
throw new Error("CHAT_TURN_ACTIVE");
|
|
935
|
+
if (typeof session.compact !== "function")
|
|
936
|
+
throw new Error("COMPACTION_UNSUPPORTED");
|
|
937
|
+
const result = await session.compact(customInstructions);
|
|
938
|
+
return projectCompactSnapshot(result ?? {});
|
|
939
|
+
}
|
|
420
940
|
dispose(sessionId) {
|
|
421
941
|
const session = this.sessions.get(sessionId);
|
|
422
942
|
if (session) {
|
|
@@ -427,6 +947,9 @@ export class ConsolePiRuntime {
|
|
|
427
947
|
// ignore
|
|
428
948
|
}
|
|
429
949
|
this.sessions.delete(sessionId);
|
|
950
|
+
this.sessionManagers.delete(sessionId);
|
|
951
|
+
this.mainlineLeaves.delete(sessionId);
|
|
952
|
+
this.modelRuntimes.delete(sessionId);
|
|
430
953
|
}
|
|
431
954
|
}
|
|
432
955
|
disposeAll() {
|
|
@@ -451,11 +974,11 @@ function mapSdkEvent(sessionId, event) {
|
|
|
451
974
|
text: extractAssistantText(event.message),
|
|
452
975
|
};
|
|
453
976
|
case "message_end":
|
|
454
|
-
return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
977
|
+
return { type: "message_end", sessionId, text: extractAssistantText(event.message) };
|
|
978
|
+
case "turn_end": {
|
|
979
|
+
const usage = extractUsageSample(event);
|
|
980
|
+
return usage ? { type: "usage", sessionId, usage } : undefined;
|
|
981
|
+
}
|
|
459
982
|
case "tool_execution_start":
|
|
460
983
|
return {
|
|
461
984
|
type: "tool_call",
|
|
@@ -573,6 +1096,21 @@ export function createDefaultPiSdkBindings() {
|
|
|
573
1096
|
id: opts.sessionId,
|
|
574
1097
|
});
|
|
575
1098
|
},
|
|
1099
|
+
forkSessionManager: async (opts) => {
|
|
1100
|
+
const sdk = (await loadSdk());
|
|
1101
|
+
return sdk.SessionManager.forkFrom(opts.sourcePath, opts.targetCwd, opts.targetSessionDir, { id: opts.targetSessionId });
|
|
1102
|
+
},
|
|
1103
|
+
openSessionManager: async (opts) => {
|
|
1104
|
+
const sdk = (await loadSdk());
|
|
1105
|
+
try {
|
|
1106
|
+
return sdk.SessionManager.open(opts.sessionFile);
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
// Missing/corrupt session file — caller fails closed rather than
|
|
1110
|
+
// silently creating a fresh session and losing prior context.
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
576
1114
|
resolveModel: (opts) => {
|
|
577
1115
|
const rt = opts.modelRuntime;
|
|
578
1116
|
if (typeof rt?.getModel !== "function")
|