@themoltnet/agent-daemon 0.5.3 → 0.6.1
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/README.md +19 -0
- package/dist/main.js +221 -64
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -66,6 +66,25 @@ To use an alternate auth-file path: `PI_AUTH_PATH=/abs/path/to/auth.json`.
|
|
|
66
66
|
| `MOLTNET_OTEL_ENDPOINT` | unset | OTLP traces endpoint. Empty = disabled. |
|
|
67
67
|
| `LOG_LEVEL` | `info` | Pino log level override. |
|
|
68
68
|
|
|
69
|
+
### Host command auto-approval
|
|
70
|
+
|
|
71
|
+
The daemon reads `sandbox.json` through `--sandbox` or by searching up from the
|
|
72
|
+
current directory. Configure host-side auto-approval there, not in task data:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"hostExec": {
|
|
77
|
+
"autoApprove": [
|
|
78
|
+
{ "argsPrefix": ["push"], "executable": "git" },
|
|
79
|
+
{ "argsPrefix": ["pr", "create"], "executable": "gh" }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Set `"autoApprove": true` only for isolated hosts where every built-in
|
|
86
|
+
host-exec command is safe to run without a dialog.
|
|
87
|
+
|
|
69
88
|
## Correlation anchors
|
|
70
89
|
|
|
71
90
|
When a `fulfill_brief` task carries a non-null `correlationId`, the daemon
|
package/dist/main.js
CHANGED
|
@@ -3,7 +3,7 @@ import crypto, { createHash } from "crypto";
|
|
|
3
3
|
import { parseArgs, parseEnv, promisify } from "node:util";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
5
5
|
import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
6
|
-
import { pino } from "pino";
|
|
6
|
+
import { pino, transport } from "pino";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { createHash as createHash$1 } from "node:crypto";
|
|
9
9
|
import path, { dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -12,6 +12,7 @@ import { execFile, execFileSync } from "node:child_process";
|
|
|
12
12
|
import { DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createEditToolDefinition, createReadToolDefinition, createSyntheticSourceInfo, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type, getModel } from "@earendil-works/pi-ai";
|
|
14
14
|
import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
|
|
15
|
+
import { once } from "node:events";
|
|
15
16
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
16
17
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
17
18
|
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
@@ -5990,6 +5991,14 @@ var COMMON_OPTIONAL_FLAGS = `\
|
|
|
5990
5991
|
--heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
|
|
5991
5992
|
--max-batch-size <n> Reporter message batch size. Default: 50.
|
|
5992
5993
|
--flush-interval-ms <n> Reporter flush window. Default: 200.
|
|
5994
|
+
--max-turns <n> Cap on tool-use turns per attempt; over this,
|
|
5995
|
+
the pi session aborts and the attempt fails
|
|
5996
|
+
with code 'max_turns_exceeded'. 0 = disabled.
|
|
5997
|
+
Default: 0. Recommended ~30 for fulfill_brief.
|
|
5998
|
+
--max-bash-timeouts <n> Cap on bash-tool timeouts per attempt; over
|
|
5999
|
+
this, the pi session aborts and the attempt
|
|
6000
|
+
fails with code 'max_bash_timeouts_exceeded'.
|
|
6001
|
+
0 = disabled. Default: 3.
|
|
5993
6002
|
--debug Verbose logging: also log successful list/claim
|
|
5994
6003
|
outcomes (candidate counts, claim attempts).`;
|
|
5995
6004
|
var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
|
|
@@ -13945,6 +13954,7 @@ function renderPhase6Markdown(pack) {
|
|
|
13945
13954
|
* These tools run on the host (not in the VM) via the MoltNet SDK,
|
|
13946
13955
|
* so agent credentials never touch the VM filesystem.
|
|
13947
13956
|
*/
|
|
13957
|
+
var DIARY_TAG_MAX_LENGTH = 128;
|
|
13948
13958
|
/**
|
|
13949
13959
|
* Baseline env keys forwarded to host-exec child processes.
|
|
13950
13960
|
* Callers can extend this set at sandbox startup via `MoltNetToolsConfig.hostExecBaseEnv`.
|
|
@@ -13973,6 +13983,19 @@ function ensureConnected(config) {
|
|
|
13973
13983
|
teamId: config.getTeamId() ?? ""
|
|
13974
13984
|
};
|
|
13975
13985
|
}
|
|
13986
|
+
function hostExecMatchesAutoApproveRule(params, rule) {
|
|
13987
|
+
if (params.executable !== rule.executable) return false;
|
|
13988
|
+
if (rule.argsExcludes?.some((arg) => params.args.includes(arg))) return false;
|
|
13989
|
+
if (rule.argsPrefix && !rule.argsPrefix.every((arg, index) => params.args[index] === arg)) return false;
|
|
13990
|
+
if (rule.argsContains && !rule.argsContains.every((arg) => params.args.includes(arg))) return false;
|
|
13991
|
+
return true;
|
|
13992
|
+
}
|
|
13993
|
+
function shouldAutoApproveHostExec(params, config) {
|
|
13994
|
+
const policy = config.autoApproveHostExec === true ? true : config.hostExecAutoApprove ?? false;
|
|
13995
|
+
if (policy === true) return true;
|
|
13996
|
+
if (!Array.isArray(policy)) return false;
|
|
13997
|
+
return policy.some((rule) => hostExecMatchesAutoApproveRule(params, rule));
|
|
13998
|
+
}
|
|
13976
13999
|
/**
|
|
13977
14000
|
* Expand the `taskFilter` shorthand on the diary list/search tools into
|
|
13978
14001
|
* the matching `task:*` provenance tags emitted by `moltnet_create_entry`
|
|
@@ -14191,14 +14214,14 @@ function createMoltNetTools(config) {
|
|
|
14191
14214
|
limit: Type.Optional(Type.Number({ description: "Max entries to return (default 10)" })),
|
|
14192
14215
|
tags: Type.Optional(Type.Array(Type.String({
|
|
14193
14216
|
minLength: 1,
|
|
14194
|
-
maxLength:
|
|
14217
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
14195
14218
|
}), {
|
|
14196
14219
|
description: "Tags filter — entry must have ALL listed tags (AND). Max 20.",
|
|
14197
14220
|
maxItems: 20
|
|
14198
14221
|
})),
|
|
14199
14222
|
excludeTags: Type.Optional(Type.Array(Type.String({
|
|
14200
14223
|
minLength: 1,
|
|
14201
|
-
maxLength:
|
|
14224
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
14202
14225
|
}), {
|
|
14203
14226
|
description: "Tags to exclude — entry must have NONE of these. Max 20.",
|
|
14204
14227
|
maxItems: 20
|
|
@@ -14291,14 +14314,14 @@ function createMoltNetTools(config) {
|
|
|
14291
14314
|
limit: Type.Optional(Type.Number({ description: "Max results (default 5)" })),
|
|
14292
14315
|
tags: Type.Optional(Type.Array(Type.String({
|
|
14293
14316
|
minLength: 1,
|
|
14294
|
-
maxLength:
|
|
14317
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
14295
14318
|
}), {
|
|
14296
14319
|
description: "Entry must have ALL listed tags (AND). Max 20.",
|
|
14297
14320
|
maxItems: 20
|
|
14298
14321
|
})),
|
|
14299
14322
|
excludeTags: Type.Optional(Type.Array(Type.String({
|
|
14300
14323
|
minLength: 1,
|
|
14301
|
-
maxLength:
|
|
14324
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
14302
14325
|
}), {
|
|
14303
14326
|
description: "Entry must have NONE of these tags. Max 20.",
|
|
14304
14327
|
maxItems: 20
|
|
@@ -14344,7 +14367,7 @@ function createMoltNetTools(config) {
|
|
|
14344
14367
|
const createEntry = defineTool({
|
|
14345
14368
|
name: "moltnet_create_entry",
|
|
14346
14369
|
label: "Create MoltNet Diary Entry",
|
|
14347
|
-
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.",
|
|
14370
|
+
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.",
|
|
14348
14371
|
parameters: Type.Object({
|
|
14349
14372
|
title: Type.String({ description: "Entry title (concise, descriptive)" }),
|
|
14350
14373
|
content: Type.String({ description: "Entry content (markdown)" }),
|
|
@@ -14484,7 +14507,7 @@ function createMoltNetTools(config) {
|
|
|
14484
14507
|
}),
|
|
14485
14508
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
14486
14509
|
if (!HOST_EXEC_ALLOWED.has(params.executable)) throw new Error(`host_exec: '${params.executable}' is not in the allowed list (${[...HOST_EXEC_ALLOWED].join(", ")}). Extend HOST_EXEC_ALLOWED only after explicit security review.`);
|
|
14487
|
-
if (ctx?.ui) {
|
|
14510
|
+
if (ctx?.ui && !shouldAutoApproveHostExec(params, config)) {
|
|
14488
14511
|
const cmdDisplay = [params.executable, ...params.args].join(" ");
|
|
14489
14512
|
if (!await ctx.ui.confirm("Allow host command?", `The agent wants to run on your machine:\n\n ${cmdDisplay}\n\nAllow?`)) throw new Error(`host_exec: user declined approval for: ${cmdDisplay}`);
|
|
14490
14513
|
}
|
|
@@ -14924,38 +14947,48 @@ async function resumeVm(config) {
|
|
|
14924
14947
|
[GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
|
|
14925
14948
|
} }
|
|
14926
14949
|
});
|
|
14927
|
-
|
|
14950
|
+
try {
|
|
14951
|
+
await vm.exec(`sh -c '
|
|
14928
14952
|
cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
|
|
14929
14953
|
update-ca-certificates 2>/dev/null
|
|
14930
14954
|
cat /etc/gondolin/mitm/ca.crt >> /etc/ssl/certs/ca-certificates.crt
|
|
14931
14955
|
'`);
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14956
|
+
await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
|
|
14957
|
+
await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
|
|
14958
|
+
for (const [i, cmd] of (config.sandboxConfig?.resumeCommands ?? []).entries()) await vmRun(vm, `resumeCommands[${i}]`, cmd);
|
|
14959
|
+
const vmSshDir = `${vmAgentDir}/ssh`;
|
|
14960
|
+
await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
|
|
14961
|
+
if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
|
|
14962
|
+
const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
|
|
14963
|
+
await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
|
|
14964
|
+
await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
|
|
14965
|
+
if (creds.gitconfig) {
|
|
14966
|
+
const vmSigningKey = `${vmSshDir}/id_ed25519`;
|
|
14967
|
+
let vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
|
|
14968
|
+
vmGitconfig = ensureRelativeWorktreePaths(vmGitconfig);
|
|
14969
|
+
await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
|
|
14970
|
+
}
|
|
14971
|
+
if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
|
|
14972
|
+
if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
|
|
14973
|
+
if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
|
|
14974
|
+
if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
|
|
14975
|
+
await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
|
|
14976
|
+
return {
|
|
14977
|
+
vm,
|
|
14978
|
+
credentials: creds,
|
|
14979
|
+
mountPath: config.mountPath,
|
|
14980
|
+
guestWorkspace: GUEST_WORKSPACE$1,
|
|
14981
|
+
agentDir
|
|
14982
|
+
};
|
|
14983
|
+
} catch (err) {
|
|
14984
|
+
try {
|
|
14985
|
+
await vm.close();
|
|
14986
|
+
} catch (closeErr) {
|
|
14987
|
+
const m = closeErr instanceof Error ? closeErr.message : String(closeErr);
|
|
14988
|
+
process.stderr.write(`[vm] post-throw vm.close() failed: ${m}\n`);
|
|
14989
|
+
}
|
|
14990
|
+
throw err;
|
|
14991
|
+
}
|
|
14959
14992
|
}
|
|
14960
14993
|
/**
|
|
14961
14994
|
* Rewrite host-absolute paths inside moltnet.json to VM-local equivalents.
|
|
@@ -15464,8 +15497,8 @@ function buildRuntimeInstructor(ctx) {
|
|
|
15464
15497
|
"## Diary discipline",
|
|
15465
15498
|
"",
|
|
15466
15499
|
`- During this task, every diary entry MUST land in \`${ctx.diaryId}\``,
|
|
15467
|
-
" (the task diary). The
|
|
15468
|
-
" and rejects mismatched explicit `diaryId` parameters.",
|
|
15500
|
+
" (the task diary). The `moltnet_create_entry` custom tool enforces",
|
|
15501
|
+
" this and rejects mismatched explicit `diaryId` parameters.",
|
|
15469
15502
|
`- Provenance tags \`task:id:${ctx.taskId}\`, \`task:type:${ctx.taskType}\`,`,
|
|
15470
15503
|
` and \`task:attempt:${ctx.attemptN}\`${ctx.correlationId ? `, plus \`task:correlation:${ctx.correlationId}\`` : ""} are auto-injected on every entry.`,
|
|
15471
15504
|
" These share the `task:` namespace so `moltnet_diary_tags` with",
|
|
@@ -15473,12 +15506,23 @@ function buildRuntimeInstructor(ctx) {
|
|
|
15473
15506
|
" `taskFilter` shorthand on `moltnet_list_entries` /",
|
|
15474
15507
|
" `moltnet_search_entries` expands into them. You may add additional",
|
|
15475
15508
|
" tags but you cannot remove the auto-injected ones.",
|
|
15509
|
+
"- **DO NOT shell out to `moltnet entry create` / `moltnet entry",
|
|
15510
|
+
" create-signed` / any other `moltnet entry` subcommand via bash.**",
|
|
15511
|
+
" Those CLI paths hit the REST API directly and bypass the",
|
|
15512
|
+
" custom tool's task-tag auto-injection, leaving you with",
|
|
15513
|
+
" untagged entries that `moltnet_list_entries` with a",
|
|
15514
|
+
" `taskFilter: { taskId: ... }` cannot find. The legreffier skill",
|
|
15515
|
+
" recommends `moltnet entry *` for normal interactive sessions —",
|
|
15516
|
+
" inside a running task that advice does not apply. Use the",
|
|
15517
|
+
" `moltnet_create_entry` custom tool only.",
|
|
15476
15518
|
"",
|
|
15477
15519
|
"## Accountable commits",
|
|
15478
15520
|
"",
|
|
15479
15521
|
"- Every commit you make during this task MUST be paired with a signed",
|
|
15480
|
-
" diary entry created via `moltnet_create_entry
|
|
15481
|
-
" entry
|
|
15522
|
+
" diary entry created via the `moltnet_create_entry` custom tool",
|
|
15523
|
+
" (NOT via `moltnet entry create-signed` from bash — see Diary",
|
|
15524
|
+
" discipline above). Embed the returned entry id in the commit",
|
|
15525
|
+
" trailer `MoltNet-Diary: <id>`.",
|
|
15482
15526
|
"- Commits must be signed with the agent credentials (gitconfig is",
|
|
15483
15527
|
" pre-configured). Do not bypass signing.",
|
|
15484
15528
|
"",
|
|
@@ -16103,6 +16147,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16103
16147
|
clearSessionErrors: () => {},
|
|
16104
16148
|
getHostCwd: () => mountPath,
|
|
16105
16149
|
hostExecBaseEnv: new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(managed.credentials.agentEnv)]),
|
|
16150
|
+
hostExecAutoApprove: opts.hostExecAutoApprove ?? opts.sandboxConfig?.hostExec?.autoApprove ?? false,
|
|
16106
16151
|
getTaskContext: () => ({
|
|
16107
16152
|
taskId: task.id,
|
|
16108
16153
|
taskType: task.taskType,
|
|
@@ -16175,6 +16220,11 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16175
16220
|
let assistantText = "";
|
|
16176
16221
|
let reporterError = null;
|
|
16177
16222
|
const usage = finalUsage;
|
|
16223
|
+
let capAbort = null;
|
|
16224
|
+
let toolUseTurnCount = 0;
|
|
16225
|
+
let bashTimeoutCount = 0;
|
|
16226
|
+
const maxTurns = opts.maxTurns ?? 0;
|
|
16227
|
+
const maxBashTimeouts = opts.maxBashTimeouts ?? 3;
|
|
16178
16228
|
cancelListener = wireSessionAbort(reporter.cancelSignal, session);
|
|
16179
16229
|
const recordingPromise = [];
|
|
16180
16230
|
const track = (p) => {
|
|
@@ -16189,6 +16239,23 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16189
16239
|
}
|
|
16190
16240
|
}));
|
|
16191
16241
|
};
|
|
16242
|
+
const liveSession = session;
|
|
16243
|
+
const triggerCapAbort = (code, message) => {
|
|
16244
|
+
if (capAbort) return;
|
|
16245
|
+
capAbort = {
|
|
16246
|
+
code,
|
|
16247
|
+
message
|
|
16248
|
+
};
|
|
16249
|
+
liveSession.abort().catch((err) => {
|
|
16250
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
16251
|
+
process.stderr.write(`[cap] session.abort() failed: ${m}\n`);
|
|
16252
|
+
});
|
|
16253
|
+
track(emit("info", {
|
|
16254
|
+
event: "cap_abort",
|
|
16255
|
+
code,
|
|
16256
|
+
message
|
|
16257
|
+
}));
|
|
16258
|
+
};
|
|
16192
16259
|
session.subscribe((event) => {
|
|
16193
16260
|
if (event.type === "message_update") {
|
|
16194
16261
|
const ae = event.assistantMessageEvent;
|
|
@@ -16197,12 +16264,17 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16197
16264
|
track(emit("text_delta", { delta: ae.delta }));
|
|
16198
16265
|
}
|
|
16199
16266
|
} else if (event.type === "tool_execution_start") track(emit("tool_call_start", { tool_name: event.toolName }));
|
|
16200
|
-
else if (event.type === "tool_execution_end")
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
|
|
16205
|
-
|
|
16267
|
+
else if (event.type === "tool_execution_end") {
|
|
16268
|
+
track(emit("tool_call_end", {
|
|
16269
|
+
tool_name: event.toolName,
|
|
16270
|
+
is_error: event.isError,
|
|
16271
|
+
result: event.isError ? truncateForWire(event.result) : void 0
|
|
16272
|
+
}));
|
|
16273
|
+
if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
|
|
16274
|
+
bashTimeoutCount += 1;
|
|
16275
|
+
if (bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
|
|
16276
|
+
}
|
|
16277
|
+
} else if (event.type === "turn_end") {
|
|
16206
16278
|
const msg = event.message;
|
|
16207
16279
|
if (msg?.role === "assistant" && msg.usage) {
|
|
16208
16280
|
usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
|
|
@@ -16212,7 +16284,12 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16212
16284
|
if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
|
|
16213
16285
|
if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
|
|
16214
16286
|
}
|
|
16215
|
-
|
|
16287
|
+
const stopReason = msg?.stopReason ?? "end_turn";
|
|
16288
|
+
track(emit("turn_end", { stop_reason: stopReason }));
|
|
16289
|
+
if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
|
|
16290
|
+
toolUseTurnCount += 1;
|
|
16291
|
+
if (toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
|
|
16292
|
+
}
|
|
16216
16293
|
llmAbort = msg?.stopReason === "error";
|
|
16217
16294
|
if (msg?.stopReason === "error") llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
|
|
16218
16295
|
else llmErrorMessage = null;
|
|
@@ -16241,7 +16318,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16241
16318
|
let parsedOutput = null;
|
|
16242
16319
|
let parsedOutputCid = null;
|
|
16243
16320
|
let parseError = null;
|
|
16244
|
-
if (!runError && !llmAbort && !cancelled) {
|
|
16321
|
+
if (!runError && !llmAbort && !cancelled && !capAbort) {
|
|
16245
16322
|
const captured = submitToolHandle?.getCaptured() ?? null;
|
|
16246
16323
|
if (captured) try {
|
|
16247
16324
|
parsedOutput = captured;
|
|
@@ -16294,6 +16371,21 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16294
16371
|
retryable: false
|
|
16295
16372
|
}
|
|
16296
16373
|
};
|
|
16374
|
+
const capAbortSnapshot = capAbort;
|
|
16375
|
+
if (capAbortSnapshot) return {
|
|
16376
|
+
taskId: task.id,
|
|
16377
|
+
attemptN,
|
|
16378
|
+
status: "failed",
|
|
16379
|
+
output: null,
|
|
16380
|
+
outputCid: null,
|
|
16381
|
+
usage,
|
|
16382
|
+
durationMs: Date.now() - startTime,
|
|
16383
|
+
error: {
|
|
16384
|
+
code: capAbortSnapshot.code,
|
|
16385
|
+
message: capAbortSnapshot.message,
|
|
16386
|
+
retryable: false
|
|
16387
|
+
}
|
|
16388
|
+
};
|
|
16297
16389
|
const status = runError || llmAbort || parseError || reporterError ? "failed" : "completed";
|
|
16298
16390
|
const errorCode = runError?.code ?? parseError?.code ?? reporterError?.code ?? (llmAbort ? "llm_api_error" : void 0);
|
|
16299
16391
|
const errorMessage = runError?.message ?? parseError?.message ?? reporterError?.message ?? (llmAbort ? llmErrorMessage ?? "LLM API error during turn" : void 0);
|
|
@@ -16395,6 +16487,25 @@ function summarizePayloadForLog(kind, payload) {
|
|
|
16395
16487
|
default: return payload;
|
|
16396
16488
|
}
|
|
16397
16489
|
}
|
|
16490
|
+
/**
|
|
16491
|
+
* Detect pi's bash-timeout error wrapper in a `tool_execution_end`
|
|
16492
|
+
* result. The bash tool surfaces a timeout as a structured tool result
|
|
16493
|
+
* `{ content: [{ type: 'text', text: '… Command timed out after N
|
|
16494
|
+
* seconds' }] }` (see `@earendil-works/pi-coding-agent`'s bash.js).
|
|
16495
|
+
* Substring-match against the stable wrapper string is the only
|
|
16496
|
+
* mechanism short of patching pi; the string is part of pi's external
|
|
16497
|
+
* tool-error API and changing it would break agents that read tool
|
|
16498
|
+
* errors.
|
|
16499
|
+
*/
|
|
16500
|
+
function isBashTimeoutResult(result) {
|
|
16501
|
+
if (result === null || result === void 0) return false;
|
|
16502
|
+
if (typeof result === "string") return result.includes("Command timed out after");
|
|
16503
|
+
if (typeof result !== "object") return false;
|
|
16504
|
+
const content = result.content;
|
|
16505
|
+
if (!Array.isArray(content)) return false;
|
|
16506
|
+
for (const part of content) if (typeof part === "object" && part !== null && typeof part.text === "string" && part.text.includes("Command timed out after")) return true;
|
|
16507
|
+
return false;
|
|
16508
|
+
}
|
|
16398
16509
|
var TRUNCATE_LIMIT = 4 * 1024;
|
|
16399
16510
|
function truncateForWire(value) {
|
|
16400
16511
|
if (value === null || value === void 0) return value;
|
|
@@ -16533,6 +16644,7 @@ async function finalizeTask(agent, output, ctx = {}) {
|
|
|
16533
16644
|
message: "Task execution failed before producing a valid output.",
|
|
16534
16645
|
retryable: false
|
|
16535
16646
|
};
|
|
16647
|
+
if ((await agent.tasks.heartbeat(output.taskId, output.attemptN, {})).cancelled) return;
|
|
16536
16648
|
await agent.tasks.fail(output.taskId, output.attemptN, { error });
|
|
16537
16649
|
}
|
|
16538
16650
|
async function maybeWriteAnchors(output, ctx) {
|
|
@@ -16552,12 +16664,53 @@ async function maybeWriteAnchors(output, ctx) {
|
|
|
16552
16664
|
}
|
|
16553
16665
|
}
|
|
16554
16666
|
//#endregion
|
|
16667
|
+
//#region src/lib/logger.ts
|
|
16668
|
+
/**
|
|
16669
|
+
* Logger setup + teardown for the agent-daemon CLI commands.
|
|
16670
|
+
*
|
|
16671
|
+
* Why this helper exists: pino with `pino-pretty` transport spawns a
|
|
16672
|
+
* Node Worker thread for human-readable output. The Worker holds the
|
|
16673
|
+
* parent's TTYWrap handles ref'd; without an explicit `.end()` the
|
|
16674
|
+
* daemon never exits after the task completes — symptom: `once`
|
|
16675
|
+
* prints `[done] TaskOutput: …` then hangs until Ctrl-C
|
|
16676
|
+
* (see issue #1107). This module owns both creation and shutdown
|
|
16677
|
+
* so every CLI command gets the same cleanup contract without
|
|
16678
|
+
* duplicating the dance.
|
|
16679
|
+
*
|
|
16680
|
+
* Non-TTY mode (CI, file redirect) skips the transport entirely:
|
|
16681
|
+
* pino writes raw NDJSON directly to stderr, no worker thread,
|
|
16682
|
+
* nothing extra to clean up.
|
|
16683
|
+
*/
|
|
16684
|
+
function createRootLogger(options) {
|
|
16685
|
+
const prettyTransport = process.stderr.isTTY ? transport({
|
|
16686
|
+
target: "pino-pretty",
|
|
16687
|
+
options: { colorize: true }
|
|
16688
|
+
}) : null;
|
|
16689
|
+
const logger = pino(options, prettyTransport ?? void 0);
|
|
16690
|
+
const shutdown = async () => {
|
|
16691
|
+
if (!prettyTransport) return;
|
|
16692
|
+
logger.flush();
|
|
16693
|
+
try {
|
|
16694
|
+
prettyTransport.end();
|
|
16695
|
+
await once(prettyTransport, "close");
|
|
16696
|
+
} catch (err) {
|
|
16697
|
+
process.stderr.write(`[pino] transport teardown failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
|
|
16698
|
+
}
|
|
16699
|
+
};
|
|
16700
|
+
return {
|
|
16701
|
+
logger,
|
|
16702
|
+
shutdown
|
|
16703
|
+
};
|
|
16704
|
+
}
|
|
16705
|
+
//#endregion
|
|
16555
16706
|
//#region src/lib/options.ts
|
|
16556
16707
|
var DEFAULTS = {
|
|
16557
16708
|
leaseTtlSec: 300,
|
|
16558
16709
|
heartbeatIntervalMs: 6e4,
|
|
16559
16710
|
maxBatchSize: 50,
|
|
16560
|
-
flushIntervalMs: 200
|
|
16711
|
+
flushIntervalMs: 200,
|
|
16712
|
+
maxTurns: 0,
|
|
16713
|
+
maxBashTimeouts: 3
|
|
16561
16714
|
};
|
|
16562
16715
|
var MissingRequiredOptionError = class extends Error {
|
|
16563
16716
|
constructor(flag) {
|
|
@@ -16579,6 +16732,8 @@ function parseCommonOptions(args) {
|
|
|
16579
16732
|
heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", DEFAULTS.heartbeatIntervalMs),
|
|
16580
16733
|
maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", DEFAULTS.maxBatchSize),
|
|
16581
16734
|
flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
|
|
16735
|
+
maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
|
|
16736
|
+
maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
|
|
16582
16737
|
debug: args.debug === true
|
|
16583
16738
|
};
|
|
16584
16739
|
}
|
|
@@ -16612,6 +16767,8 @@ function commonOptionDefs() {
|
|
|
16612
16767
|
"heartbeat-interval-ms": { type: "string" },
|
|
16613
16768
|
"max-batch-size": { type: "string" },
|
|
16614
16769
|
"flush-interval-ms": { type: "string" },
|
|
16770
|
+
"max-turns": { type: "string" },
|
|
16771
|
+
"max-bash-timeouts": { type: "string" },
|
|
16615
16772
|
debug: { type: "boolean" }
|
|
16616
16773
|
};
|
|
16617
16774
|
}
|
|
@@ -16778,14 +16935,11 @@ async function runPolling(opts) {
|
|
|
16778
16935
|
"moltnet.llm.model": common.model
|
|
16779
16936
|
}
|
|
16780
16937
|
});
|
|
16781
|
-
const
|
|
16938
|
+
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
16782
16939
|
name: `agent-daemon.${opts.modeLabel}`,
|
|
16783
|
-
level: cfg.logLevel || (common.debug ? "debug" : "info")
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
options: { colorize: true }
|
|
16787
|
-
} } : {}
|
|
16788
|
-
}).child({
|
|
16940
|
+
level: cfg.logLevel || (common.debug ? "debug" : "info")
|
|
16941
|
+
});
|
|
16942
|
+
const rootLogger = logger.child({
|
|
16789
16943
|
mode: opts.modeLabel,
|
|
16790
16944
|
agent: common.agent,
|
|
16791
16945
|
teamId,
|
|
@@ -16818,7 +16972,9 @@ async function runPolling(opts) {
|
|
|
16818
16972
|
provider: common.provider,
|
|
16819
16973
|
model: common.model,
|
|
16820
16974
|
sandboxConfig: sandbox.config,
|
|
16821
|
-
makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger)
|
|
16975
|
+
makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
|
|
16976
|
+
maxTurns: common.maxTurns,
|
|
16977
|
+
maxBashTimeouts: common.maxBashTimeouts
|
|
16822
16978
|
});
|
|
16823
16979
|
runtime = new AgentRuntime({
|
|
16824
16980
|
logger: rootLogger,
|
|
@@ -16897,6 +17053,7 @@ async function runPolling(opts) {
|
|
|
16897
17053
|
return drained.some((o) => o.status !== "completed") ? 1 : 0;
|
|
16898
17054
|
} finally {
|
|
16899
17055
|
await otelShutdown();
|
|
17056
|
+
await shutdownLogger();
|
|
16900
17057
|
}
|
|
16901
17058
|
}
|
|
16902
17059
|
function parseCsv(raw) {
|
|
@@ -16968,14 +17125,11 @@ async function runOnce(argv) {
|
|
|
16968
17125
|
"moltnet.llm.model": opts.model
|
|
16969
17126
|
}
|
|
16970
17127
|
});
|
|
16971
|
-
const
|
|
17128
|
+
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
16972
17129
|
name: "agent-daemon.once",
|
|
16973
|
-
level: cfg.logLevel || (opts.debug ? "debug" : "info")
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
options: { colorize: true }
|
|
16977
|
-
} } : {}
|
|
16978
|
-
}).child({
|
|
17130
|
+
level: cfg.logLevel || (opts.debug ? "debug" : "info")
|
|
17131
|
+
});
|
|
17132
|
+
const rootLogger = logger.child({
|
|
16979
17133
|
mode: "once",
|
|
16980
17134
|
agent: opts.agent,
|
|
16981
17135
|
provider: opts.provider,
|
|
@@ -17012,7 +17166,9 @@ async function runOnce(argv) {
|
|
|
17012
17166
|
provider: opts.provider,
|
|
17013
17167
|
model: opts.model,
|
|
17014
17168
|
sandboxConfig: sandbox.config,
|
|
17015
|
-
onTurnEvent: makeTurnEventHandler(rootLogger, { taskId })
|
|
17169
|
+
onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
|
|
17170
|
+
maxTurns: opts.maxTurns,
|
|
17171
|
+
maxBashTimeouts: opts.maxBashTimeouts
|
|
17016
17172
|
});
|
|
17017
17173
|
const writeCorrelationAnchors = makePrBodyAnchorWriter({
|
|
17018
17174
|
gh: createGhCliClient(),
|
|
@@ -17049,6 +17205,7 @@ async function runOnce(argv) {
|
|
|
17049
17205
|
return output.status === "completed" ? 0 : 1;
|
|
17050
17206
|
} finally {
|
|
17051
17207
|
await otelShutdown();
|
|
17208
|
+
await shutdownLogger();
|
|
17052
17209
|
}
|
|
17053
17210
|
}
|
|
17054
17211
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
|
-
"@themoltnet/agent-runtime": "0.15.0",
|
|
37
36
|
"@themoltnet/sdk": "0.102.0",
|
|
38
|
-
"@themoltnet/pi-extension": "0.
|
|
37
|
+
"@themoltnet/pi-extension": "0.16.1",
|
|
38
|
+
"@themoltnet/agent-runtime": "0.15.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsx": "^4.7.0",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vite": "^8.0.0",
|
|
44
44
|
"vitest": "^3.0.0",
|
|
45
45
|
"@moltnet/crypto-service": "0.1.0",
|
|
46
|
+
"@moltnet/tasks": "0.1.0",
|
|
46
47
|
"@moltnet/database": "0.1.0",
|
|
47
|
-
"@moltnet/bootstrap": "0.1.0"
|
|
48
|
-
"@moltnet/tasks": "0.1.0"
|
|
48
|
+
"@moltnet/bootstrap": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"nx": {
|
|
51
51
|
"tags": [
|