@themoltnet/pi-extension 0.15.2 → 0.16.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.d.ts +22 -0
- package/dist/index.js +128 -41
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -249,6 +249,28 @@ export declare interface ExecutePiTaskOptions {
|
|
|
249
249
|
* process. See #1078.
|
|
250
250
|
*/
|
|
251
251
|
makeOnTurnEvent?: TurnEventHandlerFactory;
|
|
252
|
+
/**
|
|
253
|
+
* Cap the number of tool-use turns per attempt. When the limit is
|
|
254
|
+
* reached, the pi session is aborted and the attempt finalizes with
|
|
255
|
+
* `error.code: max_turns_exceeded`. A tool-use turn = any `turn_end`
|
|
256
|
+
* whose `stopReason !== 'end_turn'` (matches the Anthropic SDK
|
|
257
|
+
* `max_turns` semantics: the model's final text-only response doesn't
|
|
258
|
+
* count). Default `0` = disabled. Recommended `30` for `fulfill_brief`.
|
|
259
|
+
* Closes part of #1094.
|
|
260
|
+
*/
|
|
261
|
+
maxTurns?: number;
|
|
262
|
+
/**
|
|
263
|
+
* Cap the number of `bash` tool timeouts per attempt. A timeout is a
|
|
264
|
+
* `tool_execution_end` for `bash` whose result text contains
|
|
265
|
+
* "Command timed out after" (pi's stable error wrapper from
|
|
266
|
+
* `@earendil-works/pi-coding-agent`'s bash tool). When the limit is
|
|
267
|
+
* reached, the pi session is aborted and the attempt finalizes with
|
|
268
|
+
* `error.code: max_bash_timeouts_exceeded`. Catches the death-spiral
|
|
269
|
+
* pattern from task `a3762f44` where the model kept retrying
|
|
270
|
+
* long-blocking shell commands until the host job timeout fired.
|
|
271
|
+
* Default `3`. Set to `0` to disable. Closes part of #1094.
|
|
272
|
+
*/
|
|
273
|
+
maxBashTimeouts?: number;
|
|
252
274
|
}
|
|
253
275
|
|
|
254
276
|
/**
|
package/dist/index.js
CHANGED
|
@@ -7673,7 +7673,7 @@ function createMoltNetTools(config) {
|
|
|
7673
7673
|
const createEntry = defineTool({
|
|
7674
7674
|
name: "moltnet_create_entry",
|
|
7675
7675
|
label: "Create MoltNet Diary Entry",
|
|
7676
|
-
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.",
|
|
7676
|
+
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.",
|
|
7677
7677
|
parameters: Type.Object({
|
|
7678
7678
|
title: Type.String({ description: "Entry title (concise, descriptive)" }),
|
|
7679
7679
|
content: Type.String({ description: "Entry content (markdown)" }),
|
|
@@ -8253,38 +8253,48 @@ async function resumeVm(config) {
|
|
|
8253
8253
|
[GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
|
|
8254
8254
|
} }
|
|
8255
8255
|
});
|
|
8256
|
-
|
|
8256
|
+
try {
|
|
8257
|
+
await vm.exec(`sh -c '
|
|
8257
8258
|
cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
|
|
8258
8259
|
update-ca-certificates 2>/dev/null
|
|
8259
8260
|
cat /etc/gondolin/mitm/ca.crt >> /etc/ssl/certs/ca-certificates.crt
|
|
8260
8261
|
'`);
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8262
|
+
await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
|
|
8263
|
+
await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
|
|
8264
|
+
for (const [i, cmd] of (config.sandboxConfig?.resumeCommands ?? []).entries()) await vmRun(vm, `resumeCommands[${i}]`, cmd);
|
|
8265
|
+
const vmSshDir = `${vmAgentDir}/ssh`;
|
|
8266
|
+
await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
|
|
8267
|
+
if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
|
|
8268
|
+
const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
|
|
8269
|
+
await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
|
|
8270
|
+
await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
|
|
8271
|
+
if (creds.gitconfig) {
|
|
8272
|
+
const vmSigningKey = `${vmSshDir}/id_ed25519`;
|
|
8273
|
+
let vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
|
|
8274
|
+
vmGitconfig = ensureRelativeWorktreePaths(vmGitconfig);
|
|
8275
|
+
await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
|
|
8276
|
+
}
|
|
8277
|
+
if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
|
|
8278
|
+
if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
|
|
8279
|
+
if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
|
|
8280
|
+
if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
|
|
8281
|
+
await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
|
|
8282
|
+
return {
|
|
8283
|
+
vm,
|
|
8284
|
+
credentials: creds,
|
|
8285
|
+
mountPath: config.mountPath,
|
|
8286
|
+
guestWorkspace: GUEST_WORKSPACE$2,
|
|
8287
|
+
agentDir
|
|
8288
|
+
};
|
|
8289
|
+
} catch (err) {
|
|
8290
|
+
try {
|
|
8291
|
+
await vm.close();
|
|
8292
|
+
} catch (closeErr) {
|
|
8293
|
+
const m = closeErr instanceof Error ? closeErr.message : String(closeErr);
|
|
8294
|
+
process.stderr.write(`[vm] post-throw vm.close() failed: ${m}\n`);
|
|
8295
|
+
}
|
|
8296
|
+
throw err;
|
|
8297
|
+
}
|
|
8288
8298
|
}
|
|
8289
8299
|
/**
|
|
8290
8300
|
* Rewrite host-absolute paths inside moltnet.json to VM-local equivalents.
|
|
@@ -14706,8 +14716,8 @@ function buildRuntimeInstructor(ctx) {
|
|
|
14706
14716
|
"## Diary discipline",
|
|
14707
14717
|
"",
|
|
14708
14718
|
`- During this task, every diary entry MUST land in \`${ctx.diaryId}\``,
|
|
14709
|
-
" (the task diary). The
|
|
14710
|
-
" and rejects mismatched explicit `diaryId` parameters.",
|
|
14719
|
+
" (the task diary). The `moltnet_create_entry` custom tool enforces",
|
|
14720
|
+
" this and rejects mismatched explicit `diaryId` parameters.",
|
|
14711
14721
|
`- Provenance tags \`task:id:${ctx.taskId}\`, \`task:type:${ctx.taskType}\`,`,
|
|
14712
14722
|
` and \`task:attempt:${ctx.attemptN}\`${ctx.correlationId ? `, plus \`task:correlation:${ctx.correlationId}\`` : ""} are auto-injected on every entry.`,
|
|
14713
14723
|
" These share the `task:` namespace so `moltnet_diary_tags` with",
|
|
@@ -14715,12 +14725,23 @@ function buildRuntimeInstructor(ctx) {
|
|
|
14715
14725
|
" `taskFilter` shorthand on `moltnet_list_entries` /",
|
|
14716
14726
|
" `moltnet_search_entries` expands into them. You may add additional",
|
|
14717
14727
|
" tags but you cannot remove the auto-injected ones.",
|
|
14728
|
+
"- **DO NOT shell out to `moltnet entry create` / `moltnet entry",
|
|
14729
|
+
" create-signed` / any other `moltnet entry` subcommand via bash.**",
|
|
14730
|
+
" Those CLI paths hit the REST API directly and bypass the",
|
|
14731
|
+
" custom tool's task-tag auto-injection, leaving you with",
|
|
14732
|
+
" untagged entries that `moltnet_list_entries` with a",
|
|
14733
|
+
" `taskFilter: { taskId: ... }` cannot find. The legreffier skill",
|
|
14734
|
+
" recommends `moltnet entry *` for normal interactive sessions —",
|
|
14735
|
+
" inside a running task that advice does not apply. Use the",
|
|
14736
|
+
" `moltnet_create_entry` custom tool only.",
|
|
14718
14737
|
"",
|
|
14719
14738
|
"## Accountable commits",
|
|
14720
14739
|
"",
|
|
14721
14740
|
"- Every commit you make during this task MUST be paired with a signed",
|
|
14722
|
-
" diary entry created via `moltnet_create_entry
|
|
14723
|
-
" entry
|
|
14741
|
+
" diary entry created via the `moltnet_create_entry` custom tool",
|
|
14742
|
+
" (NOT via `moltnet entry create-signed` from bash — see Diary",
|
|
14743
|
+
" discipline above). Embed the returned entry id in the commit",
|
|
14744
|
+
" trailer `MoltNet-Diary: <id>`.",
|
|
14724
14745
|
"- Commits must be signed with the agent credentials (gitconfig is",
|
|
14725
14746
|
" pre-configured). Do not bypass signing.",
|
|
14726
14747
|
"",
|
|
@@ -15417,6 +15438,11 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15417
15438
|
let assistantText = "";
|
|
15418
15439
|
let reporterError = null;
|
|
15419
15440
|
const usage = finalUsage;
|
|
15441
|
+
let capAbort = null;
|
|
15442
|
+
let toolUseTurnCount = 0;
|
|
15443
|
+
let bashTimeoutCount = 0;
|
|
15444
|
+
const maxTurns = opts.maxTurns ?? 0;
|
|
15445
|
+
const maxBashTimeouts = opts.maxBashTimeouts ?? 3;
|
|
15420
15446
|
cancelListener = wireSessionAbort(reporter.cancelSignal, session);
|
|
15421
15447
|
const recordingPromise = [];
|
|
15422
15448
|
const track = (p) => {
|
|
@@ -15431,6 +15457,23 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15431
15457
|
}
|
|
15432
15458
|
}));
|
|
15433
15459
|
};
|
|
15460
|
+
const liveSession = session;
|
|
15461
|
+
const triggerCapAbort = (code, message) => {
|
|
15462
|
+
if (capAbort) return;
|
|
15463
|
+
capAbort = {
|
|
15464
|
+
code,
|
|
15465
|
+
message
|
|
15466
|
+
};
|
|
15467
|
+
liveSession.abort().catch((err) => {
|
|
15468
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
15469
|
+
process.stderr.write(`[cap] session.abort() failed: ${m}\n`);
|
|
15470
|
+
});
|
|
15471
|
+
track(emit("info", {
|
|
15472
|
+
event: "cap_abort",
|
|
15473
|
+
code,
|
|
15474
|
+
message
|
|
15475
|
+
}));
|
|
15476
|
+
};
|
|
15434
15477
|
session.subscribe((event) => {
|
|
15435
15478
|
if (event.type === "message_update") {
|
|
15436
15479
|
const ae = event.assistantMessageEvent;
|
|
@@ -15439,12 +15482,17 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15439
15482
|
track(emit("text_delta", { delta: ae.delta }));
|
|
15440
15483
|
}
|
|
15441
15484
|
} else if (event.type === "tool_execution_start") track(emit("tool_call_start", { tool_name: event.toolName }));
|
|
15442
|
-
else if (event.type === "tool_execution_end")
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15485
|
+
else if (event.type === "tool_execution_end") {
|
|
15486
|
+
track(emit("tool_call_end", {
|
|
15487
|
+
tool_name: event.toolName,
|
|
15488
|
+
is_error: event.isError,
|
|
15489
|
+
result: event.isError ? truncateForWire(event.result) : void 0
|
|
15490
|
+
}));
|
|
15491
|
+
if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
|
|
15492
|
+
bashTimeoutCount += 1;
|
|
15493
|
+
if (bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
|
|
15494
|
+
}
|
|
15495
|
+
} else if (event.type === "turn_end") {
|
|
15448
15496
|
const msg = event.message;
|
|
15449
15497
|
if (msg?.role === "assistant" && msg.usage) {
|
|
15450
15498
|
usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
|
|
@@ -15454,7 +15502,12 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15454
15502
|
if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
|
|
15455
15503
|
if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
|
|
15456
15504
|
}
|
|
15457
|
-
|
|
15505
|
+
const stopReason = msg?.stopReason ?? "end_turn";
|
|
15506
|
+
track(emit("turn_end", { stop_reason: stopReason }));
|
|
15507
|
+
if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
|
|
15508
|
+
toolUseTurnCount += 1;
|
|
15509
|
+
if (toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
|
|
15510
|
+
}
|
|
15458
15511
|
llmAbort = msg?.stopReason === "error";
|
|
15459
15512
|
if (msg?.stopReason === "error") llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
|
|
15460
15513
|
else llmErrorMessage = null;
|
|
@@ -15483,7 +15536,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15483
15536
|
let parsedOutput = null;
|
|
15484
15537
|
let parsedOutputCid = null;
|
|
15485
15538
|
let parseError = null;
|
|
15486
|
-
if (!runError && !llmAbort && !cancelled) {
|
|
15539
|
+
if (!runError && !llmAbort && !cancelled && !capAbort) {
|
|
15487
15540
|
const captured = submitToolHandle?.getCaptured() ?? null;
|
|
15488
15541
|
if (captured) try {
|
|
15489
15542
|
parsedOutput = captured;
|
|
@@ -15536,6 +15589,21 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15536
15589
|
retryable: false
|
|
15537
15590
|
}
|
|
15538
15591
|
};
|
|
15592
|
+
const capAbortSnapshot = capAbort;
|
|
15593
|
+
if (capAbortSnapshot) return {
|
|
15594
|
+
taskId: task.id,
|
|
15595
|
+
attemptN,
|
|
15596
|
+
status: "failed",
|
|
15597
|
+
output: null,
|
|
15598
|
+
outputCid: null,
|
|
15599
|
+
usage,
|
|
15600
|
+
durationMs: Date.now() - startTime,
|
|
15601
|
+
error: {
|
|
15602
|
+
code: capAbortSnapshot.code,
|
|
15603
|
+
message: capAbortSnapshot.message,
|
|
15604
|
+
retryable: false
|
|
15605
|
+
}
|
|
15606
|
+
};
|
|
15539
15607
|
const status = runError || llmAbort || parseError || reporterError ? "failed" : "completed";
|
|
15540
15608
|
const errorCode = runError?.code ?? parseError?.code ?? reporterError?.code ?? (llmAbort ? "llm_api_error" : void 0);
|
|
15541
15609
|
const errorMessage = runError?.message ?? parseError?.message ?? reporterError?.message ?? (llmAbort ? llmErrorMessage ?? "LLM API error during turn" : void 0);
|
|
@@ -15637,6 +15705,25 @@ function summarizePayloadForLog(kind, payload) {
|
|
|
15637
15705
|
default: return payload;
|
|
15638
15706
|
}
|
|
15639
15707
|
}
|
|
15708
|
+
/**
|
|
15709
|
+
* Detect pi's bash-timeout error wrapper in a `tool_execution_end`
|
|
15710
|
+
* result. The bash tool surfaces a timeout as a structured tool result
|
|
15711
|
+
* `{ content: [{ type: 'text', text: '… Command timed out after N
|
|
15712
|
+
* seconds' }] }` (see `@earendil-works/pi-coding-agent`'s bash.js).
|
|
15713
|
+
* Substring-match against the stable wrapper string is the only
|
|
15714
|
+
* mechanism short of patching pi; the string is part of pi's external
|
|
15715
|
+
* tool-error API and changing it would break agents that read tool
|
|
15716
|
+
* errors.
|
|
15717
|
+
*/
|
|
15718
|
+
function isBashTimeoutResult(result) {
|
|
15719
|
+
if (result === null || result === void 0) return false;
|
|
15720
|
+
if (typeof result === "string") return result.includes("Command timed out after");
|
|
15721
|
+
if (typeof result !== "object") return false;
|
|
15722
|
+
const content = result.content;
|
|
15723
|
+
if (!Array.isArray(content)) return false;
|
|
15724
|
+
for (const part of content) if (typeof part === "object" && part !== null && typeof part.text === "string" && part.text.includes("Command timed out after")) return true;
|
|
15725
|
+
return false;
|
|
15726
|
+
}
|
|
15640
15727
|
var TRUNCATE_LIMIT = 4 * 1024;
|
|
15641
15728
|
function truncateForWire(value) {
|
|
15642
15729
|
if (value === null || value === void 0) return value;
|
package/package.json
CHANGED