@tea-agent/loop-agent 0.42.1 → 0.43.0-next.10
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 +63 -3
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +5 -20
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +78 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +100 -1104
- package/dist/worker/console/chat/routes.js +32 -17
- package/dist/worker/console/chat/terminal-sessions.js +65 -1
- package/dist/worker/console/chat/terminal-tools.js +17 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C9eI7nEo.js → abnfDiagram-N423BO3Z-D_fNbWc_.js} +1 -1
- package/dist/worker/console/static/assets/{arc-VJWsxWhB.js → arc-_HuqLW6m.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BGcyiMSo.js → architectureDiagram-T3A2C74G-Djy2qY1m.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BScnFwyi.js → blockDiagram-VBNYF7ZC-bB-1TSyY.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bl9_BsLi.js → c4Diagram-5PPSVZJV-C5Hmghrz.js} +1 -1
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-F1uKxiUt.js → chunk-2GRJ4B5K-Duyl39oy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-rSRMynVu.js → chunk-2Q5K7J3B-x6mdnSyf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DAsJm1WD.js → chunk-5RXB4S5H-WlBuFj7W.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DQIdVI0a.js → chunk-5VM5RSS4-CTF429HX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4A_8rJ-Q.js → chunk-6Q2QTUOP-Dwd6r-SF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-BHnT-vjh.js → chunk-GF5L2VYU-C-Br82D1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Rq7QKBkn.js → chunk-JWPE2WC7-CuGp0DeE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BXh-AI2u.js → chunk-KBJHAD2P-CJhFX6Lh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BblcLy8m.js → chunk-RYQCIY6F-DpGOa_jT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-zHZmVnB6.js → chunk-XXDRQBXY-DXJaTynd.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Cmwz0Wi4.js → cose-bilkent-JH36ORCC-CRBZWjE4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Dq76MHQU.js → cynefin-VYW2F7L2-BdXcxtlb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-zPahV1fM.js → cynefinDiagram-MW4NZA55-07UKsOts.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DQJ3xPc_.js → dagre-VZM6K2ZE-D38p9__S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BwX7u6fv.js → diagram-7IWD3JNH-nSiBNNjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CodnDCco.js → diagram-B4RE2ZJO-BFOTot5D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-gAjtWkxG.js → diagram-LBJQPF4R-DypKaZ6k.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CGCqz0cS.js → diagram-Q27KOJAE-B6yoOQeW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-RHwYEywd.js → diagram-UB23O5K3-BHYS6viX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B6seb_p_.js → ebnfDiagram-BXEA7PRR-D2VD5Snj.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmwCKYe8.js → erDiagram-JOGREHBK-E98mWxN1.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BfNsaZ0V.js → flowDiagram-UKHOOZJN-CT1zxhqP.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-zmYgi_Z3.js → ganttDiagram-PKOTCBZU-6GcngNcy.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BDEzzSKF.js → gitGraphDiagram-DS77QQ5N-DCL6NvKe.js} +1 -1
- package/dist/worker/console/static/assets/{index-DgenUAfc.js → index-24jp3aBh.js} +77 -77
- package/dist/worker/console/static/assets/index-CY6y6I8M.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DeSz8l42.js → infoDiagram-6WML65LV-C0627e-9.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B1LV29aH.js → ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CCw4W2oU.js → journeyDiagram-NVQOT4AX-DPDPv9HL.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BozaMoRJ.js → kanban-definition-27J2QSJJ-D3OHdWiO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-G1JR40SX.js → linear-DI88zD7n.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CCgPF0oA.js → mermaid.core-p2l8IrWY.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CEqnCHnD.js → mindmap-definition-FAOFIHXS-B1bAX-oS.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DpmQPKuI.js → pegDiagram-VL7TDLO6-DwOnWxdD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BP501AxR.js → pieDiagram-7S7Q4E2Y-xRkTXnJC.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-D1dBhl_A.js → quadrantDiagram-CIZ2JOQS-_9UC5Zio.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DgZDS5pY.js → railroadDiagram-AXF67PYL-CgEUirWJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BzB3pyIq.js → requirementDiagram-LRYGKXZP-Czbpwiyz.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B9ZprFFP.js → sankeyDiagram-W5VNT64P-_j7_KiqY.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CC2-_XrA.js → sequenceDiagram-SI44F4Z6-BNJa6Hyi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UVtAUqgB.js → sizeCapture-X5ZJPWSS-DedpKtOc.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CWcS5JYG.js → stateDiagram-OKZ733FA-BBvthr7c.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C4p-dn06.js → swimlanes-SLNWSIFB-Ds7umgEM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Dnz52OHX.js → timeline-definition-Z64GVDOM-CKa-p1nF.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CzTE0Nfu.js → vennDiagram-T6HMQDX7-LoUNR56o.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-NAR9cjQQ.js → wardleyDiagram-T6FBY63Y-Bqbl6ym7.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DVMk8413.js → xychartDiagram-ELKLHX3M-B8dUg4lf.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +13 -6
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
- package/dist/worker/observe/static/styles.css +7 -4
- package/dist/worker/observe/static/views/dag-inspector.js +5 -19
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +0 -1
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +0 -8
|
@@ -26,28 +26,35 @@
|
|
|
26
26
|
* `createDefaultPiSdkBindings()`.
|
|
27
27
|
*/
|
|
28
28
|
import { createHash, randomBytes } from "node:crypto";
|
|
29
|
-
import {
|
|
29
|
+
import { stat } from "node:fs/promises";
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
|
|
32
32
|
import { parseProviderError, } from "./provider-error.js";
|
|
33
33
|
import { projectCompactSnapshot, } from "./chat-event-store.js";
|
|
34
34
|
import { extractUsageSample } from "./usage.js";
|
|
35
35
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
36
|
-
import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE,
|
|
36
|
+
import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, chatActiveToolBaseline, composeChatSystemPrompt, composeChatSystemPromptParts, computeChatActiveToolNames, shouldInjectGoalContinuation, shouldLoadWorkflowBusinessTools, } from "./session-mode.js";
|
|
37
37
|
import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
|
|
38
38
|
import { createOperatorChatResourceLoader, } from "./resource-loader.js";
|
|
39
|
-
import { applySkillAutoInvocationPreferences,
|
|
40
|
-
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
39
|
+
import { applySkillAutoInvocationPreferences, loadResourcePreferences, } from "./resource-preferences-store.js";
|
|
41
40
|
import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
|
|
42
41
|
import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
|
|
43
42
|
import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
|
|
44
43
|
import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
45
|
-
import {
|
|
44
|
+
import { redactRuntimeText } from "./runtime-context.js";
|
|
46
45
|
import { buildTodoContinuationContext, } from "./todos.js";
|
|
47
46
|
import { buildPersistentTerminalTools } from "./terminal-tools.js";
|
|
48
47
|
import { buildScmTools } from "./scm-tools.js";
|
|
49
48
|
import { buildGoalContinuationContext, } from "./goals.js";
|
|
50
49
|
import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
|
|
50
|
+
import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
|
|
51
|
+
import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
|
|
52
|
+
import { buildTodoWriteTool } from "./pi-runtime/custom-tools/todo-write.js";
|
|
53
|
+
import { buildGoalTools } from "./pi-runtime/custom-tools/goal-tools.js";
|
|
54
|
+
import { buildExploreCustomTools } from "./pi-runtime/custom-tools/explore-tools.js";
|
|
55
|
+
import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime/inspection-types.js";
|
|
56
|
+
import { collectRuntimeInventories, collectSlashCommands, projectRuntimeSnapshot, } from "./pi-runtime/inspection.js";
|
|
57
|
+
import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-bindings.js";
|
|
51
58
|
/**
|
|
52
59
|
* Base system prompt fragment every General Operator Chat session receives.
|
|
53
60
|
*
|
|
@@ -278,595 +285,8 @@ export function createWizardLocalHeadersExtension(options) {
|
|
|
278
285
|
},
|
|
279
286
|
};
|
|
280
287
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
* Each tool's execute() routes into the operator action dispatcher (Gate 3).
|
|
284
|
-
* Requires the `typebox` + `defineTool` packages (both bundled with the SDK).
|
|
285
|
-
*/
|
|
286
|
-
async function buildCustomOperatorTools(options) {
|
|
287
|
-
// typebox is a transitive dep of @earendil-works/pi-coding-agent; Node's
|
|
288
|
-
// resolver hoists to the SDK's nested copy. defineTool is re-exported by SDK.
|
|
289
|
-
const [{ Type }, { defineTool }] = await Promise.all([
|
|
290
|
-
import("typebox"),
|
|
291
|
-
import("@earendil-works/pi-coding-agent"),
|
|
292
|
-
]);
|
|
293
|
-
const schemas = buildModelCallableToolSchemas();
|
|
294
|
-
const ctx = options.actionContext;
|
|
295
|
-
return schemas.map((schema) => {
|
|
296
|
-
// TypeBox schema is built dynamically; cast through unknown to the builder
|
|
297
|
-
// since Static<TParams> is structural and cannot be statically inferred here.
|
|
298
|
-
const parameters = Type.Object(schema.inputParams.reduce((acc, param) => {
|
|
299
|
-
// Carry the registry's param description into the tool schema so
|
|
300
|
-
// the model sees parameter semantics, not just names/types.
|
|
301
|
-
const desc = param.description?.trim()
|
|
302
|
-
? { description: param.description }
|
|
303
|
-
: {};
|
|
304
|
-
let t;
|
|
305
|
-
switch (param.type) {
|
|
306
|
-
case "string":
|
|
307
|
-
t = Type.String(desc);
|
|
308
|
-
break;
|
|
309
|
-
case "boolean":
|
|
310
|
-
t = Type.Boolean(desc);
|
|
311
|
-
break;
|
|
312
|
-
case "number":
|
|
313
|
-
t = Type.Number(desc);
|
|
314
|
-
break;
|
|
315
|
-
case "array":
|
|
316
|
-
t = Type.Array(Type.String({}), Object.keys(desc).length ? desc : {});
|
|
317
|
-
break;
|
|
318
|
-
default:
|
|
319
|
-
t = Type.Object({}, { additionalProperties: true });
|
|
320
|
-
}
|
|
321
|
-
acc[param.name] = param.required ? t : Type.Optional(t);
|
|
322
|
-
return acc;
|
|
323
|
-
}, {}), { additionalProperties: false });
|
|
324
|
-
return defineTool({
|
|
325
|
-
name: schema.toolId,
|
|
326
|
-
label: schema.toolId,
|
|
327
|
-
description: `${schema.description} (kind=${schema.kind}; cli: ${schema.cli})`,
|
|
328
|
-
promptSnippet: schema.description,
|
|
329
|
-
parameters: parameters,
|
|
330
|
-
async execute(toolCallId, params) {
|
|
331
|
-
const decision = authorizeChatSessionTool(schema.toolId, options.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
|
|
332
|
-
if (!decision.ok) {
|
|
333
|
-
return {
|
|
334
|
-
content: [
|
|
335
|
-
{
|
|
336
|
-
type: "text",
|
|
337
|
-
text: JSON.stringify({
|
|
338
|
-
error: decision.message,
|
|
339
|
-
code: decision.code,
|
|
340
|
-
}),
|
|
341
|
-
},
|
|
342
|
-
],
|
|
343
|
-
details: { error: decision.message, code: decision.code },
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
// Defer require to avoid circular import at module load.
|
|
347
|
-
const { dispatchChatToolCall } = await import("./session-store.js");
|
|
348
|
-
const result = await dispatchChatToolCall(ctx, decision.toolId, params, toolCallId, options.sessionMode);
|
|
349
|
-
const payload = result.ok
|
|
350
|
-
? result.result
|
|
351
|
-
: { error: result.message, code: result.errorCode };
|
|
352
|
-
return {
|
|
353
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
354
|
-
details: payload,
|
|
355
|
-
};
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
|
|
361
|
-
"The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
|
|
362
|
-
"Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
|
|
363
|
-
async function buildAskUserQuestionTool(options) {
|
|
364
|
-
const [{ Type }, { defineTool }] = await Promise.all([
|
|
365
|
-
import("typebox"),
|
|
366
|
-
import("@earendil-works/pi-coding-agent"),
|
|
367
|
-
]);
|
|
368
|
-
return defineTool({
|
|
369
|
-
name: "ask_user_question",
|
|
370
|
-
label: "Ask user",
|
|
371
|
-
description: ASK_USER_QUESTION_DESCRIPTION,
|
|
372
|
-
promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
|
|
373
|
-
parameters: Type.Object({
|
|
374
|
-
questions: Type.Array(Type.Object({
|
|
375
|
-
id: Type.String({ description: "Stable question id echoed in the answer." }),
|
|
376
|
-
question: Type.String({ description: "Specific user-facing question." }),
|
|
377
|
-
header: Type.Optional(Type.String({ description: "Optional short heading." })),
|
|
378
|
-
detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
|
|
379
|
-
options: Type.Optional(Type.Array(Type.Object({
|
|
380
|
-
label: Type.String({ description: "Short user-facing option label." }),
|
|
381
|
-
description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
|
|
382
|
-
}, { additionalProperties: false }))),
|
|
383
|
-
multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
|
|
384
|
-
}, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
|
|
385
|
-
}, { additionalProperties: false }),
|
|
386
|
-
async execute(toolCallId, params, signal) {
|
|
387
|
-
const answer = await options.registry.ask({
|
|
388
|
-
sessionId: options.sessionId,
|
|
389
|
-
toolCallId,
|
|
390
|
-
questions: params.questions,
|
|
391
|
-
signal,
|
|
392
|
-
});
|
|
393
|
-
return {
|
|
394
|
-
content: [{ type: "text", text: JSON.stringify(answer) }],
|
|
395
|
-
details: answer,
|
|
396
|
-
};
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
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 (no partial updates, no per-item edits). Before starting multi-step work, add one todo per concrete step. Keep at least one todo `in_progress` while work remains. Mark a todo `completed` immediately when it is done (do not batch completions) and resend the full list as progress or DAG state advances. Skip the list for simple Q&A, read-only lookups, and single-step tasks. Use it for multi-step implementation and whenever you prepare, run, or supervise a DAG — call `todo_write` before entering the DAG tool chain. Phrase DAG todos as user-facing phases users can understand, not copies of internal DAG nodes. Statuses: `pending` / `in_progress` / `completed`.";
|
|
401
|
-
async function buildTodoWriteTool(options) {
|
|
402
|
-
const [{ Type }, { defineTool }] = await Promise.all([
|
|
403
|
-
import("typebox"),
|
|
404
|
-
import("@earendil-works/pi-coding-agent"),
|
|
405
|
-
]);
|
|
406
|
-
const description = options.sessionMode === "pi"
|
|
407
|
-
? PI_TODO_WRITE_DESCRIPTION
|
|
408
|
-
: TODO_WRITE_DESCRIPTION;
|
|
409
|
-
return defineTool({
|
|
410
|
-
name: "todo_write",
|
|
411
|
-
label: "Update todo list",
|
|
412
|
-
description,
|
|
413
|
-
promptSnippet: description,
|
|
414
|
-
parameters: Type.Object({
|
|
415
|
-
todos: Type.Array(Type.Object({
|
|
416
|
-
content: Type.String({ description: "Short imperative task description." }),
|
|
417
|
-
status: Type.Union([
|
|
418
|
-
Type.Literal("pending"),
|
|
419
|
-
Type.Literal("in_progress"),
|
|
420
|
-
Type.Literal("completed"),
|
|
421
|
-
]),
|
|
422
|
-
}, { additionalProperties: false })),
|
|
423
|
-
}, { additionalProperties: false }),
|
|
424
|
-
async execute(toolCallId, params) {
|
|
425
|
-
const result = options.registry.write(options.sessionId, toolCallId, params.todos);
|
|
426
|
-
return {
|
|
427
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
428
|
-
details: result,
|
|
429
|
-
};
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
const GET_GOAL_DESCRIPTION = "Read the current session's durable cross-turn Goal, including revision, status, round limit, and whether this Console process is authorized to continue it.";
|
|
434
|
-
const CREATE_GOAL_DESCRIPTION = "Create a durable cross-turn Goal only when the user explicitly asks to start or create a Goal. Never silently upgrade an ordinary request. A session may have only one unfinished Goal.";
|
|
435
|
-
const UPDATE_GOAL_DESCRIPTION = "Finish or block the current durable Goal using its exact goalId and revision. Set complete only when the objective is genuinely achieved and no required work remains. Set blocked only when safe progress cannot continue; include the blocker.";
|
|
436
|
-
async function buildGoalTools(options) {
|
|
437
|
-
const [{ Type }, { defineTool }] = await Promise.all([
|
|
438
|
-
import("typebox"),
|
|
439
|
-
import("@earendil-works/pi-coding-agent"),
|
|
440
|
-
]);
|
|
441
|
-
const textResult = (value) => ({
|
|
442
|
-
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
443
|
-
details: value,
|
|
444
|
-
});
|
|
445
|
-
return [
|
|
446
|
-
defineTool({
|
|
447
|
-
name: "get_goal",
|
|
448
|
-
label: "Get goal",
|
|
449
|
-
description: GET_GOAL_DESCRIPTION,
|
|
450
|
-
promptSnippet: GET_GOAL_DESCRIPTION,
|
|
451
|
-
parameters: Type.Object({}, { additionalProperties: false }),
|
|
452
|
-
async execute() {
|
|
453
|
-
return textResult({ goal: options.registry.get(options.sessionId) });
|
|
454
|
-
},
|
|
455
|
-
}),
|
|
456
|
-
defineTool({
|
|
457
|
-
name: "create_goal",
|
|
458
|
-
label: "Create goal",
|
|
459
|
-
description: CREATE_GOAL_DESCRIPTION,
|
|
460
|
-
promptSnippet: CREATE_GOAL_DESCRIPTION,
|
|
461
|
-
parameters: Type.Object({
|
|
462
|
-
objective: Type.String({
|
|
463
|
-
description: "Concrete final objective and completion standard explicitly requested by the user.",
|
|
464
|
-
}),
|
|
465
|
-
}, { additionalProperties: false }),
|
|
466
|
-
async execute(_toolCallId, params) {
|
|
467
|
-
if (!options.registry.consumeModelCreateGrant(options.sessionId)) {
|
|
468
|
-
return textResult({
|
|
469
|
-
ok: false,
|
|
470
|
-
code: "GOAL_EXPLICIT_REQUEST_REQUIRED",
|
|
471
|
-
message: "create_goal requires an explicit user request to create or start a Goal",
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
return textResult(options.registry.create({
|
|
475
|
-
sessionId: options.sessionId,
|
|
476
|
-
objective: params.objective,
|
|
477
|
-
}));
|
|
478
|
-
},
|
|
479
|
-
}),
|
|
480
|
-
defineTool({
|
|
481
|
-
name: "update_goal",
|
|
482
|
-
label: "Update goal",
|
|
483
|
-
description: UPDATE_GOAL_DESCRIPTION,
|
|
484
|
-
promptSnippet: UPDATE_GOAL_DESCRIPTION,
|
|
485
|
-
parameters: Type.Object({
|
|
486
|
-
goalId: Type.String(),
|
|
487
|
-
revision: Type.Number({ minimum: 1 }),
|
|
488
|
-
status: Type.Union([
|
|
489
|
-
Type.Literal("complete"),
|
|
490
|
-
Type.Literal("blocked"),
|
|
491
|
-
]),
|
|
492
|
-
blocker: Type.Optional(Type.String()),
|
|
493
|
-
}, { additionalProperties: false }),
|
|
494
|
-
async execute(_toolCallId, params) {
|
|
495
|
-
return textResult(options.registry.update({
|
|
496
|
-
sessionId: options.sessionId,
|
|
497
|
-
goalId: params.goalId,
|
|
498
|
-
expectedRevision: params.revision,
|
|
499
|
-
phase: params.status,
|
|
500
|
-
blocker: params.blocker,
|
|
501
|
-
arm: false,
|
|
502
|
-
}));
|
|
503
|
-
},
|
|
504
|
-
}),
|
|
505
|
-
];
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
|
|
509
|
-
* safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
|
|
510
|
-
* the model can probe the repo WITHOUT write escapes (echo redirect, tee,
|
|
511
|
-
* sed -i, node -e) and WITHOUT reading sensitive files (env files, key/pem,
|
|
512
|
-
* auth.json, sessions dir) — and every returned byte is secret-scrubbed
|
|
513
|
-
* before reaching the model context.
|
|
514
|
-
*
|
|
515
|
-
* gitStatus/gitDiff use a FIXED argv (typed, no shell) so they cannot be
|
|
516
|
-
* turned into an arbitrary-shell vector.
|
|
517
|
-
*/
|
|
518
|
-
async function buildExploreCustomTools(options) {
|
|
519
|
-
const [{ Type }, { defineTool }] = await Promise.all([
|
|
520
|
-
import("typebox"),
|
|
521
|
-
import("@earendil-works/pi-coding-agent"),
|
|
522
|
-
]);
|
|
523
|
-
const { execFileSync } = await import("node:child_process");
|
|
524
|
-
const repoRoot = options.repoRoot;
|
|
525
|
-
const { safeReadFile, safeGrep, scrubSecrets } = await import("./explore-tools.js");
|
|
526
|
-
function safeGit(args) {
|
|
527
|
-
try {
|
|
528
|
-
const stdout = execFileSync("git", args, {
|
|
529
|
-
cwd: repoRoot,
|
|
530
|
-
encoding: "utf8",
|
|
531
|
-
// Fixed argv, no shell, bounded output.
|
|
532
|
-
maxBuffer: 512 * 1024,
|
|
533
|
-
timeout: 10_000,
|
|
534
|
-
windowsHide: true,
|
|
535
|
-
});
|
|
536
|
-
return { ok: true, stdout };
|
|
537
|
-
}
|
|
538
|
-
catch (e) {
|
|
539
|
-
return {
|
|
540
|
-
ok: false,
|
|
541
|
-
code: "GIT_FAILED",
|
|
542
|
-
message: e instanceof Error ? e.message : String(e),
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
const safeReadTool = defineTool({
|
|
547
|
-
name: "safe-read",
|
|
548
|
-
label: "safe-read",
|
|
549
|
-
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.",
|
|
550
|
-
promptSnippet: "safe-read: read a repo file (safe; secrets scrubbed)",
|
|
551
|
-
parameters: Type.Object({
|
|
552
|
-
path: Type.String({ description: "repo-relative file path" }),
|
|
553
|
-
}, { additionalProperties: false }),
|
|
554
|
-
async execute(_toolCallId, params) {
|
|
555
|
-
const raw = String(params.path ?? "");
|
|
556
|
-
try {
|
|
557
|
-
const result = await safeReadFile(repoRoot, raw);
|
|
558
|
-
return {
|
|
559
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
560
|
-
details: result,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
catch (e) {
|
|
564
|
-
const err = e;
|
|
565
|
-
return {
|
|
566
|
-
content: [
|
|
567
|
-
{
|
|
568
|
-
type: "text",
|
|
569
|
-
text: JSON.stringify({
|
|
570
|
-
error: err.message ?? String(e),
|
|
571
|
-
code: err.code ?? "READ_FAILED",
|
|
572
|
-
}),
|
|
573
|
-
},
|
|
574
|
-
],
|
|
575
|
-
details: {
|
|
576
|
-
error: err.message ?? String(e),
|
|
577
|
-
code: err.code ?? "READ_FAILED",
|
|
578
|
-
},
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
|
-
});
|
|
583
|
-
const safeGrepTool = defineTool({
|
|
584
|
-
name: "safe-grep",
|
|
585
|
-
label: "safe-grep",
|
|
586
|
-
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.",
|
|
587
|
-
promptSnippet: "safe-grep: search repo files (safe; secrets scrubbed)",
|
|
588
|
-
parameters: Type.Object({
|
|
589
|
-
pattern: Type.String({ description: "regex source (no shell)" }),
|
|
590
|
-
caseInsensitive: Type.Optional(Type.Boolean()),
|
|
591
|
-
glob: Type.Optional(Type.String({ description: "optional glob filter e.g. *.ts" })),
|
|
592
|
-
}, { additionalProperties: false }),
|
|
593
|
-
async execute(_toolCallId, params) {
|
|
594
|
-
try {
|
|
595
|
-
const result = await safeGrep(repoRoot, String(params.pattern ?? ""), {
|
|
596
|
-
caseInsensitive: Boolean(params.caseInsensitive),
|
|
597
|
-
glob: params.glob ? String(params.glob) : undefined,
|
|
598
|
-
});
|
|
599
|
-
return {
|
|
600
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
601
|
-
details: result,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
catch (e) {
|
|
605
|
-
const err = e;
|
|
606
|
-
return {
|
|
607
|
-
content: [
|
|
608
|
-
{
|
|
609
|
-
type: "text",
|
|
610
|
-
text: JSON.stringify({
|
|
611
|
-
error: err.message ?? String(e),
|
|
612
|
-
code: err.code ?? "GREP_FAILED",
|
|
613
|
-
}),
|
|
614
|
-
},
|
|
615
|
-
],
|
|
616
|
-
details: {
|
|
617
|
-
error: err.message ?? String(e),
|
|
618
|
-
code: err.code ?? "GREP_FAILED",
|
|
619
|
-
},
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
const gitStatusTool = defineTool({
|
|
625
|
-
name: "git-status",
|
|
626
|
-
label: "git-status",
|
|
627
|
-
description: "Typed `git status --porcelain` (fixed argv, no shell). Returns scrubbed working-tree status.",
|
|
628
|
-
promptSnippet: "git-status: working tree status (read-only)",
|
|
629
|
-
parameters: Type.Object({}, { additionalProperties: false }),
|
|
630
|
-
async execute() {
|
|
631
|
-
const r = safeGit(["status", "--porcelain"]);
|
|
632
|
-
if (!r.ok) {
|
|
633
|
-
return {
|
|
634
|
-
content: [
|
|
635
|
-
{
|
|
636
|
-
type: "text",
|
|
637
|
-
text: JSON.stringify({ error: r.message, code: r.code }),
|
|
638
|
-
},
|
|
639
|
-
],
|
|
640
|
-
details: { error: r.message, code: r.code },
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
const { scrubbed, redactions } = scrubSecrets(r.stdout);
|
|
644
|
-
const payload = { status: scrubbed, redactions };
|
|
645
|
-
return {
|
|
646
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
647
|
-
details: payload,
|
|
648
|
-
};
|
|
649
|
-
},
|
|
650
|
-
});
|
|
651
|
-
const gitDiffTool = defineTool({
|
|
652
|
-
name: "git-diff",
|
|
653
|
-
label: "git-diff",
|
|
654
|
-
description: "Typed `git diff` (fixed argv: --stat or a single -- path; no shell). Returns scrubbed diff. path must be repo-relative and not sensitive.",
|
|
655
|
-
promptSnippet: "git-diff: diff (read-only, scrubbed)",
|
|
656
|
-
parameters: Type.Object({
|
|
657
|
-
path: Type.Optional(Type.String({
|
|
658
|
-
description: "optional repo-relative path to diff (default: whole tree --stat)",
|
|
659
|
-
})),
|
|
660
|
-
}, { additionalProperties: false }),
|
|
661
|
-
async execute(_toolCallId, params) {
|
|
662
|
-
let rawPath = params.path ? String(params.path) : undefined;
|
|
663
|
-
// If a path is supplied, validate it via the read-path guard so git diff
|
|
664
|
-
// cannot be aimed at sensitive files (.env* / auth.json / sessions).
|
|
665
|
-
if (rawPath) {
|
|
666
|
-
const { guardReadPath } = await import("./explore-tools.js");
|
|
667
|
-
const g = await guardReadPath(repoRoot, rawPath);
|
|
668
|
-
if (!g.ok) {
|
|
669
|
-
return {
|
|
670
|
-
content: [
|
|
671
|
-
{
|
|
672
|
-
type: "text",
|
|
673
|
-
text: JSON.stringify({ error: g.message, code: g.code }),
|
|
674
|
-
},
|
|
675
|
-
],
|
|
676
|
-
details: { error: g.message, code: g.code },
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
rawPath = g.repoRelative;
|
|
680
|
-
}
|
|
681
|
-
const args = rawPath ? ["diff", "--", rawPath] : ["diff", "--stat"];
|
|
682
|
-
const r = safeGit(args);
|
|
683
|
-
if (!r.ok) {
|
|
684
|
-
return {
|
|
685
|
-
content: [
|
|
686
|
-
{
|
|
687
|
-
type: "text",
|
|
688
|
-
text: JSON.stringify({ error: r.message, code: r.code }),
|
|
689
|
-
},
|
|
690
|
-
],
|
|
691
|
-
details: { error: r.message, code: r.code },
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
const { scrubbed, redactions } = scrubSecrets(r.stdout);
|
|
695
|
-
const payload = { diff: scrubbed, redactions };
|
|
696
|
-
return {
|
|
697
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
698
|
-
details: payload,
|
|
699
|
-
};
|
|
700
|
-
},
|
|
701
|
-
});
|
|
702
|
-
return [safeReadTool, safeGrepTool, gitStatusTool, gitDiffTool];
|
|
703
|
-
}
|
|
704
|
-
export const THINKING_LEVELS = [
|
|
705
|
-
"off",
|
|
706
|
-
"minimal",
|
|
707
|
-
"low",
|
|
708
|
-
"medium",
|
|
709
|
-
"high",
|
|
710
|
-
"xhigh",
|
|
711
|
-
"max",
|
|
712
|
-
];
|
|
713
|
-
/**
|
|
714
|
-
* Structured critical-failure error for snapshot projection (RF-02). Model
|
|
715
|
-
* enumeration or system prompt projection failures throw this so consuming
|
|
716
|
-
* routes can return 503 PI_RUNTIME_UNAVAILABLE instead of a fake-empty 200.
|
|
717
|
-
* Partial ResourceLoader / package-inventory failures are NOT critical — they
|
|
718
|
-
* keep the 200 snapshot and surface as structured diagnostics instead.
|
|
719
|
-
*/
|
|
720
|
-
export class RuntimeSnapshotUnavailableError extends Error {
|
|
721
|
-
code = "PI_RUNTIME_UNAVAILABLE";
|
|
722
|
-
constructor(message) {
|
|
723
|
-
super(message);
|
|
724
|
-
this.name = "RuntimeSnapshotUnavailableError";
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Browser-safe projection of a live runtime snapshot (design §2.2 / RF-03).
|
|
729
|
-
*
|
|
730
|
-
* Every browser-visible string runs the shared secret-scrub chain
|
|
731
|
-
* (scrubSecrets + ghp_/Bearer redaction via redactRuntimeText) and a fixed
|
|
732
|
-
* per-field length limit: 200 (names/spec/source/origin), 400 (paths/errors/
|
|
733
|
-
* diagnostic messages), 600 (skill descriptions), 80 (package versions);
|
|
734
|
-
* truncation uses the fixed `…[truncated N chars …]` marker. The server-only
|
|
735
|
-
* `raw` prompt and the RAW-based `sha256`/`characters` pass through untouched,
|
|
736
|
-
* and real empty lists stay empty.
|
|
737
|
-
*
|
|
738
|
-
* Pure + deterministic and exported so the route harness reuses the exact
|
|
739
|
-
* production implementation (same code path as getRuntimeSnapshot).
|
|
740
|
-
*/
|
|
741
|
-
export function projectRuntimeSnapshot(input) {
|
|
742
|
-
const field = (value, max) => value === undefined ? undefined : redactRuntimeText(value, max);
|
|
743
|
-
const projectSkill = (skill) => ({
|
|
744
|
-
name: field(skill.name, 200) ?? "",
|
|
745
|
-
description: field(skill.description, 600) ?? "",
|
|
746
|
-
path: field(skill.path, 400) ?? "",
|
|
747
|
-
...(skill.baseDir ? { baseDir: field(skill.baseDir, 400) } : {}),
|
|
748
|
-
scope: skill.scope,
|
|
749
|
-
source: field(skill.source, 200) ?? "",
|
|
750
|
-
resourceId: field(skill.resourceId, 200) ?? "",
|
|
751
|
-
autoInvocationEnabled: skill.autoInvocationEnabled,
|
|
752
|
-
diagnostics: skill.diagnostics.map((diag) => field(diag, 400) ?? ""),
|
|
753
|
-
});
|
|
754
|
-
const projectExtension = (ext) => ({
|
|
755
|
-
name: field(ext.name, 200) ?? "",
|
|
756
|
-
path: field(ext.path, 400) ?? "",
|
|
757
|
-
...(ext.resolvedPath
|
|
758
|
-
? { resolvedPath: field(ext.resolvedPath, 400) }
|
|
759
|
-
: {}),
|
|
760
|
-
source: field(ext.source, 200) ?? "",
|
|
761
|
-
scope: ext.scope,
|
|
762
|
-
...(ext.origin ? { origin: field(ext.origin, 200) } : {}),
|
|
763
|
-
enabled: ext.enabled,
|
|
764
|
-
...(ext.error ? { error: field(ext.error, 400) } : {}),
|
|
765
|
-
});
|
|
766
|
-
const projectPackage = (pkg) => ({
|
|
767
|
-
spec: field(pkg.spec, 200) ?? "",
|
|
768
|
-
scope: pkg.scope,
|
|
769
|
-
enabled: pkg.enabled,
|
|
770
|
-
...(pkg.version ? { version: field(pkg.version, 80) } : {}),
|
|
771
|
-
...(pkg.configuredVersion
|
|
772
|
-
? { configuredVersion: field(pkg.configuredVersion, 80) }
|
|
773
|
-
: {}),
|
|
774
|
-
...(pkg.packageName ? { packageName: field(pkg.packageName, 200) } : {}),
|
|
775
|
-
...(pkg.path ? { path: field(pkg.path, 400) } : {}),
|
|
776
|
-
...(pkg.installedPath ? { installedPath: field(pkg.installedPath, 400) } : {}),
|
|
777
|
-
...(pkg.cwd ? { cwd: field(pkg.cwd, 400) } : {}),
|
|
778
|
-
resolved: pkg.resolved.map((resource) => ({
|
|
779
|
-
type: resource.type,
|
|
780
|
-
name: field(resource.name, 200) ?? "",
|
|
781
|
-
path: field(resource.path, 400) ?? "",
|
|
782
|
-
source: field(resource.source, 200) ?? "",
|
|
783
|
-
scope: resource.scope,
|
|
784
|
-
})),
|
|
785
|
-
diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
|
|
786
|
-
});
|
|
787
|
-
const projectMcpTool = (tool) => ({
|
|
788
|
-
name: field(tool.name, 200) ?? "",
|
|
789
|
-
description: field(tool.description, 600) ?? "",
|
|
790
|
-
...(tool.server ? { server: field(tool.server, 200) } : {}),
|
|
791
|
-
source: field(tool.source, 200) ?? "",
|
|
792
|
-
scope: tool.scope,
|
|
793
|
-
...(tool.path ? { path: field(tool.path, 400) } : {}),
|
|
794
|
-
active: tool.active,
|
|
795
|
-
});
|
|
796
|
-
const projectMcp = (mcp) => ({
|
|
797
|
-
servers: mcp.servers.map((server) => ({
|
|
798
|
-
name: field(server.name, 200) ?? "",
|
|
799
|
-
scope: server.scope,
|
|
800
|
-
status: server.status,
|
|
801
|
-
enabled: server.enabled,
|
|
802
|
-
...(server.transport ? { transport: field(server.transport, 80) } : {}),
|
|
803
|
-
...(server.command ? { command: field(server.command, 400) } : {}),
|
|
804
|
-
...(server.url ? { url: field(server.url, 400) } : {}),
|
|
805
|
-
...(server.configPath ? { configPath: field(server.configPath, 400) } : {}),
|
|
806
|
-
tools: server.tools.map(projectMcpTool),
|
|
807
|
-
})),
|
|
808
|
-
unassignedTools: mcp.unassignedTools.map(projectMcpTool),
|
|
809
|
-
});
|
|
810
|
-
const projectDiagnostic = (diag) => ({
|
|
811
|
-
type: diag.type,
|
|
812
|
-
...(diag.code ? { code: diag.code } : {}),
|
|
813
|
-
message: field(diag.message, 400) ?? "",
|
|
814
|
-
...(diag.path ? { path: field(diag.path, 400) } : {}),
|
|
815
|
-
...(diag.retryable !== undefined
|
|
816
|
-
? { retryable: diag.retryable }
|
|
817
|
-
: {}),
|
|
818
|
-
});
|
|
819
|
-
return {
|
|
820
|
-
sessionId: input.sessionId,
|
|
821
|
-
revision: input.revision,
|
|
822
|
-
loadedAt: input.loadedAt,
|
|
823
|
-
selection: {
|
|
824
|
-
...(input.selection.model
|
|
825
|
-
? {
|
|
826
|
-
model: {
|
|
827
|
-
provider: input.selection.model.provider,
|
|
828
|
-
id: input.selection.model.id,
|
|
829
|
-
...(input.selection.model.name
|
|
830
|
-
? { name: field(input.selection.model.name, 200) }
|
|
831
|
-
: {}),
|
|
832
|
-
},
|
|
833
|
-
}
|
|
834
|
-
: {}),
|
|
835
|
-
thinkingSelection: input.selection.thinkingSelection,
|
|
836
|
-
...(input.selection.effectiveThinkingLevel
|
|
837
|
-
? { effectiveThinkingLevel: input.selection.effectiveThinkingLevel }
|
|
838
|
-
: {}),
|
|
839
|
-
},
|
|
840
|
-
models: input.models.map((model) => ({
|
|
841
|
-
provider: model.provider,
|
|
842
|
-
id: model.id,
|
|
843
|
-
...(model.name ? { name: field(model.name, 200) } : {}),
|
|
844
|
-
...(model.hasCredentials !== undefined
|
|
845
|
-
? { hasCredentials: model.hasCredentials }
|
|
846
|
-
: {}),
|
|
847
|
-
})),
|
|
848
|
-
thinkingLevels: input.thinkingLevels.map((option) => ({ ...option })),
|
|
849
|
-
systemPrompt: {
|
|
850
|
-
...(input.systemPrompt.raw !== undefined
|
|
851
|
-
? { raw: input.systemPrompt.raw }
|
|
852
|
-
: {}),
|
|
853
|
-
text: redactRuntimeText(input.systemPrompt.text, RUNTIME_CONTEXT_TEXT_MAX),
|
|
854
|
-
sha256: input.systemPrompt.sha256,
|
|
855
|
-
characters: input.systemPrompt.characters,
|
|
856
|
-
},
|
|
857
|
-
activeTools: [...input.activeTools],
|
|
858
|
-
contextFiles: input.contextFiles.map((file) => ({
|
|
859
|
-
path: field(file.path, 400) ?? "",
|
|
860
|
-
characters: file.characters,
|
|
861
|
-
})),
|
|
862
|
-
skills: input.skills.map(projectSkill),
|
|
863
|
-
extensions: input.extensions.map(projectExtension),
|
|
864
|
-
packages: input.packages.map(projectPackage),
|
|
865
|
-
mcp: projectMcp(input.mcp ?? { servers: [], unassignedTools: [] }),
|
|
866
|
-
diagnostics: input.diagnostics.map(projectDiagnostic),
|
|
867
|
-
hasBash: input.hasBash,
|
|
868
|
-
};
|
|
869
|
-
}
|
|
288
|
+
export { THINKING_LEVELS, RuntimeSnapshotUnavailableError, projectRuntimeSnapshot, };
|
|
289
|
+
export { createDefaultPiSdkBindings } from "./pi-runtime/sdk-bindings.js";
|
|
870
290
|
export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
|
|
871
291
|
function readSessionManagerId(manager) {
|
|
872
292
|
if (!manager || typeof manager !== "object")
|
|
@@ -1418,31 +838,33 @@ export class ConsolePiRuntime {
|
|
|
1418
838
|
this.sessionInit.set(sessionId, { status: "initializing", promise });
|
|
1419
839
|
return promise;
|
|
1420
840
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
841
|
+
/**
|
|
842
|
+
* Shared session preparation for create/reopen/fork: config capture, prompt
|
|
843
|
+
* composition, resource loading, model resolution, tool assembly, SDK session
|
|
844
|
+
* create, extension bind, tool pin, compaction override. Lifecycle maps stay
|
|
845
|
+
* owned by the caller.
|
|
846
|
+
*/
|
|
847
|
+
async prepareLiveSession(init) {
|
|
848
|
+
const aborted = () => Boolean(init.abortIfDisposed && this.disposedSessions.has(init.sessionId));
|
|
849
|
+
if (aborted())
|
|
850
|
+
return undefined;
|
|
1426
851
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
1427
852
|
// Capture before services parse models.json. A concurrent edit can therefore
|
|
1428
853
|
// cause at most one harmless extra refresh, never a missed next-turn refresh.
|
|
1429
854
|
const modelConfigFile = await captureModelConfigFile(agentDir);
|
|
1430
855
|
const appendSystemPrompt = composeChatSystemPromptParts({
|
|
1431
|
-
sessionMode,
|
|
856
|
+
sessionMode: init.sessionMode,
|
|
1432
857
|
workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
1433
858
|
systemPromptSuffix: init.systemPromptSuffix,
|
|
1434
859
|
});
|
|
1435
|
-
// Single services construction for formal materialization (+ optional
|
|
1436
|
-
// default-model resolve from the same modelRuntime). Repo-scoped skill
|
|
1437
|
-
// auto-invocation prefs inject skillsOverride without touching SKILL.md.
|
|
1438
860
|
await this.refreshSkillPrefsCache().catch(() => undefined);
|
|
1439
861
|
const { services } = await this.bindings.createServices({
|
|
1440
862
|
cwd: this.options.cwd,
|
|
1441
863
|
agentDir,
|
|
1442
|
-
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, sessionId),
|
|
864
|
+
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
|
|
1443
865
|
});
|
|
1444
|
-
if (
|
|
1445
|
-
return;
|
|
866
|
+
if (aborted())
|
|
867
|
+
return undefined;
|
|
1446
868
|
const modelRuntime = services.modelRuntime;
|
|
1447
869
|
let model = init.model;
|
|
1448
870
|
if (!model && init.resolveDefaultModel) {
|
|
@@ -1456,9 +878,9 @@ export class ConsolePiRuntime {
|
|
|
1456
878
|
}) ?? undefined)
|
|
1457
879
|
: undefined;
|
|
1458
880
|
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
1459
|
-
const customTools = await this.loadCustomTools(sessionId);
|
|
1460
|
-
if (
|
|
1461
|
-
return;
|
|
881
|
+
const customTools = await this.loadCustomTools(init.sessionId);
|
|
882
|
+
if (aborted())
|
|
883
|
+
return undefined;
|
|
1462
884
|
// pi-web parity: do NOT pass `tools`. A closed allowlist becomes
|
|
1463
885
|
// SDK allowedToolNames and filters extension MCP tools out of the
|
|
1464
886
|
// registry before Gate 2 can re-add them. Operator/safe customTools
|
|
@@ -1472,35 +894,59 @@ export class ConsolePiRuntime {
|
|
|
1472
894
|
...(customTools.length > 0 ? { customTools } : {}),
|
|
1473
895
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
1474
896
|
});
|
|
1475
|
-
if (
|
|
897
|
+
if (aborted()) {
|
|
1476
898
|
try {
|
|
1477
899
|
session.dispose();
|
|
1478
900
|
}
|
|
1479
901
|
catch {
|
|
1480
902
|
// ignore late dispose cleanup
|
|
1481
903
|
}
|
|
1482
|
-
return;
|
|
904
|
+
return undefined;
|
|
1483
905
|
}
|
|
1484
906
|
await bindSessionExtensions(session);
|
|
1485
|
-
pinOperatorChatTools(session, sessionMode);
|
|
907
|
+
pinOperatorChatTools(session, init.sessionMode);
|
|
908
|
+
this.applySessionCompactionOverride(init.sessionId, services, session);
|
|
909
|
+
return {
|
|
910
|
+
session,
|
|
911
|
+
modelRuntime,
|
|
912
|
+
modelConfigFile,
|
|
913
|
+
model,
|
|
914
|
+
services: {
|
|
915
|
+
resourceLoader: services.resourceLoader,
|
|
916
|
+
settingsManager: services.settingsManager,
|
|
917
|
+
},
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
async materializeSession(sessionId, init) {
|
|
921
|
+
if (this.disposedSessions.has(sessionId))
|
|
922
|
+
return;
|
|
923
|
+
await this.syncAimaxAuthToken();
|
|
924
|
+
const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
|
|
925
|
+
const prepared = await this.prepareLiveSession({
|
|
926
|
+
sessionId,
|
|
927
|
+
sessionManager: init.sessionManager,
|
|
928
|
+
model: init.model,
|
|
929
|
+
systemPromptSuffix: init.systemPromptSuffix,
|
|
930
|
+
sessionMode,
|
|
931
|
+
resolveDefaultModel: init.resolveDefaultModel,
|
|
932
|
+
abortIfDisposed: true,
|
|
933
|
+
});
|
|
934
|
+
if (!prepared)
|
|
935
|
+
return;
|
|
1486
936
|
// Register then re-check dispose race: a concurrent dispose must win.
|
|
1487
|
-
this.sessions.set(sessionId, session);
|
|
937
|
+
this.sessions.set(sessionId, prepared.session);
|
|
1488
938
|
this.sessionManagers.set(sessionId, init.sessionManager);
|
|
1489
|
-
this.modelRuntimes.set(sessionId, modelRuntime);
|
|
1490
|
-
this.modelConfigFiles.set(sessionId, modelConfigFile);
|
|
1491
|
-
this.sessionModels.set(sessionId, model);
|
|
1492
|
-
this.serviceScopes.set(sessionId,
|
|
1493
|
-
resourceLoader: services.resourceLoader,
|
|
1494
|
-
settingsManager: services.settingsManager,
|
|
1495
|
-
});
|
|
1496
|
-
this.applySessionCompactionOverride(sessionId, services, session);
|
|
939
|
+
this.modelRuntimes.set(sessionId, prepared.modelRuntime);
|
|
940
|
+
this.modelConfigFiles.set(sessionId, prepared.modelConfigFile);
|
|
941
|
+
this.sessionModels.set(sessionId, prepared.model);
|
|
942
|
+
this.serviceScopes.set(sessionId, prepared.services);
|
|
1497
943
|
this.revisions.set(sessionId, 1);
|
|
1498
944
|
if (!this.thinkingSelections.has(sessionId)) {
|
|
1499
945
|
this.thinkingSelections.set(sessionId, "auto");
|
|
1500
946
|
}
|
|
1501
947
|
if (this.disposedSessions.has(sessionId)) {
|
|
1502
948
|
try {
|
|
1503
|
-
session.dispose();
|
|
949
|
+
prepared.session.dispose();
|
|
1504
950
|
}
|
|
1505
951
|
catch {
|
|
1506
952
|
// ignore
|
|
@@ -1967,91 +1413,14 @@ export class ConsolePiRuntime {
|
|
|
1967
1413
|
* Never returns prompt/skill file bodies.
|
|
1968
1414
|
*/
|
|
1969
1415
|
async listSlashCommands(sessionId) {
|
|
1970
|
-
const scrub = (value) => {
|
|
1971
|
-
const raw = String(value ?? "");
|
|
1972
|
-
return raw
|
|
1973
|
-
.replace(/\/Users\/[^\s]+/gi, "~")
|
|
1974
|
-
.replace(/[A-Za-z]:\\Users\\[^\s]+/gi, "~")
|
|
1975
|
-
.slice(0, 240);
|
|
1976
|
-
};
|
|
1977
1416
|
// Fail soft at the HTTP layer: bubble readiness errors so routes can attach
|
|
1978
1417
|
// a compact warning while the UI keeps local Operator/Pi Web commands.
|
|
1979
1418
|
await this.ensureSessionReady(sessionId);
|
|
1980
1419
|
const session = this.sessions.get(sessionId);
|
|
1981
1420
|
if (!session)
|
|
1982
1421
|
return { commands: [] };
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
const SOURCE_RANK = {
|
|
1986
|
-
extension: 0,
|
|
1987
|
-
skill: 1,
|
|
1988
|
-
prompt: 2,
|
|
1989
|
-
};
|
|
1990
|
-
const byKey = new Map();
|
|
1991
|
-
const sourceWarnings = [];
|
|
1992
|
-
const push = (command, label, description, source) => {
|
|
1993
|
-
const name = command.startsWith("/") ? command : `/${command}`;
|
|
1994
|
-
const key = name.toLowerCase();
|
|
1995
|
-
if (!key || key === "/")
|
|
1996
|
-
return;
|
|
1997
|
-
const next = {
|
|
1998
|
-
command: name,
|
|
1999
|
-
label: (label || name).slice(0, 80),
|
|
2000
|
-
description: scrub(description),
|
|
2001
|
-
source,
|
|
2002
|
-
};
|
|
2003
|
-
const existing = byKey.get(key);
|
|
2004
|
-
if (!existing || SOURCE_RANK[source] < SOURCE_RANK[existing.source]) {
|
|
2005
|
-
byKey.set(key, next);
|
|
2006
|
-
}
|
|
2007
|
-
};
|
|
2008
|
-
try {
|
|
2009
|
-
const cmds = session.extensionRunner?.getRegisteredCommands?.() ?? [];
|
|
2010
|
-
for (const cmd of cmds) {
|
|
2011
|
-
const name = cmd.invocationName || cmd.name;
|
|
2012
|
-
if (!name)
|
|
2013
|
-
continue;
|
|
2014
|
-
push(String(name), String(cmd.name || name), String(cmd.description || ""), "extension");
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
catch (error) {
|
|
2018
|
-
// Source isolation: keep other sources; surface compact warning (AC-3).
|
|
2019
|
-
sourceWarnings.push(`extension: ${scrub(error instanceof Error ? error.message : String(error))}`);
|
|
2020
|
-
}
|
|
2021
|
-
try {
|
|
2022
|
-
// Enumerate prompts before skills on purpose: priority must still let skill win.
|
|
2023
|
-
for (const tpl of session.promptTemplates ?? []) {
|
|
2024
|
-
if (!tpl?.name)
|
|
2025
|
-
continue;
|
|
2026
|
-
push(String(tpl.name), String(tpl.name), String(tpl.description || ""), "prompt");
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
catch (error) {
|
|
2030
|
-
sourceWarnings.push(`prompt: ${scrub(error instanceof Error ? error.message : String(error))}`);
|
|
2031
|
-
}
|
|
2032
|
-
try {
|
|
2033
|
-
const services = this.serviceScopes.get(sessionId);
|
|
2034
|
-
const resourceLoader = services?.resourceLoader;
|
|
2035
|
-
const skills = resourceLoader?.getSkills?.()?.skills ?? [];
|
|
2036
|
-
for (const skill of skills) {
|
|
2037
|
-
if (!skill?.name)
|
|
2038
|
-
continue;
|
|
2039
|
-
const command = skill.name.startsWith("skill:")
|
|
2040
|
-
? `/${skill.name}`
|
|
2041
|
-
: `/skill:${skill.name}`;
|
|
2042
|
-
push(command, skill.name, String(skill.description || ""), "skill");
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
catch (error) {
|
|
2046
|
-
sourceWarnings.push(`skill: ${scrub(error instanceof Error ? error.message : String(error))}`);
|
|
2047
|
-
}
|
|
2048
|
-
const warning = sourceWarnings.length > 0
|
|
2049
|
-
? sourceWarnings.join("; ").slice(0, 160)
|
|
2050
|
-
: undefined;
|
|
2051
|
-
return {
|
|
2052
|
-
commands: [...byKey.values()],
|
|
2053
|
-
...(warning ? { warning } : {}),
|
|
2054
|
-
};
|
|
1422
|
+
const resourceLoader = this.serviceScopes.get(sessionId)?.resourceLoader;
|
|
1423
|
+
return collectSlashCommands({ session, resourceLoader });
|
|
2055
1424
|
}
|
|
2056
1425
|
async getRuntimeSnapshot(sessionId, options) {
|
|
2057
1426
|
try {
|
|
@@ -2095,9 +1464,6 @@ export class ConsolePiRuntime {
|
|
|
2095
1464
|
throw new RuntimeSnapshotUnavailableError(`model enumeration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2096
1465
|
}
|
|
2097
1466
|
}
|
|
2098
|
-
// Thinking options: auto is always offered; explicit levels only when the
|
|
2099
|
-
// runtime session supports setThinkingLevel (xhigh/max ship with the SDK
|
|
2100
|
-
// and therefore surface only when this runtime provides them).
|
|
2101
1467
|
const supportedLevels = typeof session.setThinkingLevel === "function"
|
|
2102
1468
|
? [...THINKING_LEVELS]
|
|
2103
1469
|
: [];
|
|
@@ -2105,220 +1471,25 @@ export class ConsolePiRuntime {
|
|
|
2105
1471
|
{ value: "auto" },
|
|
2106
1472
|
...supportedLevels.map((value) => ({ value })),
|
|
2107
1473
|
];
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
// Each subface is isolated (RF-02): a throwing subface keeps the other
|
|
2111
|
-
// inventories AND surfaces a structured retryable diagnostic instead of
|
|
2112
|
-
// silently dropping the whole resource projection.
|
|
2113
|
-
const contextFiles = [];
|
|
2114
|
-
const skills = [];
|
|
2115
|
-
const extensions = [];
|
|
2116
|
-
const diagnostics = [];
|
|
2117
|
-
const resolvedResources = [];
|
|
2118
|
-
const resourceFailure = (subface, error) => {
|
|
2119
|
-
diagnostics.push({
|
|
2120
|
-
type: "error",
|
|
2121
|
-
code: "RESOURCE_LOAD_FAILED",
|
|
2122
|
-
message: `${subface} inventory failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2123
|
-
retryable: true,
|
|
2124
|
-
});
|
|
2125
|
-
};
|
|
1474
|
+
let skillPrefs = { schemaVersion: 1, skills: {} };
|
|
1475
|
+
let skillPrefsError;
|
|
2126
1476
|
if (resourceLoader) {
|
|
2127
|
-
try {
|
|
2128
|
-
const agentsFiles = resourceLoader.getAgentsFiles?.();
|
|
2129
|
-
for (const file of agentsFiles?.agentsFiles ?? []) {
|
|
2130
|
-
if (!file.path)
|
|
2131
|
-
continue;
|
|
2132
|
-
contextFiles.push({
|
|
2133
|
-
path: file.path,
|
|
2134
|
-
characters: file.content?.length ?? 0,
|
|
2135
|
-
});
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
catch (error) {
|
|
2139
|
-
resourceFailure("context files", error);
|
|
2140
|
-
}
|
|
2141
|
-
// Preferences are the source of truth for UI auto-invocation state;
|
|
2142
|
-
// loader disableModelInvocation may also reflect frontmatter. Prefer
|
|
2143
|
-
// explicit preference (default on) when projecting autoInvocationEnabled.
|
|
2144
|
-
let skillPrefs = { schemaVersion: 1, skills: {} };
|
|
2145
1477
|
try {
|
|
2146
1478
|
skillPrefs = await this.loadSkillAutoInvocationPreferences();
|
|
2147
1479
|
}
|
|
2148
1480
|
catch (error) {
|
|
2149
|
-
|
|
2150
|
-
type: "error",
|
|
2151
|
-
code: "PI_RESOURCE_PREFERENCES_MALFORMED",
|
|
2152
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2153
|
-
retryable: true,
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
try {
|
|
2157
|
-
const skillsResult = resourceLoader.getSkills?.();
|
|
2158
|
-
for (const skill of skillsResult?.skills ?? []) {
|
|
2159
|
-
const resourceId = skillResourceId({
|
|
2160
|
-
name: skill.name,
|
|
2161
|
-
filePath: skill.filePath,
|
|
2162
|
-
});
|
|
2163
|
-
// Preference off always means autoInvocationEnabled=false even if
|
|
2164
|
-
// loader already applied disableModelInvocation. Frontmatter-only
|
|
2165
|
-
// disable (no preference) also surfaces as autoInvocationEnabled=false.
|
|
2166
|
-
const prefEnabled = isAutoInvocationEnabled(skillPrefs, resourceId);
|
|
2167
|
-
const autoInvocationEnabled = prefEnabled && skill.disableModelInvocation !== true;
|
|
2168
|
-
skills.push({
|
|
2169
|
-
name: skill.name,
|
|
2170
|
-
description: skill.description ?? "",
|
|
2171
|
-
path: skill.filePath,
|
|
2172
|
-
...(skill.baseDir ? { baseDir: skill.baseDir } : {}),
|
|
2173
|
-
scope: skill.sourceInfo?.scope ?? "unknown",
|
|
2174
|
-
source: skill.sourceInfo?.source ?? "",
|
|
2175
|
-
resourceId,
|
|
2176
|
-
autoInvocationEnabled,
|
|
2177
|
-
diagnostics: [],
|
|
2178
|
-
});
|
|
2179
|
-
resolvedResources.push({
|
|
2180
|
-
type: "skill",
|
|
2181
|
-
name: skill.name,
|
|
2182
|
-
path: skill.filePath,
|
|
2183
|
-
source: skill.sourceInfo?.source ?? "",
|
|
2184
|
-
scope: skill.sourceInfo?.scope ?? "unknown",
|
|
2185
|
-
});
|
|
2186
|
-
}
|
|
2187
|
-
for (const diag of skillsResult?.diagnostics ?? []) {
|
|
2188
|
-
if (!diag?.message)
|
|
2189
|
-
continue;
|
|
2190
|
-
diagnostics.push({
|
|
2191
|
-
type: diag.type ?? "warning",
|
|
2192
|
-
message: diag.message,
|
|
2193
|
-
...(diag.path ? { path: diag.path } : {}),
|
|
2194
|
-
});
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
catch (error) {
|
|
2198
|
-
resourceFailure("skills", error);
|
|
2199
|
-
}
|
|
2200
|
-
try {
|
|
2201
|
-
const extResult = resourceLoader.getExtensions?.();
|
|
2202
|
-
for (const ext of extResult?.extensions ?? []) {
|
|
2203
|
-
const path = ext.resolvedPath ?? ext.path ?? "";
|
|
2204
|
-
extensions.push({
|
|
2205
|
-
name: ext.sourceInfo?.source ?? ext.path ?? "extension",
|
|
2206
|
-
path,
|
|
2207
|
-
...(ext.resolvedPath ? { resolvedPath: ext.resolvedPath } : {}),
|
|
2208
|
-
source: ext.sourceInfo?.source ?? "",
|
|
2209
|
-
scope: ext.sourceInfo?.scope ?? "unknown",
|
|
2210
|
-
...(ext.sourceInfo?.origin ? { origin: ext.sourceInfo.origin } : {}),
|
|
2211
|
-
enabled: true,
|
|
2212
|
-
});
|
|
2213
|
-
resolvedResources.push({
|
|
2214
|
-
type: "extension",
|
|
2215
|
-
name: ext.sourceInfo?.source ?? ext.path ?? "extension",
|
|
2216
|
-
path,
|
|
2217
|
-
source: ext.sourceInfo?.source ?? "",
|
|
2218
|
-
scope: ext.sourceInfo?.scope ?? "unknown",
|
|
2219
|
-
});
|
|
2220
|
-
}
|
|
2221
|
-
for (const err of extResult?.errors ?? []) {
|
|
2222
|
-
if (!err?.error)
|
|
2223
|
-
continue;
|
|
2224
|
-
diagnostics.push({
|
|
2225
|
-
type: "error",
|
|
2226
|
-
message: err.error,
|
|
2227
|
-
...(err.path ? { path: err.path } : {}),
|
|
2228
|
-
});
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
catch (error) {
|
|
2232
|
-
resourceFailure("extensions", error);
|
|
2233
|
-
}
|
|
2234
|
-
try {
|
|
2235
|
-
const promptsResult = resourceLoader.getPrompts?.();
|
|
2236
|
-
for (const prompt of promptsResult?.prompts ?? []) {
|
|
2237
|
-
resolvedResources.push({
|
|
2238
|
-
type: "prompt",
|
|
2239
|
-
name: prompt.name,
|
|
2240
|
-
path: prompt.filePath,
|
|
2241
|
-
source: prompt.sourceInfo?.source ?? "",
|
|
2242
|
-
scope: prompt.sourceInfo?.scope ?? "unknown",
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
|
-
for (const diag of promptsResult?.diagnostics ?? []) {
|
|
2246
|
-
if (!diag?.message)
|
|
2247
|
-
continue;
|
|
2248
|
-
diagnostics.push({
|
|
2249
|
-
type: diag.type ?? "warning",
|
|
2250
|
-
message: diag.message,
|
|
2251
|
-
...(diag.path ? { path: diag.path } : {}),
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
catch (error) {
|
|
2256
|
-
resourceFailure("prompts", error);
|
|
2257
|
-
}
|
|
2258
|
-
try {
|
|
2259
|
-
const themesResult = resourceLoader.getThemes?.();
|
|
2260
|
-
for (const theme of themesResult?.themes ?? []) {
|
|
2261
|
-
if (!theme.path)
|
|
2262
|
-
continue;
|
|
2263
|
-
resolvedResources.push({
|
|
2264
|
-
type: "theme",
|
|
2265
|
-
name: theme.name ?? theme.path,
|
|
2266
|
-
path: theme.path,
|
|
2267
|
-
source: "",
|
|
2268
|
-
scope: "unknown",
|
|
2269
|
-
});
|
|
2270
|
-
}
|
|
2271
|
-
for (const diag of themesResult?.diagnostics ?? []) {
|
|
2272
|
-
if (!diag?.message)
|
|
2273
|
-
continue;
|
|
2274
|
-
diagnostics.push({
|
|
2275
|
-
type: diag.type ?? "warning",
|
|
2276
|
-
message: diag.message,
|
|
2277
|
-
...(diag.path ? { path: diag.path } : {}),
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
1481
|
+
skillPrefsError = error;
|
|
2280
1482
|
}
|
|
2281
|
-
catch (error) {
|
|
2282
|
-
resourceFailure("themes", error);
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
// Packages: Pi settings specs + resolved resource metadata. Fields the SDK
|
|
2286
|
-
// does not expose stay absent (real missing, never fabricated). A
|
|
2287
|
-
// readPackageInventory failure is a PARTIAL failure (RF-02): the snapshot
|
|
2288
|
-
// stays 200 with a structured retryable diagnostic — never a silent
|
|
2289
|
-
// fake-empty package list.
|
|
2290
|
-
const { readPackageInventory } = await import("./pi-console-config.js");
|
|
2291
|
-
let packages = [];
|
|
2292
|
-
try {
|
|
2293
|
-
packages = (await readPackageInventory(await safeGetAgentDir(this.bindings), this.options.cwd, resolvedResources));
|
|
2294
|
-
}
|
|
2295
|
-
catch (error) {
|
|
2296
|
-
packages = [];
|
|
2297
|
-
diagnostics.push({
|
|
2298
|
-
type: "error",
|
|
2299
|
-
code: "PACKAGE_INVENTORY_FAILED",
|
|
2300
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2301
|
-
retryable: true,
|
|
2302
|
-
});
|
|
2303
|
-
}
|
|
2304
|
-
const { readMcpInventory } = await import("./mcp-inventory.js");
|
|
2305
|
-
let mcp = { servers: [], unassignedTools: [] };
|
|
2306
|
-
try {
|
|
2307
|
-
mcp = await readMcpInventory({
|
|
2308
|
-
agentDir: await safeGetAgentDir(this.bindings),
|
|
2309
|
-
cwd: this.options.cwd,
|
|
2310
|
-
tools: session.getAllTools?.() ?? [],
|
|
2311
|
-
activeToolNames: session.getActiveToolNames(),
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
catch (error) {
|
|
2315
|
-
diagnostics.push({
|
|
2316
|
-
type: "error",
|
|
2317
|
-
code: "MCP_INVENTORY_FAILED",
|
|
2318
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2319
|
-
retryable: true,
|
|
2320
|
-
});
|
|
2321
1483
|
}
|
|
1484
|
+
const inventories = await collectRuntimeInventories({
|
|
1485
|
+
resourceLoader,
|
|
1486
|
+
skillPrefs,
|
|
1487
|
+
skillPrefsError,
|
|
1488
|
+
agentDir: await safeGetAgentDir(this.bindings),
|
|
1489
|
+
cwd: this.options.cwd,
|
|
1490
|
+
tools: session.getAllTools?.() ?? [],
|
|
1491
|
+
activeToolNames: session.getActiveToolNames(),
|
|
1492
|
+
});
|
|
2322
1493
|
return projectRuntimeSnapshot({
|
|
2323
1494
|
sessionId,
|
|
2324
1495
|
revision: this.revisions.get(sessionId) ?? 0,
|
|
@@ -2343,19 +1514,17 @@ export class ConsolePiRuntime {
|
|
|
2343
1514
|
thinkingLevels,
|
|
2344
1515
|
systemPrompt: {
|
|
2345
1516
|
...(options?.includeRaw ? { raw: rawSystemPrompt } : {}),
|
|
2346
|
-
// The RAW prompt flows into the projection: text is scrubbed +
|
|
2347
|
-
// length-limited while sha256/characters stay RAW-based (design §2.2).
|
|
2348
1517
|
text: rawSystemPrompt,
|
|
2349
1518
|
sha256: createHash("sha256").update(rawSystemPrompt).digest("hex"),
|
|
2350
1519
|
characters: rawSystemPrompt.length,
|
|
2351
1520
|
},
|
|
2352
1521
|
activeTools: session.getActiveToolNames(),
|
|
2353
|
-
contextFiles,
|
|
2354
|
-
skills,
|
|
2355
|
-
extensions,
|
|
2356
|
-
packages,
|
|
2357
|
-
mcp,
|
|
2358
|
-
diagnostics,
|
|
1522
|
+
contextFiles: inventories.contextFiles,
|
|
1523
|
+
skills: inventories.skills,
|
|
1524
|
+
extensions: inventories.extensions,
|
|
1525
|
+
packages: inventories.packages,
|
|
1526
|
+
mcp: inventories.mcp,
|
|
1527
|
+
diagnostics: inventories.diagnostics,
|
|
2359
1528
|
hasBash: true,
|
|
2360
1529
|
});
|
|
2361
1530
|
}
|
|
@@ -2534,47 +1703,21 @@ export class ConsolePiRuntime {
|
|
|
2534
1703
|
*/
|
|
2535
1704
|
async buildSessionWithServices(init) {
|
|
2536
1705
|
const sessionMode = this.rememberSessionMode(init.sessionId, init.sessionMode ?? this.sessionModeOf(init.sessionId));
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2539
|
-
const appendSystemPrompt = composeChatSystemPromptParts({
|
|
2540
|
-
sessionMode,
|
|
2541
|
-
workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
2542
|
-
systemPromptSuffix: init.systemPromptSuffix,
|
|
2543
|
-
});
|
|
2544
|
-
await this.refreshSkillPrefsCache().catch(() => undefined);
|
|
2545
|
-
const { services } = await this.bindings.createServices({
|
|
2546
|
-
cwd: this.options.cwd,
|
|
2547
|
-
agentDir,
|
|
2548
|
-
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
|
|
2549
|
-
});
|
|
2550
|
-
const modelRuntime = services.modelRuntime;
|
|
2551
|
-
const resolvedModel = init.model
|
|
2552
|
-
? (this.bindings.resolveModel({
|
|
2553
|
-
modelRuntime,
|
|
2554
|
-
provider: init.model.provider,
|
|
2555
|
-
modelId: init.model.modelId,
|
|
2556
|
-
}) ?? undefined)
|
|
2557
|
-
: undefined;
|
|
2558
|
-
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
2559
|
-
const customTools = await this.loadCustomTools(init.sessionId);
|
|
2560
|
-
const { session } = await this.bindings.createSessionFromServices({
|
|
2561
|
-
services,
|
|
1706
|
+
const prepared = await this.prepareLiveSession({
|
|
1707
|
+
sessionId: init.sessionId,
|
|
2562
1708
|
sessionManager: init.sessionManager,
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
1709
|
+
model: init.model,
|
|
1710
|
+
systemPromptSuffix: init.systemPromptSuffix,
|
|
1711
|
+
sessionMode,
|
|
2566
1712
|
});
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
1713
|
+
if (!prepared) {
|
|
1714
|
+
throw new Error(`chat session preparation aborted: ${init.sessionId}`);
|
|
1715
|
+
}
|
|
2570
1716
|
return {
|
|
2571
|
-
session,
|
|
2572
|
-
modelRuntime,
|
|
2573
|
-
modelConfigFile,
|
|
2574
|
-
services:
|
|
2575
|
-
resourceLoader: services.resourceLoader,
|
|
2576
|
-
settingsManager: services.settingsManager,
|
|
2577
|
-
},
|
|
1717
|
+
session: prepared.session,
|
|
1718
|
+
modelRuntime: prepared.modelRuntime,
|
|
1719
|
+
modelConfigFile: prepared.modelConfigFile,
|
|
1720
|
+
services: prepared.services,
|
|
2578
1721
|
};
|
|
2579
1722
|
}
|
|
2580
1723
|
/** Re-open a previously created session (cross-request). */
|
|
@@ -3208,153 +2351,6 @@ async function waitForIdle(session, options) {
|
|
|
3208
2351
|
await new Promise((r) => setTimeout(r, 50));
|
|
3209
2352
|
}
|
|
3210
2353
|
}
|
|
3211
|
-
async function safeGetAgentDir(bindings) {
|
|
3212
|
-
try {
|
|
3213
|
-
return await bindings.getAgentDir();
|
|
3214
|
-
}
|
|
3215
|
-
catch {
|
|
3216
|
-
// Fall back to a Console-app-data scoped agent dir so we never accidentally
|
|
3217
|
-
// touch the user's real ~/.pi/agent config if getAgentDir throws.
|
|
3218
|
-
return path.join(process.cwd(), ".harness", "console", "pi-agent-dir");
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
/**
|
|
3222
|
-
* Cached SDK module loader. The pi-coding-agent package is ESM-only, so we
|
|
3223
|
-
* must use dynamic import() (not require). Cached to avoid repeated imports.
|
|
3224
|
-
*/
|
|
3225
|
-
let cachedSdk;
|
|
3226
|
-
async function loadSdk() {
|
|
3227
|
-
if (!cachedSdk) {
|
|
3228
|
-
cachedSdk = import("@earendil-works/pi-coding-agent");
|
|
3229
|
-
}
|
|
3230
|
-
return cachedSdk;
|
|
3231
|
-
}
|
|
3232
|
-
/**
|
|
3233
|
-
* Default production bindings backed by the real Pi SDK (services mode).
|
|
3234
|
-
* Dynamically imported so the worker bundle does not hard-fail if the SDK is
|
|
3235
|
-
* absent at module load (discovered lazily by pi-readiness instead).
|
|
3236
|
-
*/
|
|
3237
|
-
export function createDefaultPiSdkBindings() {
|
|
3238
|
-
return {
|
|
3239
|
-
getAgentDir: async () => {
|
|
3240
|
-
const sdk = (await loadSdk());
|
|
3241
|
-
return sdk.getAgentDir();
|
|
3242
|
-
},
|
|
3243
|
-
createServices: async (opts) => {
|
|
3244
|
-
const sdk = (await loadSdk());
|
|
3245
|
-
// createAgentSessionServices returns the full AgentSessionServices object
|
|
3246
|
-
// directly (cwd/agentDir/modelRuntime/settingsManager/resourceLoader).
|
|
3247
|
-
// Wrap it so our type contract is satisfied.
|
|
3248
|
-
const result = await sdk.createAgentSessionServices({
|
|
3249
|
-
cwd: opts.cwd,
|
|
3250
|
-
agentDir: opts.agentDir,
|
|
3251
|
-
resourceLoaderOptions: opts.resourceLoaderOptions,
|
|
3252
|
-
});
|
|
3253
|
-
return { services: result };
|
|
3254
|
-
},
|
|
3255
|
-
createSessionFromServices: async (opts) => {
|
|
3256
|
-
const sdk = (await loadSdk());
|
|
3257
|
-
return sdk.createAgentSessionFromServices({
|
|
3258
|
-
services: opts.services,
|
|
3259
|
-
sessionManager: opts.sessionManager,
|
|
3260
|
-
...(opts.tools ? { tools: opts.tools } : {}),
|
|
3261
|
-
...(opts.noTools ? { noTools: opts.noTools } : {}),
|
|
3262
|
-
...(opts.excludeTools ? { excludeTools: opts.excludeTools } : {}),
|
|
3263
|
-
...(opts.customTools ? { customTools: opts.customTools } : {}),
|
|
3264
|
-
...(opts.model ? { model: opts.model } : {}),
|
|
3265
|
-
});
|
|
3266
|
-
},
|
|
3267
|
-
createSessionManager: async (opts) => {
|
|
3268
|
-
const sdk = (await loadSdk());
|
|
3269
|
-
return sdk.SessionManager.create(opts.cwd, opts.sessionDir, {
|
|
3270
|
-
id: opts.sessionId,
|
|
3271
|
-
});
|
|
3272
|
-
},
|
|
3273
|
-
materializeSessionShell: async (opts) => {
|
|
3274
|
-
const sdk = (await loadSdk());
|
|
3275
|
-
const manager = opts.sessionManager;
|
|
3276
|
-
if (typeof manager.setSessionFile !== "function") {
|
|
3277
|
-
throw new Error("Pi SessionManager cannot materialize a session file");
|
|
3278
|
-
}
|
|
3279
|
-
let created = false;
|
|
3280
|
-
try {
|
|
3281
|
-
await mkdir(path.dirname(opts.sessionFile), { recursive: true });
|
|
3282
|
-
// Match Pi's documented session header. Rebinding through the public
|
|
3283
|
-
// SessionManager API marks it flushed, so Pi appends the first user
|
|
3284
|
-
// entry instead of trying to create the already-present file later.
|
|
3285
|
-
await writeFile(opts.sessionFile, `${JSON.stringify({
|
|
3286
|
-
type: "session",
|
|
3287
|
-
version: typeof sdk.CURRENT_SESSION_VERSION === "number"
|
|
3288
|
-
? sdk.CURRENT_SESSION_VERSION
|
|
3289
|
-
: 3,
|
|
3290
|
-
id: opts.sessionId,
|
|
3291
|
-
timestamp: new Date().toISOString(),
|
|
3292
|
-
cwd: opts.cwd,
|
|
3293
|
-
})}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
3294
|
-
created = true;
|
|
3295
|
-
manager.setSessionFile(opts.sessionFile);
|
|
3296
|
-
}
|
|
3297
|
-
catch (error) {
|
|
3298
|
-
if (created)
|
|
3299
|
-
await rm(opts.sessionFile, { force: true }).catch(() => undefined);
|
|
3300
|
-
throw error;
|
|
3301
|
-
}
|
|
3302
|
-
},
|
|
3303
|
-
createInMemorySessionManager: async (opts) => {
|
|
3304
|
-
const sdk = (await loadSdk());
|
|
3305
|
-
return sdk.SessionManager.inMemory(opts.cwd);
|
|
3306
|
-
},
|
|
3307
|
-
forkSessionManager: async (opts) => {
|
|
3308
|
-
const sdk = (await loadSdk());
|
|
3309
|
-
return sdk.SessionManager.forkFrom(opts.sourcePath, opts.targetCwd, opts.targetSessionDir, { id: opts.targetSessionId });
|
|
3310
|
-
},
|
|
3311
|
-
openSessionManager: async (opts) => {
|
|
3312
|
-
const sdk = (await loadSdk());
|
|
3313
|
-
try {
|
|
3314
|
-
// Pi SessionManager.open(missingPath) does NOT throw — it creates a
|
|
3315
|
-
// fresh in-memory session with a random UUID. That would reopen the
|
|
3316
|
-
// wrong conversation under the durable Console sessionId. Fail closed
|
|
3317
|
-
// before open when the persisted file is absent.
|
|
3318
|
-
await access(opts.sessionFile);
|
|
3319
|
-
}
|
|
3320
|
-
catch (error) {
|
|
3321
|
-
const code = error && typeof error === "object" && "code" in error
|
|
3322
|
-
? String(error.code ?? "")
|
|
3323
|
-
: "";
|
|
3324
|
-
if (code === "ENOENT") {
|
|
3325
|
-
throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
|
|
3326
|
-
}
|
|
3327
|
-
// File exists but is unreadable (permissions / I/O) — caller maps
|
|
3328
|
-
// undefined to 503 rather than silently creating a fresh session.
|
|
3329
|
-
return undefined;
|
|
3330
|
-
}
|
|
3331
|
-
return sdk.SessionManager.open(opts.sessionFile);
|
|
3332
|
-
},
|
|
3333
|
-
resolveModel: (opts) => {
|
|
3334
|
-
const rt = opts.modelRuntime;
|
|
3335
|
-
if (typeof rt?.getModel !== "function")
|
|
3336
|
-
return undefined;
|
|
3337
|
-
try {
|
|
3338
|
-
return rt.getModel(opts.provider, opts.modelId);
|
|
3339
|
-
}
|
|
3340
|
-
catch {
|
|
3341
|
-
return undefined;
|
|
3342
|
-
}
|
|
3343
|
-
},
|
|
3344
|
-
listAvailableModels: (opts) => {
|
|
3345
|
-
const rt = opts.modelRuntime;
|
|
3346
|
-
const available = rt?.snapshot?.available ?? [];
|
|
3347
|
-
const configured = rt?.snapshot?.configuredProviders;
|
|
3348
|
-
const isConfigured = (provider) => configured instanceof Set ? configured.has(provider) : false;
|
|
3349
|
-
return available.map((m) => ({
|
|
3350
|
-
id: m.id,
|
|
3351
|
-
name: m.name,
|
|
3352
|
-
provider: m.provider,
|
|
3353
|
-
hasCredentials: isConfigured(m.provider),
|
|
3354
|
-
}));
|
|
3355
|
-
},
|
|
3356
|
-
};
|
|
3357
|
-
}
|
|
3358
2354
|
/**
|
|
3359
2355
|
* Re-export the tool gates for the dispatcher layer so the per-tool-call gate
|
|
3360
2356
|
* (Gate 3) lives in exactly one place. authorizeChatSessionTool is the
|