@themoltnet/pi-extension 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +366 -156
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1965,7 +1965,7 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
|
|
|
1965
1965
|
...options
|
|
1966
1966
|
});
|
|
1967
1967
|
/**
|
|
1968
|
-
*
|
|
1968
|
+
* Queue asynchronous deletion of terminal tasks in bulk. By default, live, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
|
|
1969
1969
|
*/
|
|
1970
1970
|
var batchDeleteTasks = (options) => (options.client ?? client).delete({
|
|
1971
1971
|
security: [
|
|
@@ -18037,7 +18037,7 @@ function createMoltNetTools(config) {
|
|
|
18037
18037
|
const searchEntries = defineTool({
|
|
18038
18038
|
name: "moltnet_search_entries",
|
|
18039
18039
|
label: "Search MoltNet Diary Entries",
|
|
18040
|
-
description: "Hybrid (semantic + lexical) search over diary entries. Optional tags / excludeTags / entryTypes filters AND with the query; the taskFilter shorthand expands into task:* provenance tags so `taskFilter: { taskType: \"fulfill_brief\" }` returns only entries from fulfill_brief attempts. Filters apply server-side before ranking.",
|
|
18040
|
+
description: "Hybrid (semantic + lexical) search over diary entries. Use proactively before non-trivial investigation, code changes, review, or episodic incident capture so prior decisions and recurring failures surface before you act. Do not search randomly: pass taskFilter for task/correlation-local searches and tags or entryTypes for broader prior-knowledge searches. Optional tags / excludeTags / entryTypes filters AND with the query; the taskFilter shorthand expands into task:* provenance tags so `taskFilter: { taskType: \"fulfill_brief\" }` returns only entries from fulfill_brief attempts. Filters apply server-side before ranking.",
|
|
18041
18041
|
parameters: Type.Object({
|
|
18042
18042
|
query: Type.String({ description: "Natural language search query" }),
|
|
18043
18043
|
limit: Type.Optional(Type.Number({ description: "Max results (default 5)" })),
|
|
@@ -18096,12 +18096,18 @@ function createMoltNetTools(config) {
|
|
|
18096
18096
|
const createEntry = defineTool({
|
|
18097
18097
|
name: "moltnet_create_entry",
|
|
18098
18098
|
label: "Create MoltNet Diary Entry",
|
|
18099
|
-
description: "Create a new diary entry to record decisions, findings, incidents, or reflections. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected. Use this tool — NOT `moltnet entry create` / `moltnet entry create-signed` via bash. The CLI path bypasses task-tag auto-injection and leaves entries invisible to taskFilter queries.",
|
|
18099
|
+
description: "Create a new diary entry to record decisions, findings, incidents, or reflections. Before creating an episodic incident entry, first call moltnet_search_entries with the title/root-cause/error/watch-for terms plus taskFilter, tags, or entryTypes filters, then reference close matches instead of creating an isolated duplicate. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected. Use this tool — NOT `moltnet entry create` / `moltnet entry create-signed` via bash. The CLI path bypasses task-tag auto-injection and leaves entries invisible to taskFilter queries.",
|
|
18100
18100
|
parameters: Type.Object({
|
|
18101
18101
|
title: Type.String({ description: "Entry title (concise, descriptive)" }),
|
|
18102
18102
|
content: Type.String({ description: "Entry content (markdown)" }),
|
|
18103
18103
|
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorization" })),
|
|
18104
18104
|
importance: Type.Optional(Type.Number({ description: "Importance 1-10 (default 5)" })),
|
|
18105
|
+
entryType: Type.Optional(Type.Union([
|
|
18106
|
+
Type.Literal("episodic"),
|
|
18107
|
+
Type.Literal("semantic"),
|
|
18108
|
+
Type.Literal("procedural"),
|
|
18109
|
+
Type.Literal("reflection")
|
|
18110
|
+
], { description: "Entry type. Use episodic for incidents, workarounds, bugs, or recurrence evidence; defaults to semantic." })),
|
|
18105
18111
|
diaryId: Type.Optional(Type.String({ description: "Explicit diary id. During an active task, must match the task diary or the call is rejected. Outside a task, overrides the env-derived diary." }))
|
|
18106
18112
|
}),
|
|
18107
18113
|
async execute(_id, params) {
|
|
@@ -18125,7 +18131,8 @@ function createMoltNetTools(config) {
|
|
|
18125
18131
|
title: params.title,
|
|
18126
18132
|
content: params.content,
|
|
18127
18133
|
tags: mergedTags,
|
|
18128
|
-
importance: params.importance ?? 5
|
|
18134
|
+
importance: params.importance ?? 5,
|
|
18135
|
+
...params.entryType ? { entryType: params.entryType } : {}
|
|
18129
18136
|
});
|
|
18130
18137
|
return {
|
|
18131
18138
|
content: [{
|
|
@@ -18135,6 +18142,8 @@ function createMoltNetTools(config) {
|
|
|
18135
18142
|
title: entry.title,
|
|
18136
18143
|
createdAt: entry.createdAt,
|
|
18137
18144
|
diaryId: targetDiaryId,
|
|
18145
|
+
entryType: entry.entryType,
|
|
18146
|
+
importance: entry.importance,
|
|
18138
18147
|
tags: mergedTags
|
|
18139
18148
|
}, null, 2)
|
|
18140
18149
|
}],
|
|
@@ -18514,6 +18523,29 @@ function buildRuntimeInstructor(ctx) {
|
|
|
18514
18523
|
" requires human approval and is unavailable in headless task runs;",
|
|
18515
18524
|
" never use it for routine git/gh.",
|
|
18516
18525
|
"",
|
|
18526
|
+
"## Proactive memory use",
|
|
18527
|
+
"",
|
|
18528
|
+
"- Before non-trivial investigation, debugging, code changes, or review,",
|
|
18529
|
+
" check the task diary for relevant prior knowledge instead of waiting",
|
|
18530
|
+
" for a human to ask. Use `moltnet_diary_tags` for cheap reconnaissance,",
|
|
18531
|
+
" `moltnet_list_entries` when tags or task provenance are known, and",
|
|
18532
|
+
" `moltnet_search_entries` for semantic similarity. Do not search",
|
|
18533
|
+
" randomly: pass `taskFilter` for task-local or correlation-local",
|
|
18534
|
+
" queries, and pass `tags` / `entryTypes` for broader prior-knowledge",
|
|
18535
|
+
" queries using known tags such as `incident`, `decision`, or",
|
|
18536
|
+
" `scope:<area>`. Broaden only after constrained searches miss.",
|
|
18537
|
+
"- Before creating an `episodic` incident entry, you MUST search for",
|
|
18538
|
+
" similar incidents using the proposed title, root cause, error text,",
|
|
18539
|
+
" affected subsystem, and watch-for terms, filtered by `entryTypes:",
|
|
18540
|
+
" [\"episodic\", \"semantic\"]` and any known `scope:*` / task provenance",
|
|
18541
|
+
" tags. If a close prior match exists, do not create an isolated",
|
|
18542
|
+
" duplicate: reference the prior entry in your response or diary content,",
|
|
18543
|
+
" update/link it when the new occurrence adds material evidence, or",
|
|
18544
|
+
" create a new recurrence entry only when the recurrence itself is",
|
|
18545
|
+
" important signal.",
|
|
18546
|
+
"- When you create a recurrence entry, include the prior matching entry",
|
|
18547
|
+
" id(s) in the content and explain what is new about this occurrence.",
|
|
18548
|
+
"",
|
|
18517
18549
|
"## Diary discipline",
|
|
18518
18550
|
"",
|
|
18519
18551
|
`- During this task, every diary entry MUST land in \`${ctx.diaryId}\``,
|
|
@@ -20114,6 +20146,22 @@ function buildFinalOutputBlock(opts) {
|
|
|
20114
20146
|
return lines.join("\n");
|
|
20115
20147
|
}
|
|
20116
20148
|
//#endregion
|
|
20149
|
+
//#region ../agent-runtime/src/prompts/proactive-memory.ts
|
|
20150
|
+
function buildProactiveMemoryWorkflowBlock() {
|
|
20151
|
+
return [
|
|
20152
|
+
"Before material work, apply the runtime instructor's proactive memory",
|
|
20153
|
+
"rules instead of waiting for a human prompt. Start with constrained",
|
|
20154
|
+
"diary context: inspect tags/list entries when task provenance or scope",
|
|
20155
|
+
"tags are known, then use `moltnet_search_entries` with `taskFilter`,",
|
|
20156
|
+
"`entryTypes`, and tags. Do not run broad unfiltered searches before",
|
|
20157
|
+
"constrained searches miss.",
|
|
20158
|
+
"",
|
|
20159
|
+
"For incident capture, follow the runtime instructor exactly: search",
|
|
20160
|
+
"for similar episodic/semantic entries first, reference close matches,",
|
|
20161
|
+
"and create a recurrence entry only when the repeat is useful signal."
|
|
20162
|
+
].join("\n");
|
|
20163
|
+
}
|
|
20164
|
+
//#endregion
|
|
20117
20165
|
//#region ../agent-runtime/src/prompts/rubric-common.ts
|
|
20118
20166
|
function renderRubricCriteriaList(rubric) {
|
|
20119
20167
|
return rubric.criteria.map((c, i) => `${i + 1}. **${c.id}** (weight ${c.weight}, scoring: \`${c.scoring}\`) — ${c.description}`).join("\n");
|
|
@@ -20215,6 +20263,12 @@ function buildAssessBriefUserPrompt(input, ctx) {
|
|
|
20215
20263
|
header: "Querying the producer's diary entries",
|
|
20216
20264
|
body: diaryQuery
|
|
20217
20265
|
},
|
|
20266
|
+
{
|
|
20267
|
+
id: "assess_brief.proactive_memory",
|
|
20268
|
+
source: "discipline",
|
|
20269
|
+
header: "Proactive memory use",
|
|
20270
|
+
body: buildProactiveMemoryWorkflowBlock()
|
|
20271
|
+
},
|
|
20218
20272
|
{
|
|
20219
20273
|
id: "assess_brief.workspace",
|
|
20220
20274
|
source: "workspace",
|
|
@@ -20542,11 +20596,13 @@ function buildFreeformUserPrompt(input, ctx) {
|
|
|
20542
20596
|
const suggestedTaskType = input.suggestedTaskType ? [`The proposer suggested task type \`${input.suggestedTaskType}\`.`, "Use it as a hint, not as a contract."].join("\n") : "";
|
|
20543
20597
|
const workflow = [
|
|
20544
20598
|
"1. Clarify the real objective from the brief before acting.",
|
|
20545
|
-
"2.
|
|
20546
|
-
"
|
|
20547
|
-
"
|
|
20599
|
+
"2. Search MoltNet diary memory for prior decisions, incidents, and",
|
|
20600
|
+
" recurring traps relevant to the brief.",
|
|
20601
|
+
"3. Gather enough context to avoid guessing.",
|
|
20602
|
+
"4. Complete the requested work when it is safe and bounded.",
|
|
20603
|
+
"5. If the request reveals a recurring task shape, include a",
|
|
20548
20604
|
" `proposedTaskType` in the final output with a concise rationale.",
|
|
20549
|
-
"
|
|
20605
|
+
"6. If you changed code on a branch, include that branch in",
|
|
20550
20606
|
" `branch` so future continuations can recover git context."
|
|
20551
20607
|
].join("\n");
|
|
20552
20608
|
const sections = [
|
|
@@ -20585,6 +20641,12 @@ function buildFreeformUserPrompt(input, ctx) {
|
|
|
20585
20641
|
header: "Workflow",
|
|
20586
20642
|
body: workflow
|
|
20587
20643
|
},
|
|
20644
|
+
{
|
|
20645
|
+
id: "freeform.proactive_memory",
|
|
20646
|
+
source: "discipline",
|
|
20647
|
+
header: "Proactive memory use",
|
|
20648
|
+
body: buildProactiveMemoryWorkflowBlock()
|
|
20649
|
+
},
|
|
20588
20650
|
{
|
|
20589
20651
|
id: "freeform.verification",
|
|
20590
20652
|
source: "verification",
|
|
@@ -20661,13 +20723,15 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
20661
20723
|
].join("\n") : "";
|
|
20662
20724
|
const workflow = [
|
|
20663
20725
|
ctx.workspace?.mode === "dedicated_worktree" ? `1. Use the already-provisioned dedicated worktree branch${ctx.workspace.branch ? ` (\`${ctx.workspace.branch}\`)` : ""}; do not create or switch the primary checkout.` : `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
|
|
20664
|
-
"2.
|
|
20665
|
-
"
|
|
20666
|
-
"
|
|
20667
|
-
"
|
|
20726
|
+
"2. Search MoltNet diary memory for prior decisions, incidents, and",
|
|
20727
|
+
" recurring traps relevant to the brief before changing code.",
|
|
20728
|
+
"3. Understand the problem — read relevant code; do not speculate.",
|
|
20729
|
+
"4. Implement the change. Keep commits small and coherent.",
|
|
20730
|
+
"5. Add tests if applicable.",
|
|
20731
|
+
"6. For every commit, create a signed diary entry first via",
|
|
20668
20732
|
" `moltnet_create_entry` and embed its id in the commit trailer",
|
|
20669
20733
|
" `MoltNet-Diary: <id>` (per the runtime instructor).",
|
|
20670
|
-
"
|
|
20734
|
+
"7. Push the branch and open a PR — run `git push` and `gh pr create`",
|
|
20671
20735
|
" IN the VM with your normal `bash` tool (use the",
|
|
20672
20736
|
" `GH_TOKEN=$(moltnet github token …) gh …` form from the runtime",
|
|
20673
20737
|
" instructor). Do NOT use `moltnet_host_exec` for this; it needs human",
|
|
@@ -20709,6 +20773,12 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
20709
20773
|
header: "Workflow",
|
|
20710
20774
|
body: workflow
|
|
20711
20775
|
},
|
|
20776
|
+
{
|
|
20777
|
+
id: "fulfill_brief.proactive_memory",
|
|
20778
|
+
source: "discipline",
|
|
20779
|
+
header: "Proactive memory use",
|
|
20780
|
+
body: buildProactiveMemoryWorkflowBlock()
|
|
20781
|
+
},
|
|
20712
20782
|
{
|
|
20713
20783
|
id: "fulfill_brief.verification",
|
|
20714
20784
|
source: "verification",
|
|
@@ -21024,13 +21094,15 @@ function buildPrReviewUserPrompt(input, ctx) {
|
|
|
21024
21094
|
const workflow = [
|
|
21025
21095
|
"1. Read the subject summary, resources, inspection hints, and any",
|
|
21026
21096
|
" task-specific instructions before scoring.",
|
|
21027
|
-
"2.
|
|
21097
|
+
"2. Search MoltNet diary memory for prior decisions, incidents, and",
|
|
21098
|
+
" recurring review traps relevant to the subject.",
|
|
21099
|
+
"3. Inspect the target artefact directly using the tools and resources the",
|
|
21028
21100
|
" task makes available.",
|
|
21029
|
-
"
|
|
21101
|
+
"4. If you are in a dedicated disposable worktree and need the review target",
|
|
21030
21102
|
" checked out locally, do that work inside this disposable workspace only.",
|
|
21031
|
-
"
|
|
21103
|
+
"5. Apply the rubric strictly. This task is about complexity and",
|
|
21032
21104
|
" reviewability, not correctness or feature desirability.",
|
|
21033
|
-
"
|
|
21105
|
+
"6. Perform any required outward action before emitting the final",
|
|
21034
21106
|
" structured output."
|
|
21035
21107
|
].join("\n");
|
|
21036
21108
|
const taskPromptSection = input.taskPrompt ?? "";
|
|
@@ -21087,6 +21159,12 @@ function buildPrReviewUserPrompt(input, ctx) {
|
|
|
21087
21159
|
header: "Review workflow",
|
|
21088
21160
|
body: workflow
|
|
21089
21161
|
},
|
|
21162
|
+
{
|
|
21163
|
+
id: "pr_review.proactive_memory",
|
|
21164
|
+
source: "discipline",
|
|
21165
|
+
header: "Proactive memory use",
|
|
21166
|
+
body: buildProactiveMemoryWorkflowBlock()
|
|
21167
|
+
},
|
|
21090
21168
|
{
|
|
21091
21169
|
id: "pr_review.task_prompt",
|
|
21092
21170
|
source: "task_input",
|
|
@@ -26409,14 +26487,10 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26409
26487
|
});
|
|
26410
26488
|
return makeFailedOutput("session_setup_failed", message);
|
|
26411
26489
|
}
|
|
26412
|
-
|
|
26413
|
-
let llmErrorMessage = null;
|
|
26414
|
-
let assistantText = "";
|
|
26490
|
+
const turnState = createSessionTurnState();
|
|
26415
26491
|
let reporterError = null;
|
|
26416
26492
|
const usage = finalUsage;
|
|
26417
26493
|
let capAbort = null;
|
|
26418
|
-
let toolUseTurnCount = 0;
|
|
26419
|
-
let bashTimeoutCount = 0;
|
|
26420
26494
|
const maxTurns = opts.maxTurns ?? 0;
|
|
26421
26495
|
const maxBashTimeouts = opts.maxBashTimeouts ?? 3;
|
|
26422
26496
|
cancelListener = wireSessionAbort(reporter.cancelSignal, session);
|
|
@@ -26451,49 +26525,16 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26451
26525
|
message
|
|
26452
26526
|
}));
|
|
26453
26527
|
};
|
|
26454
|
-
session.subscribe((
|
|
26455
|
-
|
|
26456
|
-
|
|
26457
|
-
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
tool_name: event.toolName,
|
|
26465
|
-
is_error: event.isError,
|
|
26466
|
-
result: event.isError ? truncateForWire(event.result) : void 0
|
|
26467
|
-
}));
|
|
26468
|
-
if (shouldEmitToolCallError(event)) track(emitError("tool_call_error", describeToolErrorMessage(event.result), {
|
|
26469
|
-
tool: event.toolName,
|
|
26470
|
-
result: truncateForWire(event.result)
|
|
26471
|
-
}));
|
|
26472
|
-
if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
|
|
26473
|
-
bashTimeoutCount += 1;
|
|
26474
|
-
if (bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
|
|
26475
|
-
}
|
|
26476
|
-
} else if (event.type === "turn_end") {
|
|
26477
|
-
const msg = event.message;
|
|
26478
|
-
if (msg?.role === "assistant" && msg.usage) {
|
|
26479
|
-
usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
|
|
26480
|
-
usage.outputTokens += Math.max(0, msg.usage.output ?? 0);
|
|
26481
|
-
const cr = Math.max(0, msg.usage.cacheRead ?? 0);
|
|
26482
|
-
const cw = Math.max(0, msg.usage.cacheWrite ?? 0);
|
|
26483
|
-
if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
|
|
26484
|
-
if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
|
|
26485
|
-
}
|
|
26486
|
-
const stopReason = msg?.stopReason ?? "end_turn";
|
|
26487
|
-
track(emit("turn_end", { stop_reason: stopReason }));
|
|
26488
|
-
if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
|
|
26489
|
-
toolUseTurnCount += 1;
|
|
26490
|
-
if (toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
|
|
26491
|
-
}
|
|
26492
|
-
llmAbort = msg?.stopReason === "error";
|
|
26493
|
-
if (msg?.stopReason === "error") llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
|
|
26494
|
-
else llmErrorMessage = null;
|
|
26495
|
-
}
|
|
26496
|
-
});
|
|
26528
|
+
session.subscribe(makeSessionEventHandler({
|
|
26529
|
+
state: turnState,
|
|
26530
|
+
usage,
|
|
26531
|
+
maxTurns,
|
|
26532
|
+
maxBashTimeouts,
|
|
26533
|
+
emit,
|
|
26534
|
+
emitError,
|
|
26535
|
+
track,
|
|
26536
|
+
triggerCapAbort
|
|
26537
|
+
}));
|
|
26497
26538
|
let runError = null;
|
|
26498
26539
|
const runPrompt = (promptText) => promptWithProviderErrorRetries({
|
|
26499
26540
|
session: liveSession,
|
|
@@ -26501,8 +26542,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26501
26542
|
cancelSignal: reporter.cancelSignal,
|
|
26502
26543
|
isCapAborted: () => capAbort !== null,
|
|
26503
26544
|
getProviderErrorState: () => ({
|
|
26504
|
-
llmAbort,
|
|
26505
|
-
llmErrorMessage
|
|
26545
|
+
llmAbort: turnState.llmAbort,
|
|
26546
|
+
llmErrorMessage: turnState.llmErrorMessage
|
|
26506
26547
|
}),
|
|
26507
26548
|
maxRetries: opts.maxProviderErrorRetries ?? 2,
|
|
26508
26549
|
baseDelayMs: opts.providerErrorRetryBaseDelayMs ?? 2e3,
|
|
@@ -26531,7 +26572,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26531
26572
|
isStopped: () => submitRepromptStopped({
|
|
26532
26573
|
cancelled: reporter.cancelSignal.aborted,
|
|
26533
26574
|
capAborted: capAbort !== null,
|
|
26534
|
-
llmAbort
|
|
26575
|
+
llmAbort: turnState.llmAbort
|
|
26535
26576
|
}),
|
|
26536
26577
|
onSubmitReprompt: async (event) => {
|
|
26537
26578
|
await emit("info", event);
|
|
@@ -26552,62 +26593,18 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26552
26593
|
let parsedOutput = null;
|
|
26553
26594
|
let parsedOutputCid = null;
|
|
26554
26595
|
let parseError = null;
|
|
26555
|
-
if (!runError && !llmAbort && !cancelled && !capAbort) {
|
|
26556
|
-
const captured =
|
|
26557
|
-
|
|
26558
|
-
|
|
26559
|
-
|
|
26560
|
-
|
|
26561
|
-
|
|
26562
|
-
|
|
26563
|
-
|
|
26564
|
-
|
|
26565
|
-
|
|
26566
|
-
|
|
26567
|
-
parsedOutput = null;
|
|
26568
|
-
parsedOutputCid = null;
|
|
26569
|
-
parseError = {
|
|
26570
|
-
code: "output_cid_compute_failed",
|
|
26571
|
-
message: `Captured submit-tool output could not be canonicalized: ${message}`
|
|
26572
|
-
};
|
|
26573
|
-
recordTaskOutputParseResult({
|
|
26574
|
-
taskType: task.taskType,
|
|
26575
|
-
model: opts.model,
|
|
26576
|
-
code: "output_cid_compute_failed"
|
|
26577
|
-
});
|
|
26578
|
-
await emit("error", {
|
|
26579
|
-
message: parseError.message,
|
|
26580
|
-
phase: "output_validation"
|
|
26581
|
-
});
|
|
26582
|
-
}
|
|
26583
|
-
else if (submitToolHandle) {
|
|
26584
|
-
const exhausted = submitToolHandle.getExhaustedValidationFailure();
|
|
26585
|
-
parseError = exhausted ?? {
|
|
26586
|
-
code: "submit_output_missing",
|
|
26587
|
-
message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
|
|
26588
|
-
};
|
|
26589
|
-
if (!exhausted) recordTaskOutputParseResult({
|
|
26590
|
-
taskType: task.taskType,
|
|
26591
|
-
model: opts.model,
|
|
26592
|
-
code: "output_missing"
|
|
26593
|
-
});
|
|
26594
|
-
await emit("error", {
|
|
26595
|
-
message: parseError.message,
|
|
26596
|
-
phase: "output_validation"
|
|
26597
|
-
});
|
|
26598
|
-
} else {
|
|
26599
|
-
const parsed = await parseStructuredTaskOutput(assistantText, task.taskType, {
|
|
26600
|
-
model: opts.model,
|
|
26601
|
-
input: task.input
|
|
26602
|
-
});
|
|
26603
|
-
parsedOutput = parsed.output;
|
|
26604
|
-
parsedOutputCid = parsed.outputCid;
|
|
26605
|
-
parseError = parsed.error;
|
|
26606
|
-
if (parseError) await emit("error", {
|
|
26607
|
-
message: parseError.message,
|
|
26608
|
-
phase: "output_validation"
|
|
26609
|
-
});
|
|
26610
|
-
}
|
|
26596
|
+
if (!runError && !turnState.llmAbort && !cancelled && !capAbort) {
|
|
26597
|
+
const captured = await captureAttemptOutput({
|
|
26598
|
+
taskType: task.taskType,
|
|
26599
|
+
model: opts.model,
|
|
26600
|
+
input: task.input,
|
|
26601
|
+
assistantText: turnState.assistantText,
|
|
26602
|
+
submitToolHandle,
|
|
26603
|
+
emit
|
|
26604
|
+
});
|
|
26605
|
+
parsedOutput = captured.output;
|
|
26606
|
+
parsedOutputCid = captured.outputCid;
|
|
26607
|
+
parseError = captured.error;
|
|
26611
26608
|
}
|
|
26612
26609
|
if (cancelled) return {
|
|
26613
26610
|
taskId: task.id,
|
|
@@ -26638,54 +26635,267 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26638
26635
|
retryable: false
|
|
26639
26636
|
}
|
|
26640
26637
|
};
|
|
26641
|
-
|
|
26642
|
-
const status = runError || llmAbort || parseError || reporterErrorSnapshot ? "failed" : "completed";
|
|
26643
|
-
const errorCode = runError?.code ?? parseError?.code ?? reporterErrorSnapshot?.code ?? (llmAbort ? "llm_api_error" : void 0);
|
|
26644
|
-
const errorMessage = runError?.message ?? parseError?.message ?? reporterErrorSnapshot?.message ?? (llmAbort ? llmErrorMessage ?? "LLM API error during turn" : void 0);
|
|
26645
|
-
const errorRetryable = reporterErrorSnapshot && errorCode === reporterErrorSnapshot.code && errorMessage === reporterErrorSnapshot.message ? reporterErrorSnapshot.retryable ?? false : false;
|
|
26646
|
-
return {
|
|
26638
|
+
return buildAttemptResult({
|
|
26647
26639
|
taskId: task.id,
|
|
26648
26640
|
attemptN,
|
|
26649
|
-
status,
|
|
26650
26641
|
output: parsedOutput,
|
|
26651
26642
|
outputCid: parsedOutputCid,
|
|
26652
26643
|
usage,
|
|
26653
26644
|
durationMs: Date.now() - startTime,
|
|
26654
|
-
|
|
26655
|
-
|
|
26656
|
-
|
|
26657
|
-
|
|
26658
|
-
|
|
26659
|
-
};
|
|
26645
|
+
runError,
|
|
26646
|
+
parseError,
|
|
26647
|
+
reporterError,
|
|
26648
|
+
llmAbort: turnState.llmAbort,
|
|
26649
|
+
llmErrorMessage: turnState.llmErrorMessage
|
|
26650
|
+
});
|
|
26660
26651
|
} catch (err) {
|
|
26661
26652
|
return makeFailedOutput("executor_unexpected_error", err instanceof Error ? err.message : String(err));
|
|
26662
26653
|
} finally {
|
|
26663
|
-
|
|
26664
|
-
|
|
26665
|
-
|
|
26666
|
-
|
|
26667
|
-
|
|
26668
|
-
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
26672
|
-
|
|
26654
|
+
await cleanupAttempt({
|
|
26655
|
+
cancelSignal: reporter.cancelSignal,
|
|
26656
|
+
cancelListener,
|
|
26657
|
+
session,
|
|
26658
|
+
reporterOpen,
|
|
26659
|
+
reporter,
|
|
26660
|
+
finalUsage,
|
|
26661
|
+
managed,
|
|
26662
|
+
workspace,
|
|
26663
|
+
taskId: task.id,
|
|
26664
|
+
attemptN
|
|
26665
|
+
});
|
|
26666
|
+
}
|
|
26667
|
+
}
|
|
26668
|
+
function createSessionTurnState() {
|
|
26669
|
+
return {
|
|
26670
|
+
assistantText: "",
|
|
26671
|
+
llmAbort: false,
|
|
26672
|
+
llmErrorMessage: null,
|
|
26673
|
+
toolUseTurnCount: 0,
|
|
26674
|
+
bashTimeoutCount: 0
|
|
26675
|
+
};
|
|
26676
|
+
}
|
|
26677
|
+
/**
|
|
26678
|
+
* Build the `AgentSession.subscribe` handler for one attempt: bridges pi
|
|
26679
|
+
* events to the reporter, accumulates token usage and assistant text, and
|
|
26680
|
+
* enforces the bash-timeout and tool-use-turn caps. Extracted from
|
|
26681
|
+
* `executePiTask` so this dense, branch-heavy logic is unit-tested against a
|
|
26682
|
+
* scripted event stream instead of only through a booted VM.
|
|
26683
|
+
*
|
|
26684
|
+
* The handler mutates `deps.state` and `deps.usage` in place; the caller reads
|
|
26685
|
+
* them after `session.prompt()` resolves (by which point `state.llmAbort`
|
|
26686
|
+
* holds the terminal turn's outcome — see the "last-turn wins" note below).
|
|
26687
|
+
*
|
|
26688
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26689
|
+
*/
|
|
26690
|
+
function makeSessionEventHandler(deps) {
|
|
26691
|
+
const { state, usage, maxTurns, maxBashTimeouts, emit, emitError, track, triggerCapAbort } = deps;
|
|
26692
|
+
return (event) => {
|
|
26693
|
+
if (event.type === "message_update") {
|
|
26694
|
+
const ae = event.assistantMessageEvent;
|
|
26695
|
+
if (ae.type === "text_delta") {
|
|
26696
|
+
state.assistantText += ae.delta;
|
|
26697
|
+
track(emit("text_delta", { delta: ae.delta }));
|
|
26673
26698
|
}
|
|
26674
|
-
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26678
|
-
|
|
26699
|
+
} else if (event.type === "tool_execution_start") track(emit("tool_call_start", { tool_name: event.toolName }));
|
|
26700
|
+
else if (event.type === "tool_execution_end") {
|
|
26701
|
+
track(emit("tool_call_end", {
|
|
26702
|
+
tool_name: event.toolName,
|
|
26703
|
+
is_error: event.isError,
|
|
26704
|
+
result: event.isError ? truncateForWire(event.result) : void 0
|
|
26705
|
+
}));
|
|
26706
|
+
if (shouldEmitToolCallError(event)) track(emitError("tool_call_error", describeToolErrorMessage(event.result), {
|
|
26707
|
+
tool: event.toolName,
|
|
26708
|
+
result: truncateForWire(event.result)
|
|
26709
|
+
}));
|
|
26710
|
+
if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
|
|
26711
|
+
state.bashTimeoutCount += 1;
|
|
26712
|
+
if (state.bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${state.bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
|
|
26679
26713
|
}
|
|
26714
|
+
} else if (event.type === "turn_end") {
|
|
26715
|
+
const msg = event.message;
|
|
26716
|
+
if (msg?.role === "assistant" && msg.usage) {
|
|
26717
|
+
usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
|
|
26718
|
+
usage.outputTokens += Math.max(0, msg.usage.output ?? 0);
|
|
26719
|
+
const cr = Math.max(0, msg.usage.cacheRead ?? 0);
|
|
26720
|
+
const cw = Math.max(0, msg.usage.cacheWrite ?? 0);
|
|
26721
|
+
if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
|
|
26722
|
+
if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
|
|
26723
|
+
}
|
|
26724
|
+
const stopReason = msg?.stopReason ?? "end_turn";
|
|
26725
|
+
track(emit("turn_end", { stop_reason: stopReason }));
|
|
26726
|
+
if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
|
|
26727
|
+
state.toolUseTurnCount += 1;
|
|
26728
|
+
if (state.toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${state.toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
|
|
26729
|
+
}
|
|
26730
|
+
state.llmAbort = msg?.stopReason === "error";
|
|
26731
|
+
if (msg?.stopReason === "error") state.llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
|
|
26732
|
+
else state.llmErrorMessage = null;
|
|
26680
26733
|
}
|
|
26681
|
-
|
|
26682
|
-
|
|
26683
|
-
|
|
26734
|
+
};
|
|
26735
|
+
}
|
|
26736
|
+
/**
|
|
26737
|
+
* Resolve the attempt's structured output once the session has finished
|
|
26738
|
+
* cleanly (no run error / provider abort / cancel / cap). Three mutually
|
|
26739
|
+
* exclusive paths, in precedence order:
|
|
26740
|
+
*
|
|
26741
|
+
* 1. Submit tool captured a payload → trust it, compute its CID. A
|
|
26742
|
+
* canonicalization failure becomes `output_cid_compute_failed`.
|
|
26743
|
+
* 2. Submit tool registered but nothing captured → the exhausted-validation
|
|
26744
|
+
* failure wins if present, else `submit_output_missing` (recording the
|
|
26745
|
+
* `output_missing` counter so the never-called path is observable).
|
|
26746
|
+
* 3. No submit tool (legacy task type) → parse the trailing assistant text.
|
|
26747
|
+
*
|
|
26748
|
+
* Extracted from `executePiTask` so this precedence — the part a refactor is
|
|
26749
|
+
* most likely to silently reorder — is unit-tested directly. The caller
|
|
26750
|
+
* still owns the guard deciding whether output capture runs at all.
|
|
26751
|
+
*
|
|
26752
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26753
|
+
*/
|
|
26754
|
+
async function captureAttemptOutput(deps) {
|
|
26755
|
+
const { taskType, model, input, assistantText, submitToolHandle, emit } = deps;
|
|
26756
|
+
const captured = submitToolHandle?.getCaptured() ?? null;
|
|
26757
|
+
if (captured) try {
|
|
26758
|
+
const outputCid = await computeJsonCid(captured);
|
|
26759
|
+
recordTaskOutputParseResult({
|
|
26760
|
+
taskType,
|
|
26761
|
+
model,
|
|
26762
|
+
code: "captured_via_tool"
|
|
26763
|
+
});
|
|
26764
|
+
return {
|
|
26765
|
+
output: captured,
|
|
26766
|
+
outputCid,
|
|
26767
|
+
error: null
|
|
26768
|
+
};
|
|
26769
|
+
} catch (err) {
|
|
26770
|
+
const error = {
|
|
26771
|
+
code: "output_cid_compute_failed",
|
|
26772
|
+
message: `Captured submit-tool output could not be canonicalized: ${err instanceof Error ? err.message : String(err)}`
|
|
26773
|
+
};
|
|
26774
|
+
recordTaskOutputParseResult({
|
|
26775
|
+
taskType,
|
|
26776
|
+
model,
|
|
26777
|
+
code: "output_cid_compute_failed"
|
|
26778
|
+
});
|
|
26779
|
+
await emit("error", {
|
|
26780
|
+
message: error.message,
|
|
26781
|
+
phase: "output_validation"
|
|
26782
|
+
});
|
|
26783
|
+
return {
|
|
26784
|
+
output: null,
|
|
26785
|
+
outputCid: null,
|
|
26786
|
+
error
|
|
26787
|
+
};
|
|
26788
|
+
}
|
|
26789
|
+
if (submitToolHandle) {
|
|
26790
|
+
const exhausted = submitToolHandle.getExhaustedValidationFailure();
|
|
26791
|
+
const error = exhausted ?? {
|
|
26792
|
+
code: "submit_output_missing",
|
|
26793
|
+
message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
|
|
26794
|
+
};
|
|
26795
|
+
if (!exhausted) recordTaskOutputParseResult({
|
|
26796
|
+
taskType,
|
|
26797
|
+
model,
|
|
26798
|
+
code: "output_missing"
|
|
26799
|
+
});
|
|
26800
|
+
await emit("error", {
|
|
26801
|
+
message: error.message,
|
|
26802
|
+
phase: "output_validation"
|
|
26803
|
+
});
|
|
26804
|
+
return {
|
|
26805
|
+
output: null,
|
|
26806
|
+
outputCid: null,
|
|
26807
|
+
error
|
|
26808
|
+
};
|
|
26809
|
+
}
|
|
26810
|
+
const parsed = await parseStructuredTaskOutput(assistantText, taskType, {
|
|
26811
|
+
model,
|
|
26812
|
+
input
|
|
26813
|
+
});
|
|
26814
|
+
if (parsed.error) await emit("error", {
|
|
26815
|
+
message: parsed.error.message,
|
|
26816
|
+
phase: "output_validation"
|
|
26817
|
+
});
|
|
26818
|
+
return {
|
|
26819
|
+
output: parsed.output,
|
|
26820
|
+
outputCid: parsed.outputCid,
|
|
26821
|
+
error: parsed.error
|
|
26822
|
+
};
|
|
26823
|
+
}
|
|
26824
|
+
/**
|
|
26825
|
+
* Assemble the terminal `TaskOutput` for a clean-or-failed finish (cancel and
|
|
26826
|
+
* cap aborts are handled by the caller's earlier returns). Encapsulates the
|
|
26827
|
+
* failure-precedence ladder — runError → parseError → reporterError →
|
|
26828
|
+
* provider abort — so the ordering is unit-tested rather than buried in the
|
|
26829
|
+
* orchestrator. A provider abort with no captured diagnostic falls back to a
|
|
26830
|
+
* generic message.
|
|
26831
|
+
*
|
|
26832
|
+
* Errors are non-retryable EXCEPT a reporterError that both wins the ladder
|
|
26833
|
+
* and set `retryable: true` (a transient reporter failure, #1538).
|
|
26834
|
+
*
|
|
26835
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26836
|
+
*/
|
|
26837
|
+
function buildAttemptResult(args) {
|
|
26838
|
+
const status = args.runError || args.llmAbort || args.parseError || args.reporterError ? "failed" : "completed";
|
|
26839
|
+
const errorCode = args.runError?.code ?? args.parseError?.code ?? args.reporterError?.code ?? (args.llmAbort ? "llm_api_error" : void 0);
|
|
26840
|
+
const errorMessage = args.runError?.message ?? args.parseError?.message ?? args.reporterError?.message ?? (args.llmAbort ? args.llmErrorMessage ?? "LLM API error during turn" : void 0);
|
|
26841
|
+
const errorRetryable = args.reporterError && errorCode === args.reporterError.code && errorMessage === args.reporterError.message ? args.reporterError.retryable ?? false : false;
|
|
26842
|
+
return {
|
|
26843
|
+
taskId: args.taskId,
|
|
26844
|
+
attemptN: args.attemptN,
|
|
26845
|
+
status,
|
|
26846
|
+
output: args.output,
|
|
26847
|
+
outputCid: args.outputCid,
|
|
26848
|
+
usage: args.usage,
|
|
26849
|
+
durationMs: args.durationMs,
|
|
26850
|
+
...errorCode && errorMessage ? { error: {
|
|
26851
|
+
code: errorCode,
|
|
26852
|
+
message: errorMessage,
|
|
26853
|
+
retryable: errorRetryable
|
|
26854
|
+
} } : {}
|
|
26855
|
+
};
|
|
26856
|
+
}
|
|
26857
|
+
/**
|
|
26858
|
+
* Tear down one attempt's resources, in order: detach the cancel listener →
|
|
26859
|
+
* dispose the pi session → finalize+close the reporter → close the VM →
|
|
26860
|
+
* clean the workspace. Extracted from `executePiTask`'s `finally` so the
|
|
26861
|
+
* swallow-vs-log-vs-propagate policy is pinned by tests.
|
|
26862
|
+
*
|
|
26863
|
+
* Failure handling is deliberately asymmetric and preserved exactly:
|
|
26864
|
+
* `session.dispose()` throws are silently swallowed; reporter finalize/close
|
|
26865
|
+
* and workspace cleanup failures are logged but non-fatal (the task is about
|
|
26866
|
+
* to be reported anyway). `vm.close()` is the one teardown error allowed to
|
|
26867
|
+
* propagate: a leaked VM means a live microVM the host never reclaims, so its
|
|
26868
|
+
* failure must surface loudly rather than be logged and forgotten.
|
|
26869
|
+
*
|
|
26870
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26871
|
+
*/
|
|
26872
|
+
async function cleanupAttempt(deps) {
|
|
26873
|
+
const log = deps.logError ?? ((m) => console.error(m));
|
|
26874
|
+
if (deps.cancelListener) deps.cancelSignal.removeEventListener("abort", deps.cancelListener);
|
|
26875
|
+
if (deps.session) try {
|
|
26876
|
+
deps.session.dispose();
|
|
26877
|
+
} catch {}
|
|
26878
|
+
if (deps.reporterOpen) {
|
|
26879
|
+
try {
|
|
26880
|
+
await deps.reporter.finalize(deps.finalUsage);
|
|
26881
|
+
} catch (err) {
|
|
26882
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
26883
|
+
log(`executePiTask: reporter.finalize() failed for task ${deps.taskId} attempt ${deps.attemptN}: ${detail}`);
|
|
26884
|
+
}
|
|
26885
|
+
try {
|
|
26886
|
+
await deps.reporter.close();
|
|
26684
26887
|
} catch (err) {
|
|
26685
26888
|
const detail = err instanceof Error ? err.message : String(err);
|
|
26686
|
-
|
|
26889
|
+
log(`executePiTask: reporter.close() failed for task ${deps.taskId} attempt ${deps.attemptN}: ${detail}`);
|
|
26687
26890
|
}
|
|
26688
26891
|
}
|
|
26892
|
+
if (deps.managed) await deps.managed.vm.close();
|
|
26893
|
+
if (deps.workspace) try {
|
|
26894
|
+
deps.workspace.cleanup();
|
|
26895
|
+
} catch (err) {
|
|
26896
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
26897
|
+
log(`executePiTask: workspace cleanup failed for task ${deps.taskId} attempt ${deps.attemptN}: ${detail}`);
|
|
26898
|
+
}
|
|
26689
26899
|
}
|
|
26690
26900
|
function applyExecutionPlanSandboxOverrides(sandboxConfig, executionPlan) {
|
|
26691
26901
|
const shadowWrites = executionPlan?.workspaceAttachment?.shadowWrites;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/pi-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
|
|
6
6
|
"keywords": [
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"@earendil-works/gondolin": "^0.9.1",
|
|
37
37
|
"@opentelemetry/api": "^1.9.0",
|
|
38
38
|
"typebox": "^1.2.8",
|
|
39
|
-
"@themoltnet/agent-runtime": "0.
|
|
40
|
-
"@themoltnet/sdk": "0.
|
|
39
|
+
"@themoltnet/agent-runtime": "0.35.0",
|
|
40
|
+
"@themoltnet/sdk": "0.119.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@earendil-works/pi-coding-agent": ">=0.74.0",
|