@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.
Files changed (2) hide show
  1. package/dist/main.js +229 -5
  2. 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 reason = errorToFailReason(err);
10222
+ const classified = await prepareAttemptFailure(agent, output, errorToFailReason(err), ctx);
10049
10223
  ctx.log?.("complete-rejected-falling-back-to-fail", err);
10050
- await agent.tasks.fail(output.taskId, output.attemptN, { error: reason });
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.tasks.fail(output.taskId, output.attemptN, { error });
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.28.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.0",
49
- "@themoltnet/pi-extension": "0.30.0",
50
- "@themoltnet/sdk": "0.116.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",