@tea-agent/loop-agent 0.40.0-next.3 → 0.40.0-next.5
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 +21 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +46 -5
- package/dist/executors/shell-executor.js +28 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/task/config-types.js +10 -0
- package/dist/worker/console/chat/chat-event-store.js +62 -4
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +292 -29
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +14 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/routes.js +309 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +529 -186
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
- package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
- package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
- package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +26 -6
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +172 -0
- package/dist/worker/observe/static/styles.css +20 -1
- package/dist/worker/observe/static/views/dag-inspector.js +4 -3
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +19 -0
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/types.js +3 -1
- package/docs/README.md +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/harness.json +3 -3
- package/package.json +3 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
|
@@ -29,6 +29,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
29
29
|
import { access } from "node:fs/promises";
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
|
|
32
|
+
import { parseProviderError, } from "./provider-error.js";
|
|
32
33
|
import { projectCompactSnapshot, } from "./chat-event-store.js";
|
|
33
34
|
import { extractUsageSample } from "./usage.js";
|
|
34
35
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
@@ -38,6 +39,7 @@ import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
|
38
39
|
import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
|
|
39
40
|
import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
40
41
|
import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
|
|
42
|
+
import { buildTodoContinuationContext, } from "./todos.js";
|
|
41
43
|
/**
|
|
42
44
|
* Base system prompt fragment every General Operator Chat session receives.
|
|
43
45
|
*
|
|
@@ -210,6 +212,51 @@ async function bindSessionExtensions(session) {
|
|
|
210
212
|
function pinOperatorChatTools(session) {
|
|
211
213
|
session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
|
|
212
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Pi has no hook result that merely appends instructions to its built-in
|
|
217
|
+
* automatic compactor: session_before_compact must either cancel or replace
|
|
218
|
+
* the whole compaction result. Keep the SDK algorithm intact and inject the
|
|
219
|
+
* current Todo as ephemeral context only after a successful compaction. The
|
|
220
|
+
* next turn resets the marker and TodoListRegistry clears the list.
|
|
221
|
+
*/
|
|
222
|
+
export function createTodoCompactionContextExtension(options) {
|
|
223
|
+
return {
|
|
224
|
+
name: "operator-chat-todo-compaction-context",
|
|
225
|
+
hidden: true,
|
|
226
|
+
factory: (api) => {
|
|
227
|
+
let compactedInCurrentTurn = false;
|
|
228
|
+
api.on("turn_start", () => {
|
|
229
|
+
compactedInCurrentTurn = false;
|
|
230
|
+
});
|
|
231
|
+
api.on("session_compact", () => {
|
|
232
|
+
compactedInCurrentTurn = true;
|
|
233
|
+
});
|
|
234
|
+
api.on("context", (rawEvent) => {
|
|
235
|
+
if (!compactedInCurrentTurn)
|
|
236
|
+
return undefined;
|
|
237
|
+
const context = buildTodoContinuationContext(options.registry.get(options.sessionId));
|
|
238
|
+
if (!context)
|
|
239
|
+
return undefined;
|
|
240
|
+
const messages = Array.isArray(rawEvent.messages)
|
|
241
|
+
? rawEvent.messages
|
|
242
|
+
: [];
|
|
243
|
+
return {
|
|
244
|
+
messages: [
|
|
245
|
+
...messages,
|
|
246
|
+
{
|
|
247
|
+
role: "custom",
|
|
248
|
+
customType: "operator-chat-todo-context",
|
|
249
|
+
content: context,
|
|
250
|
+
display: false,
|
|
251
|
+
details: { todos: options.registry.get(options.sessionId) },
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
213
260
|
/**
|
|
214
261
|
* Build Pi custom ToolDefinition objects for every whitelisted operator action.
|
|
215
262
|
* Each tool's execute() routes into the operator action dispatcher (Gate 3).
|
|
@@ -289,6 +336,76 @@ async function buildCustomOperatorTools(options) {
|
|
|
289
336
|
});
|
|
290
337
|
});
|
|
291
338
|
}
|
|
339
|
+
const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
|
|
340
|
+
"The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
|
|
341
|
+
"Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
|
|
342
|
+
async function buildAskUserQuestionTool(options) {
|
|
343
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
344
|
+
import("typebox"),
|
|
345
|
+
import("@earendil-works/pi-coding-agent"),
|
|
346
|
+
]);
|
|
347
|
+
return defineTool({
|
|
348
|
+
name: "ask_user_question",
|
|
349
|
+
label: "Ask user",
|
|
350
|
+
description: ASK_USER_QUESTION_DESCRIPTION,
|
|
351
|
+
promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
|
|
352
|
+
parameters: Type.Object({
|
|
353
|
+
questions: Type.Array(Type.Object({
|
|
354
|
+
id: Type.String({ description: "Stable question id echoed in the answer." }),
|
|
355
|
+
question: Type.String({ description: "Specific user-facing question." }),
|
|
356
|
+
header: Type.Optional(Type.String({ description: "Optional short heading." })),
|
|
357
|
+
detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
|
|
358
|
+
options: Type.Optional(Type.Array(Type.Object({
|
|
359
|
+
label: Type.String({ description: "Short user-facing option label." }),
|
|
360
|
+
description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
|
|
361
|
+
}, { additionalProperties: false }))),
|
|
362
|
+
multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
|
|
363
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
|
|
364
|
+
}, { additionalProperties: false }),
|
|
365
|
+
async execute(toolCallId, params, signal) {
|
|
366
|
+
const answer = await options.registry.ask({
|
|
367
|
+
sessionId: options.sessionId,
|
|
368
|
+
toolCallId,
|
|
369
|
+
questions: params.questions,
|
|
370
|
+
signal,
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
content: [{ type: "text", text: JSON.stringify(answer) }],
|
|
374
|
+
details: answer,
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const TODO_WRITE_DESCRIPTION = "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list. Use it for multi-step work; skip trivial single-step tasks. Mark tasks pending, in_progress, or completed, and mark completed immediately when done.";
|
|
380
|
+
async function buildTodoWriteTool(options) {
|
|
381
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
382
|
+
import("typebox"),
|
|
383
|
+
import("@earendil-works/pi-coding-agent"),
|
|
384
|
+
]);
|
|
385
|
+
return defineTool({
|
|
386
|
+
name: "todo_write",
|
|
387
|
+
label: "Update todo list",
|
|
388
|
+
description: TODO_WRITE_DESCRIPTION,
|
|
389
|
+
promptSnippet: TODO_WRITE_DESCRIPTION,
|
|
390
|
+
parameters: Type.Object({
|
|
391
|
+
todos: Type.Array(Type.Object({
|
|
392
|
+
content: Type.String({ description: "Short imperative task description." }),
|
|
393
|
+
status: Type.Union([
|
|
394
|
+
Type.Literal("pending"),
|
|
395
|
+
Type.Literal("in_progress"),
|
|
396
|
+
Type.Literal("completed"),
|
|
397
|
+
]),
|
|
398
|
+
}, { additionalProperties: false })),
|
|
399
|
+
}, { additionalProperties: false }),
|
|
400
|
+
async execute(toolCallId, params) {
|
|
401
|
+
const result = options.registry.write(options.sessionId, toolCallId, params.todos);
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
404
|
+
details: result,
|
|
405
|
+
};
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
292
409
|
/**
|
|
293
410
|
* Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
|
|
294
411
|
* safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
|
|
@@ -765,9 +882,26 @@ export class ConsolePiRuntime {
|
|
|
765
882
|
* after a PATCH sees the latest resource-preferences.json (not create-time
|
|
766
883
|
* closure state).
|
|
767
884
|
*/
|
|
768
|
-
async buildResourceLoaderOptions(base = {}) {
|
|
885
|
+
async buildResourceLoaderOptions(base = {}, sessionId) {
|
|
886
|
+
const inheritedFactories = Array.isArray(base.extensionFactories)
|
|
887
|
+
? base.extensionFactories
|
|
888
|
+
: [];
|
|
889
|
+
const todoContextExtension = sessionId && this.options.todoRegistry
|
|
890
|
+
? createTodoCompactionContextExtension({
|
|
891
|
+
sessionId,
|
|
892
|
+
registry: this.options.todoRegistry,
|
|
893
|
+
})
|
|
894
|
+
: undefined;
|
|
769
895
|
return {
|
|
770
896
|
...base,
|
|
897
|
+
...(todoContextExtension
|
|
898
|
+
? {
|
|
899
|
+
extensionFactories: [
|
|
900
|
+
...inheritedFactories,
|
|
901
|
+
todoContextExtension,
|
|
902
|
+
],
|
|
903
|
+
}
|
|
904
|
+
: {}),
|
|
771
905
|
skillsOverride: (current) => {
|
|
772
906
|
// Synchronous override surface: use last-known prefs via a sync
|
|
773
907
|
// fail-closed path. Prefer async cache filled by ensurePrefs.
|
|
@@ -969,9 +1103,7 @@ export class ConsolePiRuntime {
|
|
|
969
1103
|
const { services } = await this.bindings.createServices({
|
|
970
1104
|
cwd: this.options.cwd,
|
|
971
1105
|
agentDir,
|
|
972
|
-
resourceLoaderOptions: await this.buildResourceLoaderOptions({
|
|
973
|
-
appendSystemPrompt,
|
|
974
|
-
}),
|
|
1106
|
+
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, sessionId),
|
|
975
1107
|
});
|
|
976
1108
|
if (this.disposedSessions.has(sessionId))
|
|
977
1109
|
return;
|
|
@@ -988,7 +1120,7 @@ export class ConsolePiRuntime {
|
|
|
988
1120
|
}) ?? undefined)
|
|
989
1121
|
: undefined;
|
|
990
1122
|
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
991
|
-
const customTools = await this.loadCustomTools();
|
|
1123
|
+
const customTools = await this.loadCustomTools(sessionId);
|
|
992
1124
|
if (this.disposedSessions.has(sessionId))
|
|
993
1125
|
return;
|
|
994
1126
|
// pi-web parity: do NOT pass `tools`. A closed allowlist becomes
|
|
@@ -1045,12 +1177,26 @@ export class ConsolePiRuntime {
|
|
|
1045
1177
|
}
|
|
1046
1178
|
}
|
|
1047
1179
|
/** Cached session-less custom tool definitions (operator + explore). */
|
|
1048
|
-
async loadCustomTools() {
|
|
1180
|
+
async loadCustomTools(sessionId) {
|
|
1049
1181
|
const [operatorCustomTools, exploreCustomTools] = await Promise.all([
|
|
1050
1182
|
this.loadOperatorCustomTools(),
|
|
1051
1183
|
this.loadExploreCustomTools(),
|
|
1052
1184
|
]);
|
|
1053
|
-
|
|
1185
|
+
const questionTool = this.options.questionRegistry
|
|
1186
|
+
? await buildAskUserQuestionTool({
|
|
1187
|
+
sessionId,
|
|
1188
|
+
registry: this.options.questionRegistry,
|
|
1189
|
+
})
|
|
1190
|
+
: undefined;
|
|
1191
|
+
const todoTool = this.options.todoRegistry
|
|
1192
|
+
? await buildTodoWriteTool({ sessionId, registry: this.options.todoRegistry })
|
|
1193
|
+
: undefined;
|
|
1194
|
+
return [
|
|
1195
|
+
...operatorCustomTools,
|
|
1196
|
+
...exploreCustomTools,
|
|
1197
|
+
...(questionTool ? [questionTool] : []),
|
|
1198
|
+
...(todoTool ? [todoTool] : []),
|
|
1199
|
+
];
|
|
1054
1200
|
}
|
|
1055
1201
|
loadOperatorCustomTools() {
|
|
1056
1202
|
if (!this.options.actionContext)
|
|
@@ -1090,6 +1236,7 @@ export class ConsolePiRuntime {
|
|
|
1090
1236
|
targetSessionId: sessionId,
|
|
1091
1237
|
});
|
|
1092
1238
|
const built = await this.buildSessionWithServices({
|
|
1239
|
+
sessionId,
|
|
1093
1240
|
sessionManager,
|
|
1094
1241
|
model: init.model,
|
|
1095
1242
|
});
|
|
@@ -1817,6 +1964,7 @@ export class ConsolePiRuntime {
|
|
|
1817
1964
|
}
|
|
1818
1965
|
try {
|
|
1819
1966
|
const built = await this.buildSessionWithServices({
|
|
1967
|
+
sessionId,
|
|
1820
1968
|
sessionManager,
|
|
1821
1969
|
model: init.model,
|
|
1822
1970
|
systemPromptSuffix: init.systemPromptSuffix,
|
|
@@ -1907,9 +2055,7 @@ export class ConsolePiRuntime {
|
|
|
1907
2055
|
const { services } = await this.bindings.createServices({
|
|
1908
2056
|
cwd: this.options.cwd,
|
|
1909
2057
|
agentDir,
|
|
1910
|
-
resourceLoaderOptions: await this.buildResourceLoaderOptions({
|
|
1911
|
-
appendSystemPrompt,
|
|
1912
|
-
}),
|
|
2058
|
+
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
|
|
1913
2059
|
});
|
|
1914
2060
|
const modelRuntime = services.modelRuntime;
|
|
1915
2061
|
const resolvedModel = init.model
|
|
@@ -1920,7 +2066,7 @@ export class ConsolePiRuntime {
|
|
|
1920
2066
|
}) ?? undefined)
|
|
1921
2067
|
: undefined;
|
|
1922
2068
|
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
1923
|
-
const customTools = await this.loadCustomTools();
|
|
2069
|
+
const customTools = await this.loadCustomTools(init.sessionId);
|
|
1924
2070
|
const { session } = await this.bindings.createSessionFromServices({
|
|
1925
2071
|
services,
|
|
1926
2072
|
sessionManager: init.sessionManager,
|
|
@@ -1943,6 +2089,16 @@ export class ConsolePiRuntime {
|
|
|
1943
2089
|
open(sessionId) {
|
|
1944
2090
|
return this.sessions.get(sessionId);
|
|
1945
2091
|
}
|
|
2092
|
+
/** Inject one queued message into the currently running Pi turn. */
|
|
2093
|
+
async steer(sessionId, text, images) {
|
|
2094
|
+
const session = this.sessions.get(sessionId);
|
|
2095
|
+
if (!session || !this.promptOwners.has(sessionId) || session.isStreaming === false) {
|
|
2096
|
+
throw new Error("CHAT_TURN_NOT_ACTIVE");
|
|
2097
|
+
}
|
|
2098
|
+
if (typeof session.steer !== "function")
|
|
2099
|
+
throw new Error("CHAT_STEER_UNAVAILABLE");
|
|
2100
|
+
await session.steer(text, images);
|
|
2101
|
+
}
|
|
1946
2102
|
hasSession(sessionId) {
|
|
1947
2103
|
return this.sessions.has(sessionId);
|
|
1948
2104
|
}
|
|
@@ -1997,6 +2153,12 @@ export class ConsolePiRuntime {
|
|
|
1997
2153
|
}
|
|
1998
2154
|
};
|
|
1999
2155
|
try {
|
|
2156
|
+
if (options?.turnId && this.options.questionRegistry) {
|
|
2157
|
+
this.options.questionRegistry.setTurn(sessionId, options.turnId);
|
|
2158
|
+
}
|
|
2159
|
+
if (options?.turnId && this.options.todoRegistry) {
|
|
2160
|
+
this.options.todoRegistry.startTurn(sessionId, options.turnId);
|
|
2161
|
+
}
|
|
2000
2162
|
// AC-FIX-002: if Stop already fired before readiness, never start model work.
|
|
2001
2163
|
if (options?.signal?.aborted) {
|
|
2002
2164
|
return;
|
|
@@ -2050,19 +2212,77 @@ export class ConsolePiRuntime {
|
|
|
2050
2212
|
}
|
|
2051
2213
|
};
|
|
2052
2214
|
emitContextUsage();
|
|
2215
|
+
let stepStartedAt;
|
|
2216
|
+
let firstTokenAt;
|
|
2217
|
+
let pendingProviderError;
|
|
2218
|
+
let providerErrorEmitted = false;
|
|
2219
|
+
const emitPendingProviderError = () => {
|
|
2220
|
+
if (!pendingProviderError)
|
|
2221
|
+
return;
|
|
2222
|
+
onEvent(pendingProviderError);
|
|
2223
|
+
pendingProviderError = undefined;
|
|
2224
|
+
providerErrorEmitted = true;
|
|
2225
|
+
};
|
|
2053
2226
|
unsub = session.subscribe((event) => {
|
|
2227
|
+
const observedAt = Date.now();
|
|
2054
2228
|
const mapped = mapSdkEvent(sessionId, event);
|
|
2055
|
-
if (mapped)
|
|
2056
|
-
|
|
2229
|
+
if (mapped?.type === "error") {
|
|
2230
|
+
// Pi represents provider failures as an assistant message_end and may
|
|
2231
|
+
// retry internally. Hold the candidate until the SDK says the attempt
|
|
2232
|
+
// is terminal; a later successful message_end supersedes it.
|
|
2233
|
+
pendingProviderError = mapped;
|
|
2234
|
+
stepStartedAt = undefined;
|
|
2235
|
+
firstTokenAt = undefined;
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
if (mapped?.type === "message_end")
|
|
2239
|
+
pendingProviderError = undefined;
|
|
2240
|
+
if ((mapped?.type === "agent_end" && !mapped.willRetry) ||
|
|
2241
|
+
mapped?.type === "agent_settled") {
|
|
2242
|
+
emitPendingProviderError();
|
|
2243
|
+
}
|
|
2244
|
+
if (mapped?.type === "agent_start") {
|
|
2245
|
+
stepStartedAt = observedAt;
|
|
2246
|
+
firstTokenAt = undefined;
|
|
2247
|
+
}
|
|
2248
|
+
if (mapped?.type === "message_update" && firstTokenAt === undefined) {
|
|
2249
|
+
firstTokenAt = observedAt;
|
|
2250
|
+
}
|
|
2057
2251
|
// Pi puts per-assistant-message usage on `message.usage` ({ input, output,
|
|
2058
2252
|
// cacheRead, cacheWrite, totalTokens, cost }). Emit on message_end so
|
|
2059
2253
|
// multi-step tool turns accumulate like pi-web SessionInfoBar; turn_end
|
|
2060
2254
|
// repeats the final message and must not double-count.
|
|
2061
|
-
if (
|
|
2255
|
+
if (mapped?.type === "message_end") {
|
|
2062
2256
|
const usage = extractUsageSample(event);
|
|
2257
|
+
const timing = stepStartedAt === undefined
|
|
2258
|
+
? undefined
|
|
2259
|
+
: {
|
|
2260
|
+
llmMs: Math.max(0, observedAt - stepStartedAt),
|
|
2261
|
+
...(firstTokenAt === undefined
|
|
2262
|
+
? {}
|
|
2263
|
+
: {
|
|
2264
|
+
ttftMs: Math.max(0, firstTokenAt - stepStartedAt),
|
|
2265
|
+
decodeMs: Math.max(0, observedAt - firstTokenAt),
|
|
2266
|
+
}),
|
|
2267
|
+
...(usage?.outputTokens === undefined
|
|
2268
|
+
? {}
|
|
2269
|
+
: { outputTokens: usage.outputTokens }),
|
|
2270
|
+
};
|
|
2271
|
+
onEvent({ ...mapped, ...(timing ? { timing } : {}) });
|
|
2063
2272
|
if (usage)
|
|
2064
2273
|
onEvent({ type: "usage", sessionId, usage });
|
|
2065
2274
|
emitContextUsage();
|
|
2275
|
+
stepStartedAt = undefined;
|
|
2276
|
+
firstTokenAt = undefined;
|
|
2277
|
+
}
|
|
2278
|
+
else if (mapped) {
|
|
2279
|
+
onEvent(mapped);
|
|
2280
|
+
if (mapped.type === "tool_result") {
|
|
2281
|
+
// The next model request starts after the final completed tool. If
|
|
2282
|
+
// tools run concurrently, the last result naturally wins.
|
|
2283
|
+
stepStartedAt = observedAt;
|
|
2284
|
+
firstTokenAt = undefined;
|
|
2285
|
+
}
|
|
2066
2286
|
}
|
|
2067
2287
|
// Keep the context fill bar live through the turn (pi-web SessionInfoBar).
|
|
2068
2288
|
if (event.type === "agent_start" ||
|
|
@@ -2094,6 +2314,7 @@ export class ConsolePiRuntime {
|
|
|
2094
2314
|
// User abort is closed out as Turn state `aborted`; do not publish a
|
|
2095
2315
|
// red ordinary error event for intentional Stop.
|
|
2096
2316
|
if (!options?.signal?.aborted) {
|
|
2317
|
+
emitPendingProviderError();
|
|
2097
2318
|
onEvent({ type: "agent_settled", sessionId });
|
|
2098
2319
|
}
|
|
2099
2320
|
}
|
|
@@ -2104,8 +2325,16 @@ export class ConsolePiRuntime {
|
|
|
2104
2325
|
// User abort: swallow ordinary error publication (AC-FIX-003).
|
|
2105
2326
|
return;
|
|
2106
2327
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2328
|
+
emitPendingProviderError();
|
|
2329
|
+
if (!providerErrorEmitted) {
|
|
2330
|
+
const providerError = parseProviderError(error);
|
|
2331
|
+
onEvent({
|
|
2332
|
+
type: "error",
|
|
2333
|
+
sessionId,
|
|
2334
|
+
message: providerError.message,
|
|
2335
|
+
providerError,
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2109
2338
|
throw error;
|
|
2110
2339
|
}
|
|
2111
2340
|
finally {
|
|
@@ -2114,6 +2343,8 @@ export class ConsolePiRuntime {
|
|
|
2114
2343
|
}
|
|
2115
2344
|
}
|
|
2116
2345
|
finally {
|
|
2346
|
+
if (this.options.questionRegistry)
|
|
2347
|
+
this.options.questionRegistry.clearTurn(sessionId);
|
|
2117
2348
|
this.promptOwners.delete(sessionId);
|
|
2118
2349
|
}
|
|
2119
2350
|
}
|
|
@@ -2243,6 +2474,23 @@ export class ConsolePiRuntime {
|
|
|
2243
2474
|
}
|
|
2244
2475
|
}
|
|
2245
2476
|
function mapAssistantMessageEvent(type, sessionId, message) {
|
|
2477
|
+
const record = message !== null && typeof message === "object"
|
|
2478
|
+
? message
|
|
2479
|
+
: undefined;
|
|
2480
|
+
if (type === "message_end" &&
|
|
2481
|
+
record?.stopReason === "error") {
|
|
2482
|
+
const content = extractAssistantContentBlocks(message);
|
|
2483
|
+
const providerError = parseProviderError(typeof record.errorMessage === "string"
|
|
2484
|
+
? record.errorMessage
|
|
2485
|
+
: "Provider request failed");
|
|
2486
|
+
return {
|
|
2487
|
+
type: "error",
|
|
2488
|
+
sessionId,
|
|
2489
|
+
message: providerError.message,
|
|
2490
|
+
providerError,
|
|
2491
|
+
...(content.length > 0 ? { content } : {}),
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2246
2494
|
const content = extractAssistantContentBlocks(message);
|
|
2247
2495
|
return {
|
|
2248
2496
|
type,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const MAX_RAW_CHARS = 8_000;
|
|
2
|
+
const MAX_MESSAGE_CHARS = 1_000;
|
|
3
|
+
function asRecord(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: undefined;
|
|
7
|
+
}
|
|
8
|
+
function bounded(value, limit) {
|
|
9
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
10
|
+
}
|
|
11
|
+
function scrub(value) {
|
|
12
|
+
return value
|
|
13
|
+
.replace(/((?:authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|secret)["']?\s*[:=]\s*["']?)([^"'\s,}]+)/gi, "$1[REDACTED]")
|
|
14
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
|
|
15
|
+
}
|
|
16
|
+
function parseEnvelope(raw) {
|
|
17
|
+
const candidates = [raw];
|
|
18
|
+
const start = raw.indexOf("{");
|
|
19
|
+
const end = raw.lastIndexOf("}");
|
|
20
|
+
if (start >= 0 && end > start)
|
|
21
|
+
candidates.push(raw.slice(start, end + 1));
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
try {
|
|
24
|
+
const record = asRecord(JSON.parse(candidate));
|
|
25
|
+
if (record)
|
|
26
|
+
return record;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Plain text and HTTP-prefixed provider errors are expected here.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function stringField(record, key) {
|
|
35
|
+
const value = record?.[key];
|
|
36
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
37
|
+
}
|
|
38
|
+
function codeField(record) {
|
|
39
|
+
const value = record?.code ?? record?.status ?? record?.statusCode;
|
|
40
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
41
|
+
return value;
|
|
42
|
+
if (typeof value === "string" && value.trim()) {
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return /^\d{3}$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function classify(input) {
|
|
49
|
+
const haystack = `${input.message}\n${input.raw}`.toLowerCase();
|
|
50
|
+
const status = typeof input.code === "number" ? input.code : undefined;
|
|
51
|
+
if (status === 401 ||
|
|
52
|
+
status === 403 ||
|
|
53
|
+
/认证|鉴权|unauthori[sz]ed|forbidden|authentication|invalid api[-_ ]?key/.test(haystack))
|
|
54
|
+
return "authentication";
|
|
55
|
+
if (/请求上下文过大|上下文.{0,12}(?:过大|太大|过长|超限)|context_length_exceeded|maximum context|context.{0,24}(?:length|window|limit|large|long)|too many tokens/.test(haystack))
|
|
56
|
+
return "context-overflow";
|
|
57
|
+
if (status === 429 ||
|
|
58
|
+
/rate.?limit|too many requests|请求.{0,8}(?:频繁|过多)|限流/.test(haystack))
|
|
59
|
+
return "rate-limit";
|
|
60
|
+
return "provider";
|
|
61
|
+
}
|
|
62
|
+
/** Convert an SDK/provider error into a bounded durable timeline projection. */
|
|
63
|
+
export function parseProviderError(value) {
|
|
64
|
+
const source = typeof value === "string"
|
|
65
|
+
? value
|
|
66
|
+
: value instanceof Error
|
|
67
|
+
? value.message
|
|
68
|
+
: String(value ?? "Provider request failed");
|
|
69
|
+
const raw = bounded(scrub(source.trim()), MAX_RAW_CHARS);
|
|
70
|
+
const envelope = parseEnvelope(raw);
|
|
71
|
+
const nested = asRecord(envelope?.error);
|
|
72
|
+
const firstLine = raw.split(/\r?\n/, 1)[0]?.trim();
|
|
73
|
+
const message = bounded(stringField(nested, "message") ??
|
|
74
|
+
stringField(envelope, "message") ??
|
|
75
|
+
(firstLine || undefined) ??
|
|
76
|
+
"Provider request failed", MAX_MESSAGE_CHARS);
|
|
77
|
+
const code = codeField(nested) ?? codeField(envelope);
|
|
78
|
+
const type = stringField(nested, "type") ?? stringField(envelope, "type");
|
|
79
|
+
return {
|
|
80
|
+
message,
|
|
81
|
+
raw,
|
|
82
|
+
...(code === undefined ? {} : { code }),
|
|
83
|
+
...(type ? { type } : {}),
|
|
84
|
+
category: classify({ message, raw, code }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function providerErrorTitle(category) {
|
|
88
|
+
switch (category) {
|
|
89
|
+
case "authentication":
|
|
90
|
+
return "认证失败";
|
|
91
|
+
case "context-overflow":
|
|
92
|
+
return "上下文过大";
|
|
93
|
+
case "rate-limit":
|
|
94
|
+
return "请求过于频繁";
|
|
95
|
+
default:
|
|
96
|
+
return "Provider 请求失败";
|
|
97
|
+
}
|
|
98
|
+
}
|