@tea-agent/loop-agent 0.29.1 → 0.29.3
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 +36 -16
- package/bin/agent-worker.js +0 -0
- package/dist/commands/client-recovery.js +2 -10
- package/dist/commands/init.js +1 -1
- package/dist/executors/dag-pi-executor.js +35 -10
- package/dist/executors/pi-executor.js +5 -3
- package/dist/executors/pi-playwright-cli-tool.js +9 -14
- package/dist/executors/pi-sdk-executor.js +16 -0
- package/dist/shared/operator/capabilities.js +9 -9
- package/dist/shared/pi-retry-settings.js +23 -0
- package/dist/worker/console/chat/pi-runtime.js +167 -41
- package/dist/worker/console/chat/routes.js +21 -2
- package/dist/worker/console/chat/runtime-context.js +50 -8
- package/dist/worker/console/chat/tool-preview.js +90 -0
- package/dist/worker/console/chat/turn-process.js +178 -0
- package/dist/worker/console/chat/usage.js +144 -16
- package/dist/worker/console/night-aux-ticker.js +141 -0
- package/dist/worker/console/operation-runner.js +21 -4
- package/dist/worker/console/operator-user-error.js +12 -0
- package/dist/worker/console/recovery-cta.js +6 -6
- package/dist/worker/console/routes.js +61 -0
- package/dist/worker/console/server.js +7 -0
- package/dist/worker/console/static/assets/index-D9gnJn_l.js +29 -0
- package/dist/worker/console/static/assets/index-rajoXwkM.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/night-prepare-result.js +118 -0
- package/dist/worker/scheduler/clock-install/win32-schtasks.js +70 -15
- package/dist/workflows/dag/backend-test-writer-completeness.js +98 -14
- package/dist/workflows/dag/frontend-test-case-checklist.js +3 -3
- package/dist/workflows/dag/init-hybrid.js +21 -12
- package/dist/workflows/dag/node-execution.js +41 -2
- package/docs/templates/frontend-test-case-checklist.md +1 -1
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.json +1 -1
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +6 -3
- package/package.json +1 -1
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +1 -1
- package/dist/worker/console/static/assets/index-Cwx-ZVEQ.js +0 -29
- package/dist/worker/console/static/assets/index-Yyn3ynVv.css +0 -1
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
* `createDefaultPiSdkBindings()`.
|
|
24
24
|
*/
|
|
25
25
|
import { randomBytes } from "node:crypto";
|
|
26
|
-
import {
|
|
26
|
+
import { access } from "node:fs/promises";
|
|
27
27
|
import path from "node:path";
|
|
28
|
+
import { projectCompactSnapshot, } from "./chat-event-store.js";
|
|
28
29
|
import { extractUsageSample } from "./usage.js";
|
|
29
30
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
30
31
|
import { createOperatorChatResourceLoader, } from "./resource-loader.js";
|
|
@@ -46,7 +47,7 @@ import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
|
46
47
|
*
|
|
47
48
|
* Instruction skills are appended AFTER this as read-only context (plan D1).
|
|
48
49
|
*/
|
|
49
|
-
const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
50
|
+
export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
50
51
|
"You are the General Operator Chat for loop-agent / agent-worker.",
|
|
51
52
|
"You are an OPERATOR first: orchestrate and inspect via operator_* tools, and you also have full Pi repository tools (read, write, edit, bash, grep, find, ls) plus optional safe-read/safe-grep/git-status/git-diff.",
|
|
52
53
|
"Capability honesty (ADR 0011): read/write/edit/bash ARE available. Prefer governed loop-agent / Agent DAG paths (implement-pi / repair-pi) and Human Gate for large refactors, public contracts, credentials, or production-risk changes — treat direct write/edit/bash as soft-disciplined, not as a second DAG kernel.",
|
|
@@ -55,7 +56,18 @@ const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
|
55
56
|
"High-risk mutations still use prepare + browser Human Gate. 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, diagnose with status/doctor/dagReport/inspect/contractShow/read/safe-read/safe-grep.",
|
|
56
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.",
|
|
57
58
|
"Prefer read-only diagnosis (status, doctor, dagReport, inspect, contractShow, read, safe-read, safe-grep, git-status) before mutating.",
|
|
59
|
+
"Recovery default order (save tokens/time; do NOT open a new task-id for provider flake): (1) dagReport/doctor/status — read primaryFailure.nodeId + primaryRecovery; (2) for completed failed|partial_failed standalone runs prefer dagRerunPlan then prepareMutationGate/dagRerun from the failed or safe upstream node (plan → planHash → human gate); (3) paused → approve path / resume, never treat resume as failure retry; (4) only when plan is ineligible (writer/decision/fingerprint) or contract/source truly changed: standaloneTaskRerun or same-task task advance — never invent a fresh task-id without cause; (5) Worker-owned failures use workerTaskRetry, not dagRerun. Never use direct write/edit as a substitute for repair-pi.",
|
|
58
60
|
].join("\n");
|
|
61
|
+
/** Compose the inspectable system prompt actually injected into Operator Chat. */
|
|
62
|
+
export function composeOperatorChatSystemPrompt(input) {
|
|
63
|
+
return [
|
|
64
|
+
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
65
|
+
...(input.skillFragment?.trim() ? [input.skillFragment.trim()] : []),
|
|
66
|
+
...(input.systemPromptSuffix?.trim()
|
|
67
|
+
? [input.systemPromptSuffix.trim()]
|
|
68
|
+
: []),
|
|
69
|
+
].join("\n\n");
|
|
70
|
+
}
|
|
59
71
|
/**
|
|
60
72
|
* Built-in SDK tool names registered for every Chat session (ADR 0011).
|
|
61
73
|
* Full Pi builtins: read/write/edit/bash/grep/find/ls. safe-* custom tools
|
|
@@ -125,9 +137,7 @@ async function buildCustomOperatorTools(options) {
|
|
|
125
137
|
default:
|
|
126
138
|
t = Type.Object({}, { additionalProperties: true });
|
|
127
139
|
}
|
|
128
|
-
acc[param.name] = param.required
|
|
129
|
-
? t
|
|
130
|
-
: Type.Optional(t);
|
|
140
|
+
acc[param.name] = param.required ? t : Type.Optional(t);
|
|
131
141
|
return acc;
|
|
132
142
|
}, {}), { additionalProperties: false });
|
|
133
143
|
return defineTool({
|
|
@@ -141,7 +151,13 @@ async function buildCustomOperatorTools(options) {
|
|
|
141
151
|
if (!decision.ok) {
|
|
142
152
|
return {
|
|
143
153
|
content: [
|
|
144
|
-
{
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: JSON.stringify({
|
|
157
|
+
error: decision.message,
|
|
158
|
+
code: decision.code,
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
145
161
|
],
|
|
146
162
|
details: { error: decision.message, code: decision.code },
|
|
147
163
|
};
|
|
@@ -228,7 +244,10 @@ async function buildExploreCustomTools(options) {
|
|
|
228
244
|
}),
|
|
229
245
|
},
|
|
230
246
|
],
|
|
231
|
-
details: {
|
|
247
|
+
details: {
|
|
248
|
+
error: err.message ?? String(e),
|
|
249
|
+
code: err.code ?? "READ_FAILED",
|
|
250
|
+
},
|
|
232
251
|
};
|
|
233
252
|
}
|
|
234
253
|
},
|
|
@@ -266,7 +285,10 @@ async function buildExploreCustomTools(options) {
|
|
|
266
285
|
}),
|
|
267
286
|
},
|
|
268
287
|
],
|
|
269
|
-
details: {
|
|
288
|
+
details: {
|
|
289
|
+
error: err.message ?? String(e),
|
|
290
|
+
code: err.code ?? "GREP_FAILED",
|
|
291
|
+
},
|
|
270
292
|
};
|
|
271
293
|
}
|
|
272
294
|
},
|
|
@@ -281,7 +303,12 @@ async function buildExploreCustomTools(options) {
|
|
|
281
303
|
const r = safeGit(["status", "--porcelain"]);
|
|
282
304
|
if (!r.ok) {
|
|
283
305
|
return {
|
|
284
|
-
content: [
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: JSON.stringify({ error: r.message, code: r.code }),
|
|
310
|
+
},
|
|
311
|
+
],
|
|
285
312
|
details: { error: r.message, code: r.code },
|
|
286
313
|
};
|
|
287
314
|
}
|
|
@@ -299,7 +326,9 @@ async function buildExploreCustomTools(options) {
|
|
|
299
326
|
description: "Typed `git diff` (fixed argv: --stat or a single -- path; no shell). Returns scrubbed diff. path must be repo-relative and not sensitive.",
|
|
300
327
|
promptSnippet: "git-diff: diff (read-only, scrubbed)",
|
|
301
328
|
parameters: Type.Object({
|
|
302
|
-
path: Type.Optional(Type.String({
|
|
329
|
+
path: Type.Optional(Type.String({
|
|
330
|
+
description: "optional repo-relative path to diff (default: whole tree --stat)",
|
|
331
|
+
})),
|
|
303
332
|
}, { additionalProperties: false }),
|
|
304
333
|
async execute(_toolCallId, params) {
|
|
305
334
|
let rawPath = params.path ? String(params.path) : undefined;
|
|
@@ -311,20 +340,26 @@ async function buildExploreCustomTools(options) {
|
|
|
311
340
|
if (!g.ok) {
|
|
312
341
|
return {
|
|
313
342
|
content: [
|
|
314
|
-
{
|
|
343
|
+
{
|
|
344
|
+
type: "text",
|
|
345
|
+
text: JSON.stringify({ error: g.message, code: g.code }),
|
|
346
|
+
},
|
|
315
347
|
],
|
|
316
348
|
details: { error: g.message, code: g.code },
|
|
317
349
|
};
|
|
318
350
|
}
|
|
319
351
|
rawPath = g.repoRelative;
|
|
320
352
|
}
|
|
321
|
-
const args = rawPath
|
|
322
|
-
? ["diff", "--", rawPath]
|
|
323
|
-
: ["diff", "--stat"];
|
|
353
|
+
const args = rawPath ? ["diff", "--", rawPath] : ["diff", "--stat"];
|
|
324
354
|
const r = safeGit(args);
|
|
325
355
|
if (!r.ok) {
|
|
326
356
|
return {
|
|
327
|
-
content: [
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
359
|
+
type: "text",
|
|
360
|
+
text: JSON.stringify({ error: r.message, code: r.code }),
|
|
361
|
+
},
|
|
362
|
+
],
|
|
328
363
|
details: { error: r.message, code: r.code },
|
|
329
364
|
};
|
|
330
365
|
}
|
|
@@ -338,7 +373,15 @@ async function buildExploreCustomTools(options) {
|
|
|
338
373
|
});
|
|
339
374
|
return [safeReadTool, safeGrepTool, gitStatusTool, gitDiffTool];
|
|
340
375
|
}
|
|
341
|
-
export const THINKING_LEVELS = [
|
|
376
|
+
export const THINKING_LEVELS = [
|
|
377
|
+
"off",
|
|
378
|
+
"minimal",
|
|
379
|
+
"low",
|
|
380
|
+
"medium",
|
|
381
|
+
"high",
|
|
382
|
+
"xhigh",
|
|
383
|
+
"max",
|
|
384
|
+
];
|
|
342
385
|
/**
|
|
343
386
|
* Extract the latest assistant text fragment from an SDK message object.
|
|
344
387
|
* Tolerant of multiple message shapes the SDK emits.
|
|
@@ -361,6 +404,25 @@ export function extractAssistantText(message) {
|
|
|
361
404
|
}
|
|
362
405
|
return segments.join("");
|
|
363
406
|
}
|
|
407
|
+
function readSessionManagerId(manager) {
|
|
408
|
+
if (!manager || typeof manager !== "object")
|
|
409
|
+
return undefined;
|
|
410
|
+
const record = manager;
|
|
411
|
+
if (typeof record.getSessionId === "function") {
|
|
412
|
+
try {
|
|
413
|
+
const value = record.getSessionId();
|
|
414
|
+
if (typeof value === "string" && value.trim())
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// ignore
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (typeof record.sessionId === "string" && record.sessionId.trim()) {
|
|
422
|
+
return record.sessionId;
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
364
426
|
/**
|
|
365
427
|
* Console Pi runtime — holds Chat sessions and drives prompt() with three-gate
|
|
366
428
|
* tool enforcement. Session-per-sessionId; cross-request reuse via open().
|
|
@@ -377,8 +439,7 @@ export class ConsolePiRuntime {
|
|
|
377
439
|
constructor(options) {
|
|
378
440
|
this.options = options;
|
|
379
441
|
this.loader = createOperatorChatResourceLoader();
|
|
380
|
-
this.bindings =
|
|
381
|
-
options.bindings ?? createDefaultPiSdkBindings();
|
|
442
|
+
this.bindings = options.bindings ?? createDefaultPiSdkBindings();
|
|
382
443
|
}
|
|
383
444
|
get sessionDir() {
|
|
384
445
|
return this.options.sessionDir;
|
|
@@ -388,8 +449,17 @@ export class ConsolePiRuntime {
|
|
|
388
449
|
return safeGetAgentDir(this.bindings);
|
|
389
450
|
}
|
|
390
451
|
async testConfiguredModel(model) {
|
|
391
|
-
const { services } = await this.bindings.createServices({
|
|
392
|
-
|
|
452
|
+
const { services } = await this.bindings.createServices({
|
|
453
|
+
cwd: this.options.cwd,
|
|
454
|
+
agentDir: await this.getAgentDir(),
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
available: Boolean(this.bindings.resolveModel({
|
|
458
|
+
modelRuntime: services.modelRuntime,
|
|
459
|
+
provider: model.provider,
|
|
460
|
+
modelId: model.modelId,
|
|
461
|
+
})),
|
|
462
|
+
};
|
|
393
463
|
}
|
|
394
464
|
setDisabledInstructionSkills(names) {
|
|
395
465
|
this.disabledInstructionSkills = new Set(names);
|
|
@@ -559,9 +629,7 @@ export class ConsolePiRuntime {
|
|
|
559
629
|
"coding_chat",
|
|
560
630
|
"shell",
|
|
561
631
|
],
|
|
562
|
-
...(customTools && customTools.length > 0
|
|
563
|
-
? { customTools }
|
|
564
|
-
: {}),
|
|
632
|
+
...(customTools && customTools.length > 0 ? { customTools } : {}),
|
|
565
633
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
566
634
|
});
|
|
567
635
|
// Gate 2: re-pin active tools (defensive — some SDK paths may seed
|
|
@@ -589,12 +657,21 @@ export class ConsolePiRuntime {
|
|
|
589
657
|
targetSessionDir: this.options.sessionDir,
|
|
590
658
|
targetSessionId: sessionId,
|
|
591
659
|
});
|
|
592
|
-
const built = await this.buildSessionWithServices({
|
|
660
|
+
const built = await this.buildSessionWithServices({
|
|
661
|
+
sessionManager,
|
|
662
|
+
model: init.model,
|
|
663
|
+
});
|
|
593
664
|
built.session.setActiveToolsByName(OPERATOR_CHAT_ACTIVE_TOOL_NAMES());
|
|
594
665
|
this.sessions.set(sessionId, built.session);
|
|
595
666
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
596
667
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
597
|
-
return {
|
|
668
|
+
return {
|
|
669
|
+
sessionId,
|
|
670
|
+
sessionFile: built.session.sessionFile,
|
|
671
|
+
createdAt: new Date().toISOString(),
|
|
672
|
+
model: init.model,
|
|
673
|
+
activeTools: built.session.getActiveToolNames(),
|
|
674
|
+
};
|
|
598
675
|
}
|
|
599
676
|
async branchSession(input) {
|
|
600
677
|
const session = this.sessions.get(input.sessionId);
|
|
@@ -610,7 +687,10 @@ export class ConsolePiRuntime {
|
|
|
610
687
|
const entry = input.summary
|
|
611
688
|
? manager.branchWithSummary?.(input.branchFromEntryId, input.summary)
|
|
612
689
|
: (manager.branch(input.branchFromEntryId), undefined);
|
|
613
|
-
return {
|
|
690
|
+
return {
|
|
691
|
+
branchedFromEntryId: input.branchFromEntryId,
|
|
692
|
+
...(entry?.id ? { branchSummaryEntryId: entry.id } : {}),
|
|
693
|
+
};
|
|
614
694
|
}
|
|
615
695
|
switchToMainline(sessionId) {
|
|
616
696
|
const manager = this.sessionManagers.get(sessionId);
|
|
@@ -623,7 +703,10 @@ export class ConsolePiRuntime {
|
|
|
623
703
|
const manager = this.sessionManagers.get(sessionId);
|
|
624
704
|
if (!manager)
|
|
625
705
|
throw new Error(`chat session not active: ${sessionId}`);
|
|
626
|
-
return {
|
|
706
|
+
return {
|
|
707
|
+
leafId: manager.getLeafId?.() ?? null,
|
|
708
|
+
entries: manager.getBranch?.() ?? [],
|
|
709
|
+
};
|
|
627
710
|
}
|
|
628
711
|
async listModels(sessionId) {
|
|
629
712
|
const modelRuntime = this.modelRuntimes.get(sessionId);
|
|
@@ -638,11 +721,18 @@ export class ConsolePiRuntime {
|
|
|
638
721
|
throw new Error(`chat session not active: ${sessionId}`);
|
|
639
722
|
if (!session.setModel)
|
|
640
723
|
throw new Error("MODEL_SWITCH_UNSUPPORTED");
|
|
641
|
-
const resolved = this.bindings.resolveModel({
|
|
724
|
+
const resolved = this.bindings.resolveModel({
|
|
725
|
+
modelRuntime,
|
|
726
|
+
provider: model.provider,
|
|
727
|
+
modelId: model.modelId,
|
|
728
|
+
});
|
|
642
729
|
if (!resolved)
|
|
643
730
|
throw new Error("MODEL_NOT_AVAILABLE");
|
|
644
731
|
await session.setModel(resolved);
|
|
645
|
-
return {
|
|
732
|
+
return {
|
|
733
|
+
provider: session.model?.provider ?? model.provider,
|
|
734
|
+
modelId: session.model?.id ?? session.model?.modelId ?? model.modelId,
|
|
735
|
+
};
|
|
646
736
|
}
|
|
647
737
|
applyThinkingLevel(sessionId, level) {
|
|
648
738
|
const session = this.sessions.get(sessionId);
|
|
@@ -659,7 +749,13 @@ export class ConsolePiRuntime {
|
|
|
659
749
|
return { activeTools: [] };
|
|
660
750
|
const provider = session.model?.provider;
|
|
661
751
|
const modelId = session.model?.id ?? session.model?.modelId;
|
|
662
|
-
return {
|
|
752
|
+
return {
|
|
753
|
+
...(provider && modelId ? { model: { provider, modelId } } : {}),
|
|
754
|
+
...(session.thinkingLevel
|
|
755
|
+
? { thinkingLevel: session.thinkingLevel }
|
|
756
|
+
: {}),
|
|
757
|
+
activeTools: session.getActiveToolNames(),
|
|
758
|
+
};
|
|
663
759
|
}
|
|
664
760
|
/**
|
|
665
761
|
* Re-open a previously persisted Chat session after Console restart or
|
|
@@ -704,7 +800,15 @@ export class ConsolePiRuntime {
|
|
|
704
800
|
return {
|
|
705
801
|
ok: false,
|
|
706
802
|
code: "REOPEN_FAILED",
|
|
707
|
-
message: `Pi session file not openable: ${init.sessionFile}`,
|
|
803
|
+
message: `Pi session file missing or not openable: ${init.sessionFile}`,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const managerSessionId = readSessionManagerId(sessionManager);
|
|
807
|
+
if (managerSessionId && managerSessionId !== sessionId) {
|
|
808
|
+
return {
|
|
809
|
+
ok: false,
|
|
810
|
+
code: "REOPEN_FAILED",
|
|
811
|
+
message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${managerSessionId}`,
|
|
708
812
|
};
|
|
709
813
|
}
|
|
710
814
|
try {
|
|
@@ -714,10 +818,14 @@ export class ConsolePiRuntime {
|
|
|
714
818
|
systemPromptSuffix: init.systemPromptSuffix,
|
|
715
819
|
});
|
|
716
820
|
const session = built.session;
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
|
|
821
|
+
// Prefer the SessionManager id (from the JSONL header). Some SDK
|
|
822
|
+
// builds expose a different agent-session UUID on `session.sessionId`
|
|
823
|
+
// even when the manager correctly reopened our operator-chat file —
|
|
824
|
+
// map by the durable Console sessionId in that case.
|
|
825
|
+
const reportedId = session.sessionId ?? managerSessionId;
|
|
826
|
+
if (reportedId &&
|
|
827
|
+
reportedId !== sessionId &&
|
|
828
|
+
managerSessionId !== sessionId) {
|
|
721
829
|
try {
|
|
722
830
|
session.dispose();
|
|
723
831
|
}
|
|
@@ -727,7 +835,7 @@ export class ConsolePiRuntime {
|
|
|
727
835
|
return {
|
|
728
836
|
ok: false,
|
|
729
837
|
code: "REOPEN_FAILED",
|
|
730
|
-
message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${
|
|
838
|
+
message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${reportedId}`,
|
|
731
839
|
};
|
|
732
840
|
}
|
|
733
841
|
session.setActiveToolsByName(OPERATOR_CHAT_ACTIVE_TOOL_NAMES());
|
|
@@ -865,6 +973,15 @@ export class ConsolePiRuntime {
|
|
|
865
973
|
const mapped = mapSdkEvent(sessionId, event);
|
|
866
974
|
if (mapped)
|
|
867
975
|
onEvent(mapped);
|
|
976
|
+
// Pi puts per-assistant-message usage on `message.usage` ({ input, output,
|
|
977
|
+
// cacheRead, cacheWrite, totalTokens, cost }). Emit on message_end so
|
|
978
|
+
// multi-step tool turns accumulate like pi-web SessionInfoBar; turn_end
|
|
979
|
+
// repeats the final message and must not double-count.
|
|
980
|
+
if (event.type === "message_end") {
|
|
981
|
+
const usage = extractUsageSample(event);
|
|
982
|
+
if (usage)
|
|
983
|
+
onEvent({ type: "usage", sessionId, usage });
|
|
984
|
+
}
|
|
868
985
|
});
|
|
869
986
|
const onAbort = () => {
|
|
870
987
|
// M1 T08: Stop must abort only the current turn, NOT dispose the
|
|
@@ -956,11 +1073,15 @@ function mapSdkEvent(sessionId, event) {
|
|
|
956
1073
|
text: extractAssistantText(event.message),
|
|
957
1074
|
};
|
|
958
1075
|
case "message_end":
|
|
959
|
-
return {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1076
|
+
return {
|
|
1077
|
+
type: "message_end",
|
|
1078
|
+
sessionId,
|
|
1079
|
+
text: extractAssistantText(event.message),
|
|
1080
|
+
};
|
|
1081
|
+
case "turn_end":
|
|
1082
|
+
// Usage is emitted from message_end (see prompt subscribe). turn_end
|
|
1083
|
+
// carries the same final assistant message and would double-count.
|
|
1084
|
+
return undefined;
|
|
964
1085
|
case "tool_execution_start":
|
|
965
1086
|
return {
|
|
966
1087
|
type: "tool_call",
|
|
@@ -1085,6 +1206,11 @@ export function createDefaultPiSdkBindings() {
|
|
|
1085
1206
|
openSessionManager: async (opts) => {
|
|
1086
1207
|
const sdk = (await loadSdk());
|
|
1087
1208
|
try {
|
|
1209
|
+
// Pi SessionManager.open(missingPath) does NOT throw — it creates a
|
|
1210
|
+
// fresh in-memory session with a random UUID. That would reopen the
|
|
1211
|
+
// wrong conversation under the durable Console sessionId. Fail closed
|
|
1212
|
+
// before open when the persisted file is absent.
|
|
1213
|
+
await access(opts.sessionFile);
|
|
1088
1214
|
return sdk.SessionManager.open(opts.sessionFile);
|
|
1089
1215
|
}
|
|
1090
1216
|
catch {
|
|
@@ -17,7 +17,7 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_
|
|
|
17
17
|
import { OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS, OPERATOR_CHAT_DENIED_INSTRUCTION_SKILLS, OPERATOR_CHAT_WORKFLOW_CONDITIONAL_SKILLS, composeInstructionSkillsPrompt, loadOperatorChatInstructionSkills, } from "./instruction-skills.js";
|
|
18
18
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
19
19
|
import { projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
|
|
20
|
-
import { THINKING_LEVELS } from "./pi-runtime.js";
|
|
20
|
+
import { THINKING_LEVELS, composeOperatorChatSystemPrompt, } from "./pi-runtime.js";
|
|
21
21
|
import { ComposerDraftStore } from "./composer-draft-store.js";
|
|
22
22
|
import { walkRepoFiles } from "./repo-walk.js";
|
|
23
23
|
import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
|
|
@@ -936,7 +936,26 @@ async function handleRuntimeContext(res, deps, sessionId) {
|
|
|
936
936
|
}
|
|
937
937
|
const skills = await loadOperatorChatInstructionSkills(deps.skillsDir);
|
|
938
938
|
const selection = deps.runtime.getRuntimeSelection(sessionId);
|
|
939
|
-
|
|
939
|
+
const skillFragment = composeInstructionSkillsPrompt(skills.loaded);
|
|
940
|
+
const systemPrompt = composeOperatorChatSystemPrompt({ skillFragment });
|
|
941
|
+
sendJson(res, 200, {
|
|
942
|
+
ok: true,
|
|
943
|
+
context: projectRuntimeContext({
|
|
944
|
+
systemPrompt,
|
|
945
|
+
skills: skills.loaded.map((skill) => ({
|
|
946
|
+
name: skill.name,
|
|
947
|
+
description: skill.description,
|
|
948
|
+
charCount: skill.charCount,
|
|
949
|
+
bodyText: skill.bodyText,
|
|
950
|
+
})),
|
|
951
|
+
model: selection.model ??
|
|
952
|
+
(record.modelProvider && record.modelId
|
|
953
|
+
? { provider: record.modelProvider, modelId: record.modelId }
|
|
954
|
+
: undefined),
|
|
955
|
+
thinkingLevel: selection.thinkingLevel ?? record.thinkingLevel,
|
|
956
|
+
activeTools: selection.activeTools,
|
|
957
|
+
}),
|
|
958
|
+
});
|
|
940
959
|
}
|
|
941
960
|
async function handleModels(req, res, deps, sessionId) {
|
|
942
961
|
if (!await requireChatSession(res, deps, sessionId))
|
|
@@ -1,24 +1,66 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { scrubSecrets } from "./explore-tools.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const DEFAULT_SUMMARY_MAX = 320;
|
|
4
|
+
/** Hard cap for inspectable full text returned to the browser. */
|
|
5
|
+
export const RUNTIME_CONTEXT_TEXT_MAX = 120_000;
|
|
6
|
+
function redact(text) {
|
|
6
7
|
return scrubSecrets(text).scrubbed
|
|
7
8
|
.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, "[REDACTED]")
|
|
8
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, "Bearer [REDACTED]")
|
|
9
|
-
|
|
9
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, "Bearer [REDACTED]");
|
|
10
|
+
}
|
|
11
|
+
function summary(text, max = DEFAULT_SUMMARY_MAX) {
|
|
12
|
+
if (!text)
|
|
13
|
+
return undefined;
|
|
14
|
+
const cleaned = redact(text).replace(/\s+/g, " ").trim();
|
|
15
|
+
if (!cleaned)
|
|
16
|
+
return undefined;
|
|
17
|
+
return cleaned.length > max ? `${cleaned.slice(0, max)}…` : cleaned;
|
|
18
|
+
}
|
|
19
|
+
function fullText(text, max = RUNTIME_CONTEXT_TEXT_MAX) {
|
|
20
|
+
if (!text)
|
|
21
|
+
return undefined;
|
|
22
|
+
const cleaned = redact(text);
|
|
23
|
+
if (cleaned.length <= max)
|
|
24
|
+
return cleaned;
|
|
25
|
+
return `${cleaned.slice(0, max)}\n\n…[truncated ${cleaned.length - max} chars for inspect UI]`;
|
|
10
26
|
}
|
|
11
27
|
export function projectRuntimeContext(input) {
|
|
28
|
+
const systemText = fullText(input.systemPrompt) ?? "";
|
|
29
|
+
const suffixText = fullText(input.systemPromptSuffix);
|
|
12
30
|
return {
|
|
13
31
|
readOnly: true,
|
|
14
32
|
systemPrompt: {
|
|
15
33
|
hash: createHash("sha256").update(input.systemPrompt).digest("hex"),
|
|
16
34
|
summary: summary(input.systemPrompt),
|
|
35
|
+
text: systemText,
|
|
36
|
+
charCount: input.systemPrompt.length,
|
|
37
|
+
},
|
|
38
|
+
skills: input.skills.map((skill) => ({
|
|
39
|
+
name: skill.name,
|
|
40
|
+
description: summary(skill.description, 200) ?? "",
|
|
41
|
+
charCount: skill.charCount,
|
|
42
|
+
...(skill.bodyText
|
|
43
|
+
? { body: fullText(skill.bodyText, 40_000) }
|
|
44
|
+
: {}),
|
|
45
|
+
})),
|
|
46
|
+
resources: {
|
|
47
|
+
mode: "closed",
|
|
48
|
+
noContextFiles: true,
|
|
49
|
+
noSkills: true,
|
|
50
|
+
noExtensions: true,
|
|
51
|
+
hasBash: true,
|
|
52
|
+
activeToolCount: input.activeTools.length,
|
|
53
|
+
activeTools: [...input.activeTools],
|
|
17
54
|
},
|
|
18
|
-
skills: input.skills.map((skill) => ({ ...skill, description: summary(skill.description, 200) ?? "" })),
|
|
19
|
-
resources: { mode: "closed", noContextFiles: true, noSkills: true, noExtensions: true, hasBash: true, activeToolCount: input.activeTools.length },
|
|
20
55
|
model: input.model,
|
|
21
56
|
thinkingLevel: input.thinkingLevel,
|
|
22
|
-
suffix: input.systemPromptSuffix
|
|
57
|
+
suffix: input.systemPromptSuffix
|
|
58
|
+
? {
|
|
59
|
+
present: true,
|
|
60
|
+
summary: summary(input.systemPromptSuffix),
|
|
61
|
+
...(suffixText ? { text: suffixText } : {}),
|
|
62
|
+
charCount: input.systemPromptSuffix.length,
|
|
63
|
+
}
|
|
64
|
+
: { present: false },
|
|
23
65
|
};
|
|
24
66
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable one-line preview for tool call arguments (pi-web getToolPreview).
|
|
3
|
+
*/
|
|
4
|
+
const PREVIEW_KEYS = [
|
|
5
|
+
"command",
|
|
6
|
+
"path",
|
|
7
|
+
"file_path",
|
|
8
|
+
"filePath",
|
|
9
|
+
"pattern",
|
|
10
|
+
"query",
|
|
11
|
+
"glob",
|
|
12
|
+
"url",
|
|
13
|
+
];
|
|
14
|
+
const DEFAULT_PREVIEW_MAX = 120;
|
|
15
|
+
const DEFAULT_RESULT_MAX = 800;
|
|
16
|
+
function asRecord(value) {
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
18
|
+
return undefined;
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function parseArgs(args) {
|
|
22
|
+
if (args === undefined || args === null)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (typeof args === "string") {
|
|
25
|
+
const trimmed = args.trim();
|
|
26
|
+
if (!trimmed)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return asRecord(JSON.parse(trimmed));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return asRecord(args);
|
|
36
|
+
}
|
|
37
|
+
/** One-line summary for a tool-call header; empty when nothing useful. */
|
|
38
|
+
export function getToolPreview(args, options) {
|
|
39
|
+
const maxLen = options?.maxLen ?? DEFAULT_PREVIEW_MAX;
|
|
40
|
+
const input = parseArgs(args);
|
|
41
|
+
if (!input) {
|
|
42
|
+
if (typeof args === "string") {
|
|
43
|
+
const s = args.replace(/\s+/g, " ").trim();
|
|
44
|
+
if (!s || s === "{}")
|
|
45
|
+
return "";
|
|
46
|
+
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
for (const key of PREVIEW_KEYS) {
|
|
51
|
+
const value = input[key];
|
|
52
|
+
if (typeof value === "string" && value.trim()) {
|
|
53
|
+
const s = value.replace(/\s+/g, " ").trim();
|
|
54
|
+
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const keys = Object.keys(input);
|
|
58
|
+
if (keys.length === 0)
|
|
59
|
+
return "";
|
|
60
|
+
const first = input[keys[0]];
|
|
61
|
+
const s = String(first ?? "").replace(/\s+/g, " ").trim();
|
|
62
|
+
if (!s)
|
|
63
|
+
return "";
|
|
64
|
+
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
65
|
+
}
|
|
66
|
+
export function formatToolArgsDisplay(args) {
|
|
67
|
+
if (args === undefined || args === null)
|
|
68
|
+
return "";
|
|
69
|
+
try {
|
|
70
|
+
return typeof args === "string" ? args : JSON.stringify(args, null, 2);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return String(args);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function truncateToolResult(text, options) {
|
|
77
|
+
if (text === undefined)
|
|
78
|
+
return { text: "", truncated: false };
|
|
79
|
+
const maxLen = options?.maxLen ?? DEFAULT_RESULT_MAX;
|
|
80
|
+
if (text.length <= maxLen)
|
|
81
|
+
return { text, truncated: false };
|
|
82
|
+
return { text: `${text.slice(0, maxLen)}…`, truncated: true };
|
|
83
|
+
}
|
|
84
|
+
export function toolCallDurationSeconds(createdAt, finishedAt) {
|
|
85
|
+
if (finishedAt === undefined || !Number.isFinite(createdAt) || !Number.isFinite(finishedAt)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const secs = Math.round((finishedAt - createdAt) / 1000);
|
|
89
|
+
return secs > 0 ? secs : undefined;
|
|
90
|
+
}
|