@themoltnet/agent-daemon 0.27.2 → 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 (3) hide show
  1. package/README.md +6 -5
  2. package/dist/main.js +231 -5
  3. package/package.json +7 -6
package/README.md CHANGED
@@ -244,10 +244,11 @@ and private diary id.
244
244
  ### 3. Start the daemon against the local stack
245
245
 
246
246
  The daemon picks up the API URL from the agent's `moltnet.json`. It is a
247
- workspace package, not a global CLI — invoke it via `pnpm --filter`:
247
+ workspace package, not a global CLI — invoke it through Nx so execution stays
248
+ rooted in the workspace task graph:
248
249
 
249
250
  ```bash
250
- pnpm --filter @themoltnet/agent-daemon dev poll \
251
+ pnpm exec nx run @themoltnet/agent-daemon:dev -- poll \
251
252
  --agent local-dev \
252
253
  --team "$MOLTNET_TEAM_ID" \
253
254
  --task-types fulfill_brief \
@@ -259,8 +260,8 @@ pnpm --filter @themoltnet/agent-daemon dev poll \
259
260
  registered type.
260
261
  - Pick a runtime profile whose provider/model matches your Pi auth credits.
261
262
  Set `MOLTNET_AGENT_PROFILE` to the profile UUID or team-scoped profile name.
262
- - `dev` (= `tsx watch src/main.ts`) is fine for local. Use `cli` for a
263
- one-shot run without watch.
263
+ - `dev` (= `tsx watch src/main.ts`) is fine for local. Use the Nx `cli` target
264
+ for a one-shot run without watch.
264
265
 
265
266
  Leave it running. It idles until a task lands in its queue.
266
267
 
@@ -325,7 +326,7 @@ against the local e2e stack before the new schema exists on a deployed API.
325
326
  Start the daemon with `--task-types pr_review`:
326
327
 
327
328
  ```bash
328
- pnpm --filter @themoltnet/agent-daemon dev poll \
329
+ pnpm exec nx run @themoltnet/agent-daemon:dev -- poll \
329
330
  --agent local-dev \
330
331
  --team "$MOLTNET_TEAM_ID" \
331
332
  --task-types pr_review \
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({
@@ -11010,6 +11229,7 @@ async function runPolling(opts) {
11010
11229
  topK: profile.topK,
11011
11230
  maxOutputTokens: profile.maxOutputTokens,
11012
11231
  sandboxConfig: sandbox.config,
11232
+ forwardEnv: profile.requiredEnv,
11013
11233
  makeExecutionPlan: (task) => executionPlans.getOrCreate(task),
11014
11234
  makeOnTurnEvent: makeTurnEventHandlerFactory(taskLogger),
11015
11235
  maxTurns: common.maxTurns,
@@ -11264,6 +11484,7 @@ async function runOnce(argv) {
11264
11484
  topK: profile.topK,
11265
11485
  maxOutputTokens: profile.maxOutputTokens,
11266
11486
  sandboxConfig: sandbox.config,
11487
+ forwardEnv: profile.requiredEnv,
11267
11488
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
11268
11489
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
11269
11490
  maxTurns: opts.maxTurns,
@@ -11379,6 +11600,11 @@ async function runOnce(argv) {
11379
11600
  return finalizeTask(ctx.agent, terminalOutput, {
11380
11601
  task: claimedTask.task,
11381
11602
  slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
11603
+ retryTriage: createRuntimeProfileRetryTriage({
11604
+ runtimeProfile: profile,
11605
+ piAgentDir: piAgentDir.path,
11606
+ cwd: ctx.agentRootDir
11607
+ }),
11382
11608
  writeCorrelationAnchors,
11383
11609
  log: (msg, err) => rootLogger.warn({ err }, msg)
11384
11610
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.27.2",
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,9 +45,9 @@
45
45
  "@opentelemetry/semantic-conventions": "^1.39.0",
46
46
  "pino": "^10.3.1",
47
47
  "pino-pretty": "^13.1.3",
48
- "@themoltnet/sdk": "0.115.0",
49
- "@themoltnet/pi-extension": "0.29.0",
50
- "@themoltnet/agent-runtime": "0.32.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",
@@ -55,11 +55,12 @@
55
55
  "vite": "^8.0.0",
56
56
  "vitest": "^3.0.0",
57
57
  "@moltnet/crypto-service": "0.1.0",
58
- "@moltnet/observability": "0.1.0",
59
58
  "@moltnet/tasks": "0.1.0",
60
- "@moltnet/bootstrap": "0.1.0"
59
+ "@moltnet/bootstrap": "0.1.0",
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",