@themoltnet/agent-daemon 0.28.0 → 0.29.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/main.js +229 -5
- package/package.json +6 -5
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import crypto from "crypto";
|
|
|
10
10
|
import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
11
11
|
import { parseArgs, promisify } from "node:util";
|
|
12
12
|
import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } from "@themoltnet/agent-runtime";
|
|
13
|
-
import { createPiTaskExecutor, findMainWorktree } from "@themoltnet/pi-extension";
|
|
13
|
+
import { createPiRetryTriage, createPiTaskExecutor, findMainWorktree, normalizeRetryTriageResult, redactRetryTriageSecrets } from "@themoltnet/pi-extension";
|
|
14
14
|
import { execFile, execFileSync } from "node:child_process";
|
|
15
15
|
import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
16
16
|
import { MoltNetError, connect } from "@themoltnet/sdk";
|
|
@@ -22,6 +22,7 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
|
22
22
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
23
23
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
24
24
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
25
|
+
import { getModel } from "@earendil-works/pi-ai";
|
|
25
26
|
import { mkdir, realpath, stat } from "node:fs/promises";
|
|
26
27
|
import { pipeline } from "node:stream/promises";
|
|
27
28
|
import { createHash } from "node:crypto";
|
|
@@ -9217,6 +9218,27 @@ var TaskUsage = _Object_({
|
|
|
9217
9218
|
$id: "TaskUsage",
|
|
9218
9219
|
additionalProperties: false
|
|
9219
9220
|
});
|
|
9221
|
+
var TaskRetryDecision = Union([Literal("retry"), Literal("do_not_retry")]);
|
|
9222
|
+
var TaskRetryConfidence = Union([
|
|
9223
|
+
Literal("low"),
|
|
9224
|
+
Literal("medium"),
|
|
9225
|
+
Literal("high")
|
|
9226
|
+
]);
|
|
9227
|
+
var TaskRetryInfo = _Object_({
|
|
9228
|
+
source: Union([
|
|
9229
|
+
Literal("explicit"),
|
|
9230
|
+
Literal("deterministic"),
|
|
9231
|
+
Literal("attempts_exhausted"),
|
|
9232
|
+
Literal("triage"),
|
|
9233
|
+
Literal("triage_failed")
|
|
9234
|
+
]),
|
|
9235
|
+
decision: Optional(TaskRetryDecision),
|
|
9236
|
+
confidence: Optional(TaskRetryConfidence),
|
|
9237
|
+
reason: Optional(String$1())
|
|
9238
|
+
}, {
|
|
9239
|
+
$id: "TaskRetryInfo",
|
|
9240
|
+
additionalProperties: false
|
|
9241
|
+
});
|
|
9220
9242
|
/**
|
|
9221
9243
|
* Structured error returned from a failed attempt.
|
|
9222
9244
|
*/
|
|
@@ -9224,7 +9246,8 @@ var TaskError = _Object_({
|
|
|
9224
9246
|
code: String$1(),
|
|
9225
9247
|
message: String$1(),
|
|
9226
9248
|
stack: Optional(String$1()),
|
|
9227
|
-
retryable: Optional(Boolean$1())
|
|
9249
|
+
retryable: Optional(Boolean$1()),
|
|
9250
|
+
retry: Optional(TaskRetryInfo)
|
|
9228
9251
|
}, {
|
|
9229
9252
|
$id: "TaskError",
|
|
9230
9253
|
additionalProperties: false
|
|
@@ -10010,6 +10033,157 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
|
|
|
10010
10033
|
return existsSync(fallback) ? fallback : null;
|
|
10011
10034
|
}
|
|
10012
10035
|
//#endregion
|
|
10036
|
+
//#region src/lib/retry-triage.ts
|
|
10037
|
+
var RETRYABLE_CODES = new Set([
|
|
10038
|
+
"checkpoint_upload_failed",
|
|
10039
|
+
"complete_call_failed",
|
|
10040
|
+
"daemon_abort",
|
|
10041
|
+
"dispatch_expired",
|
|
10042
|
+
"lease_expired",
|
|
10043
|
+
"llm_api_error",
|
|
10044
|
+
"runtime_session_checkpoint_failed",
|
|
10045
|
+
"session_prompt_failed"
|
|
10046
|
+
]);
|
|
10047
|
+
var NON_RETRYABLE_CODES = new Set([
|
|
10048
|
+
"bad_api_key",
|
|
10049
|
+
"invalid_api_key",
|
|
10050
|
+
"invalid_model",
|
|
10051
|
+
"output_rejected_by_server",
|
|
10052
|
+
"output_validation_failed",
|
|
10053
|
+
"producer_context_missing",
|
|
10054
|
+
"running_max_bash_timeouts_exceeded",
|
|
10055
|
+
"running_max_turns_exceeded",
|
|
10056
|
+
"task_cancelled",
|
|
10057
|
+
"unknown_task_type"
|
|
10058
|
+
]);
|
|
10059
|
+
var RETRYABLE_MESSAGE_PATTERNS = [
|
|
10060
|
+
/\b429\b/i,
|
|
10061
|
+
/\b5(?:02|03|04)\b/i,
|
|
10062
|
+
/\btimeout\b/i,
|
|
10063
|
+
/\btimed out\b/i,
|
|
10064
|
+
/\brate limit/i,
|
|
10065
|
+
/\btemporar(?:y|ily)\b/i,
|
|
10066
|
+
/\bunavailable\b/i,
|
|
10067
|
+
/\bECONNRESET\b/i,
|
|
10068
|
+
/\bECONNREFUSED\b/i,
|
|
10069
|
+
/\bETIMEDOUT\b/i,
|
|
10070
|
+
/\bENOTFOUND\b/i,
|
|
10071
|
+
/\bEAI_AGAIN\b/i,
|
|
10072
|
+
/\bDNS\b/i
|
|
10073
|
+
];
|
|
10074
|
+
var NON_RETRYABLE_MESSAGE_PATTERNS = [
|
|
10075
|
+
/\b401\b/i,
|
|
10076
|
+
/\b403\b/i,
|
|
10077
|
+
/\bunauthori[sz]ed\b/i,
|
|
10078
|
+
/\bforbidden\b/i,
|
|
10079
|
+
/\binvalid (?:api )?key\b/i,
|
|
10080
|
+
/\bmissing credentials?\b/i,
|
|
10081
|
+
/\bmodel .*not (?:found|registered|available)\b/i,
|
|
10082
|
+
/\bunknown task type\b/i,
|
|
10083
|
+
/\bvalidation failed\b/i,
|
|
10084
|
+
/\bcancelled\b/i,
|
|
10085
|
+
/\bmax (?:turn|bash)/i
|
|
10086
|
+
];
|
|
10087
|
+
async function classifyAttemptFailure(input) {
|
|
10088
|
+
if (input.remainingAttempts !== null && input.remainingAttempts !== void 0) {
|
|
10089
|
+
if (input.remainingAttempts <= 0) return {
|
|
10090
|
+
error: withRetryInfo(input.error, {
|
|
10091
|
+
retryable: false,
|
|
10092
|
+
source: "attempts_exhausted",
|
|
10093
|
+
reason: `Attempt budget exhausted at attempt ${input.attemptN}${input.maxAttempts ? ` of ${input.maxAttempts}` : ""}.`
|
|
10094
|
+
}),
|
|
10095
|
+
source: "attempts_exhausted"
|
|
10096
|
+
};
|
|
10097
|
+
}
|
|
10098
|
+
const deterministic = classifyDeterministically(input.error);
|
|
10099
|
+
if (deterministic !== "ambiguous") {
|
|
10100
|
+
const retryable = deterministic === "retryable";
|
|
10101
|
+
const source = input.error.retryable === retryable ? "explicit" : "deterministic";
|
|
10102
|
+
return {
|
|
10103
|
+
error: withRetryInfo(input.error, {
|
|
10104
|
+
retryable,
|
|
10105
|
+
source,
|
|
10106
|
+
decision: retryable ? "retry" : "do_not_retry",
|
|
10107
|
+
confidence: "high",
|
|
10108
|
+
reason: retryable ? "Matched deterministic retry policy." : "Matched deterministic no-retry policy."
|
|
10109
|
+
}),
|
|
10110
|
+
source
|
|
10111
|
+
};
|
|
10112
|
+
}
|
|
10113
|
+
if (!input.triage) return {
|
|
10114
|
+
error: withRetryInfo(input.error, {
|
|
10115
|
+
retryable: false,
|
|
10116
|
+
source: "triage_failed",
|
|
10117
|
+
reason: "Failure was ambiguous and no retry triage agent was configured; defaulted to no retry."
|
|
10118
|
+
}),
|
|
10119
|
+
source: "triage_failed"
|
|
10120
|
+
};
|
|
10121
|
+
try {
|
|
10122
|
+
const triage = normalizeRetryTriageResult(await input.triage(input));
|
|
10123
|
+
const retryable = triage.decision === "retry" && (triage.confidence === "medium" || triage.confidence === "high");
|
|
10124
|
+
return {
|
|
10125
|
+
error: withRetryInfo({
|
|
10126
|
+
...input.error,
|
|
10127
|
+
message: appendTriageReason(input.error.message, triage)
|
|
10128
|
+
}, {
|
|
10129
|
+
retryable,
|
|
10130
|
+
source: "triage",
|
|
10131
|
+
decision: triage.decision,
|
|
10132
|
+
confidence: triage.confidence,
|
|
10133
|
+
reason: triage.reason
|
|
10134
|
+
}),
|
|
10135
|
+
source: "triage",
|
|
10136
|
+
triage
|
|
10137
|
+
};
|
|
10138
|
+
} catch (err) {
|
|
10139
|
+
return {
|
|
10140
|
+
error: withRetryInfo({
|
|
10141
|
+
...input.error,
|
|
10142
|
+
message: appendTriageFailure(input.error.message, err)
|
|
10143
|
+
}, {
|
|
10144
|
+
retryable: false,
|
|
10145
|
+
source: "triage_failed",
|
|
10146
|
+
reason: `Retry triage failed: ${sanitizeReason(err)}`
|
|
10147
|
+
}),
|
|
10148
|
+
source: "triage_failed"
|
|
10149
|
+
};
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
10152
|
+
function classifyDeterministically(error) {
|
|
10153
|
+
const code = error.code.toLowerCase();
|
|
10154
|
+
const message = error.message;
|
|
10155
|
+
if (NON_RETRYABLE_CODES.has(code)) return "non_retryable";
|
|
10156
|
+
if (NON_RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) return "non_retryable";
|
|
10157
|
+
if (RETRYABLE_CODES.has(code)) return "retryable";
|
|
10158
|
+
if (RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) return "retryable";
|
|
10159
|
+
if (error.retryable === true) return "retryable";
|
|
10160
|
+
return "ambiguous";
|
|
10161
|
+
}
|
|
10162
|
+
function appendTriageReason(message, triage) {
|
|
10163
|
+
const suffix = ` Retry triage: ${triage.decision}/${triage.confidence}: ${triage.reason}`;
|
|
10164
|
+
if (message.includes("Retry triage:")) return message;
|
|
10165
|
+
return `${message}${suffix}`.slice(0, 4e3);
|
|
10166
|
+
}
|
|
10167
|
+
function appendTriageFailure(message, err) {
|
|
10168
|
+
if (message.includes("Retry triage failed:")) return message;
|
|
10169
|
+
return `${message} Retry triage failed: ${sanitizeReason(err)}`.slice(0, 4e3);
|
|
10170
|
+
}
|
|
10171
|
+
function sanitizeReason(value) {
|
|
10172
|
+
return redactRetryTriageSecrets(value instanceof Error ? value.message : String(value)).slice(0, 500);
|
|
10173
|
+
}
|
|
10174
|
+
function withRetryInfo(error, info) {
|
|
10175
|
+
return {
|
|
10176
|
+
...error,
|
|
10177
|
+
retryable: info.retryable,
|
|
10178
|
+
retry: {
|
|
10179
|
+
source: info.source,
|
|
10180
|
+
...info.decision ? { decision: info.decision } : {},
|
|
10181
|
+
...info.confidence ? { confidence: info.confidence } : {},
|
|
10182
|
+
...info.reason ? { reason: info.reason.slice(0, 500) } : {}
|
|
10183
|
+
}
|
|
10184
|
+
};
|
|
10185
|
+
}
|
|
10186
|
+
//#endregion
|
|
10013
10187
|
//#region src/lib/finalize.ts
|
|
10014
10188
|
/**
|
|
10015
10189
|
* Build the `daemonState` payload for a `/complete` call. Freeform
|
|
@@ -10045,9 +10219,10 @@ async function finalizeTask(agent, output, ctx = {}) {
|
|
|
10045
10219
|
...daemonState ? { daemonState } : {}
|
|
10046
10220
|
});
|
|
10047
10221
|
} catch (err) {
|
|
10048
|
-
const
|
|
10222
|
+
const classified = await prepareAttemptFailure(agent, output, errorToFailReason(err), ctx);
|
|
10049
10223
|
ctx.log?.("complete-rejected-falling-back-to-fail", err);
|
|
10050
|
-
|
|
10224
|
+
ctx.log?.(`attempt-failure-classified:${classified.source}`);
|
|
10225
|
+
await agent.tasks.failAttempt(output.taskId, output.attemptN, { error: classified.error });
|
|
10051
10226
|
return;
|
|
10052
10227
|
}
|
|
10053
10228
|
await maybeWriteAnchors(output, ctx);
|
|
@@ -10059,10 +10234,16 @@ async function finalizeTask(agent, output, ctx = {}) {
|
|
|
10059
10234
|
retryable: false
|
|
10060
10235
|
};
|
|
10061
10236
|
if ((await agent.tasks.heartbeat(output.taskId, output.attemptN, {})).cancelled) return;
|
|
10062
|
-
await agent
|
|
10237
|
+
const classified = await prepareAttemptFailure(agent, output, error, ctx);
|
|
10238
|
+
ctx.log?.(`attempt-failure-classified:${classified.source}`);
|
|
10239
|
+
await agent.tasks.failAttempt(output.taskId, output.attemptN, { error: classified.error });
|
|
10063
10240
|
}
|
|
10064
10241
|
function errorToFailReason(err) {
|
|
10065
10242
|
if (err instanceof MoltNetError) {
|
|
10243
|
+
if (err.code !== "VALIDATION_FAILED" && err.statusCode !== 400) return {
|
|
10244
|
+
code: "complete_call_failed",
|
|
10245
|
+
message: `Failed to report task completion (${err.code}, status ${err.statusCode ?? "?"}): ${err.detail ?? err.message}`
|
|
10246
|
+
};
|
|
10066
10247
|
const fields = err.validationErrors?.length ? "; " + err.validationErrors.map((e) => `${e.field}: ${e.message}`).join(" | ") : "";
|
|
10067
10248
|
return {
|
|
10068
10249
|
code: "output_rejected_by_server",
|
|
@@ -10076,6 +10257,28 @@ function errorToFailReason(err) {
|
|
|
10076
10257
|
retryable: false
|
|
10077
10258
|
};
|
|
10078
10259
|
}
|
|
10260
|
+
async function prepareAttemptFailure(agent, output, error, ctx) {
|
|
10261
|
+
const task = ctx.task ?? {
|
|
10262
|
+
id: output.taskId,
|
|
10263
|
+
taskType: "unknown",
|
|
10264
|
+
teamId: "unknown",
|
|
10265
|
+
input: {},
|
|
10266
|
+
maxAttempts: null
|
|
10267
|
+
};
|
|
10268
|
+
const recentMessages = classifyDeterministically(error) === "ambiguous" && ctx.retryTriage && ctx.task ? await agent.tasks.listMessages(output.taskId, output.attemptN).then((messages) => messages.slice(-12)).catch((err) => {
|
|
10269
|
+
ctx.log?.("attempt-failure-message-fetch-failed", err);
|
|
10270
|
+
return [];
|
|
10271
|
+
}) : [];
|
|
10272
|
+
return classifyAttemptFailure({
|
|
10273
|
+
task,
|
|
10274
|
+
attemptN: output.attemptN,
|
|
10275
|
+
maxAttempts: ctx.task?.maxAttempts ?? null,
|
|
10276
|
+
remainingAttempts: ctx.task?.maxAttempts === void 0 ? null : Math.max(0, ctx.task.maxAttempts - output.attemptN),
|
|
10277
|
+
error,
|
|
10278
|
+
recentMessages,
|
|
10279
|
+
triage: ctx.retryTriage
|
|
10280
|
+
});
|
|
10281
|
+
}
|
|
10079
10282
|
async function maybeWriteAnchors(output, ctx) {
|
|
10080
10283
|
const { task, writeCorrelationAnchors, log } = ctx;
|
|
10081
10284
|
if (!task || task.taskType !== "fulfill_brief") return;
|
|
@@ -10387,6 +10590,17 @@ function isExecutable(path) {
|
|
|
10387
10590
|
}
|
|
10388
10591
|
}
|
|
10389
10592
|
//#endregion
|
|
10593
|
+
//#region src/lib/runtime-profile-retry-triage.ts
|
|
10594
|
+
function createRuntimeProfileRetryTriage(options) {
|
|
10595
|
+
return createPiRetryTriage({
|
|
10596
|
+
model: getModel(options.runtimeProfile.provider, options.runtimeProfile.model),
|
|
10597
|
+
thinkingLevel: options.runtimeProfile.thinkingLevel,
|
|
10598
|
+
piAgentDir: options.piAgentDir,
|
|
10599
|
+
timeoutMs: options.timeoutMs,
|
|
10600
|
+
cwd: options.cwd
|
|
10601
|
+
});
|
|
10602
|
+
}
|
|
10603
|
+
//#endregion
|
|
10390
10604
|
//#region src/lib/runtime-sessions.ts
|
|
10391
10605
|
function resolveRuntimeSessionKind(claimedTask) {
|
|
10392
10606
|
const continueFrom = resolveContinueFrom(claimedTask);
|
|
@@ -10882,6 +11096,11 @@ async function runPolling(opts) {
|
|
|
10882
11096
|
return finalizeTask(ctx.agent, terminalOutput, {
|
|
10883
11097
|
task: claimedTask.task,
|
|
10884
11098
|
slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
|
|
11099
|
+
retryTriage: createRuntimeProfileRetryTriage({
|
|
11100
|
+
runtimeProfile: selected.profile,
|
|
11101
|
+
piAgentDir: selected.piAgentDir.path,
|
|
11102
|
+
cwd: ctx.agentRootDir
|
|
11103
|
+
}),
|
|
10885
11104
|
writeCorrelationAnchors: makePrBodyAnchorWriter({
|
|
10886
11105
|
gh: createGhCliClient(),
|
|
10887
11106
|
logger: rootLogger.child({
|
|
@@ -11381,6 +11600,11 @@ async function runOnce(argv) {
|
|
|
11381
11600
|
return finalizeTask(ctx.agent, terminalOutput, {
|
|
11382
11601
|
task: claimedTask.task,
|
|
11383
11602
|
slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
|
|
11603
|
+
retryTriage: createRuntimeProfileRetryTriage({
|
|
11604
|
+
runtimeProfile: profile,
|
|
11605
|
+
piAgentDir: piAgentDir.path,
|
|
11606
|
+
cwd: ctx.agentRootDir
|
|
11607
|
+
}),
|
|
11384
11608
|
writeCorrelationAnchors,
|
|
11385
11609
|
log: (msg, err) => rootLogger.warn({ err }, msg)
|
|
11386
11610
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
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.",
|
|
@@ -45,21 +45,22 @@
|
|
|
45
45
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
46
46
|
"pino": "^10.3.1",
|
|
47
47
|
"pino-pretty": "^13.1.3",
|
|
48
|
-
"@themoltnet/agent-runtime": "0.33.
|
|
49
|
-
"@themoltnet/pi-extension": "0.
|
|
50
|
-
"@themoltnet/sdk": "0.
|
|
48
|
+
"@themoltnet/agent-runtime": "0.33.1",
|
|
49
|
+
"@themoltnet/pi-extension": "0.31.0",
|
|
50
|
+
"@themoltnet/sdk": "0.117.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"tsx": "^4.7.0",
|
|
54
54
|
"typescript": "~5.9.2",
|
|
55
55
|
"vite": "^8.0.0",
|
|
56
56
|
"vitest": "^3.0.0",
|
|
57
|
-
"@moltnet/bootstrap": "0.1.0",
|
|
58
57
|
"@moltnet/crypto-service": "0.1.0",
|
|
59
58
|
"@moltnet/tasks": "0.1.0",
|
|
59
|
+
"@moltnet/bootstrap": "0.1.0",
|
|
60
60
|
"@moltnet/observability": "0.1.0"
|
|
61
61
|
},
|
|
62
62
|
"nx": {
|
|
63
|
+
"projectType": "application",
|
|
63
64
|
"tags": [
|
|
64
65
|
"type:app",
|
|
65
66
|
"scope:agent",
|