@themoltnet/agent-daemon 0.5.0 → 0.5.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.
Files changed (2) hide show
  1. package/dist/main.js +286 -28
  2. package/package.json +5 -5
package/dist/main.js CHANGED
@@ -5362,6 +5362,15 @@ function validateTaskOutput(taskType, output, input) {
5362
5362
  function getTaskOutputSchema(taskType) {
5363
5363
  return getTaskTypeEntry(taskType)?.outputSchema ?? null;
5364
5364
  }
5365
+ /**
5366
+ * Whether sessions running this task type should have the generic
5367
+ * `subagent` custom tool registered. Returns `false` for unknown task
5368
+ * types and for task types that didn't opt in. See `TaskTypeEntry`
5369
+ * for the design rationale.
5370
+ */
5371
+ function taskTypeUsesSubagents(taskType) {
5372
+ return getTaskTypeEntry(taskType)?.usesSubagents === true;
5373
+ }
5365
5374
  //#endregion
5366
5375
  //#region ../../libs/tasks/src/wire.ts
5367
5376
  /**
@@ -13267,6 +13276,25 @@ function abortableSleep(ms, signal) {
13267
13276
  });
13268
13277
  }
13269
13278
  //#endregion
13279
+ //#region ../../libs/agent-runtime/src/subagent-output-contracts.ts
13280
+ var REGISTRY = /* @__PURE__ */ new Map();
13281
+ /**
13282
+ * Resolve a subagent output contract by name. Returns `null` for
13283
+ * unknown names — callers (the subagent custom tool) decide whether
13284
+ * that's a tool error the parent LLM can recover from or a hard fail.
13285
+ */
13286
+ function getSubagentOutputContract(name) {
13287
+ return REGISTRY.get(name) ?? null;
13288
+ }
13289
+ /**
13290
+ * List all registered contracts. Useful for diagnostics and for the
13291
+ * subagent tool's parameter description so a parent LLM can see what
13292
+ * contracts are available without enumerating them in its prompt.
13293
+ */
13294
+ function listSubagentOutputContracts() {
13295
+ return [...REGISTRY.values()];
13296
+ }
13297
+ //#endregion
13270
13298
  //#region ../../libs/pi-extension/src/moltnet/render-phase6.ts
13271
13299
  function slugToTitle(value) {
13272
13300
  return value.split(/[:/_-]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
@@ -14691,6 +14719,39 @@ function extractUsage(message) {
14691
14719
  };
14692
14720
  }
14693
14721
  //#endregion
14722
+ //#region ../../libs/pi-extension/src/runtime/agent-session-factory.ts
14723
+ var NO_SKILLS = () => ({
14724
+ skills: [],
14725
+ diagnostics: []
14726
+ });
14727
+ /**
14728
+ * Construct an in-memory `AgentSession`. The caller is responsible for
14729
+ * eventually invoking `session.prompt(...)` and for tearing down — the
14730
+ * helper does no lifecycle management beyond construction.
14731
+ */
14732
+ async function buildAgentSession(args) {
14733
+ const piOtelExtension = createPiOtelExtension({
14734
+ agentName: args.agentName,
14735
+ spanAttributes: args.otelSpanAttrs
14736
+ });
14737
+ const resourceLoader = new DefaultResourceLoader({
14738
+ cwd: args.mountPath,
14739
+ agentDir: args.piAuthDir,
14740
+ extensionFactories: [piOtelExtension],
14741
+ appendSystemPrompt: args.appendSystemPrompt,
14742
+ skillsOverride: args.skillsOverride ?? NO_SKILLS
14743
+ });
14744
+ await resourceLoader.reload();
14745
+ return (await createAgentSession({
14746
+ agentDir: args.piAuthDir,
14747
+ cwd: args.mountPath,
14748
+ model: args.modelHandle,
14749
+ customTools: args.customTools,
14750
+ sessionManager: SessionManager.inMemory(),
14751
+ resourceLoader
14752
+ })).session;
14753
+ }
14754
+ //#endregion
14694
14755
  //#region ../../libs/pi-extension/src/runtime/inject-task-context.ts
14695
14756
  /**
14696
14757
  * Slice 1.5 of #943 — wire the agent-runtime resolver into the
@@ -14884,6 +14945,190 @@ function buildRuntimeInstructor(ctx) {
14884
14945
  ].join("\n");
14885
14946
  }
14886
14947
  //#endregion
14948
+ //#region ../../libs/pi-extension/src/runtime/subagent-tool.ts
14949
+ var SUBAGENT_SUBMIT_TOOL_NAME = "submit_subagent_output";
14950
+ /**
14951
+ * Parameters shape the parent LLM sees when calling the subagent tool.
14952
+ *
14953
+ * - `task` — natural-language instructions for the subagent.
14954
+ * The parent authors this per call. Must be
14955
+ * non-empty.
14956
+ * - `output_schema` — name of a registered SubagentOutputContract.
14957
+ * Resolved at call time; unknown names error.
14958
+ */
14959
+ var SubagentToolParameters = Type$2.Object({
14960
+ task: Type$2.String({
14961
+ minLength: 1,
14962
+ description: "Natural-language instructions for the subagent. The subagent starts with a fresh conversation and a narrowed system prompt; this is the only context it has from you."
14963
+ }),
14964
+ output_schema: Type$2.String({
14965
+ minLength: 1,
14966
+ description: "Name of a registered subagent output contract. The subagent must submit a structured payload via `submit_subagent_output` matching this contract."
14967
+ })
14968
+ }, { additionalProperties: false });
14969
+ var DEFAULT_SUBAGENT_TIMEOUT_MS = 300 * 1e3;
14970
+ /**
14971
+ * Build the subagent custom tool for a parent session. The handle
14972
+ * exposes the call counter so executors can emit summary telemetry
14973
+ * when the parent terminates.
14974
+ */
14975
+ function createSubagentTool(args) {
14976
+ const buildSession = args.buildAgentSession ?? buildAgentSession;
14977
+ let callCount = 0;
14978
+ return {
14979
+ tool: defineTool({
14980
+ name: "subagent",
14981
+ label: "Delegate to subagent",
14982
+ description: subagentToolDescription(),
14983
+ parameters: SubagentToolParameters,
14984
+ async execute(_id, params) {
14985
+ if (!Check(SubagentToolParameters, params)) return toolError(`subagent: invalid parameters: ${JSON.stringify([...Errors(SubagentToolParameters, params)].slice(0, 3))}`);
14986
+ const { task, output_schema } = params;
14987
+ const contract = getSubagentOutputContract(output_schema);
14988
+ if (!contract) return toolError(`subagent: unknown output_schema "${output_schema}". Registered contracts: [${listSubagentOutputContracts().map((c) => c.name).join(", ")}]`);
14989
+ callCount += 1;
14990
+ const callIndex = callCount;
14991
+ let captured = null;
14992
+ const submitTool = defineTool({
14993
+ name: SUBAGENT_SUBMIT_TOOL_NAME,
14994
+ label: `Submit ${output_schema}`,
14995
+ description: `Submit your structured output for this subagent task. Call exactly once when done. Args MUST match the ${output_schema} contract; mismatches return a tool error you can recover from in the same session.`,
14996
+ parameters: contract.parametersSchema,
14997
+ async execute(_innerId, innerParams) {
14998
+ if (!Check(contract.parametersSchema, innerParams)) return toolError(`submit_subagent_output: schema validation failed: ${[...Errors(contract.parametersSchema, innerParams)].slice(0, 3).map((e) => `${e.path}: ${e.message}`).join("; ")}. Re-call with a corrected payload.`);
14999
+ captured = innerParams;
15000
+ return {
15001
+ content: [{
15002
+ type: "text",
15003
+ text: "Output captured. Subagent session will terminate; no further action needed."
15004
+ }],
15005
+ details: { captured: true },
15006
+ terminate: true
15007
+ };
15008
+ }
15009
+ });
15010
+ const subagentInstructor = buildSubagentInstructor({
15011
+ contractName: output_schema,
15012
+ contractDescription: contract.description,
15013
+ parentTaskId: args.parentTaskId,
15014
+ callIndex
15015
+ });
15016
+ const session = await buildSession({
15017
+ mountPath: args.mountPath,
15018
+ piAuthDir: args.piAuthDir,
15019
+ modelHandle: args.modelHandle,
15020
+ agentName: args.agentName,
15021
+ customTools: [...args.inheritedCustomTools, submitTool],
15022
+ appendSystemPrompt: [args.parentRuntimeInstructor, subagentInstructor],
15023
+ skillsOverride: () => ({
15024
+ skills: [],
15025
+ diagnostics: []
15026
+ }),
15027
+ otelSpanAttrs: {
15028
+ "moltnet.task.id": args.parentTaskId,
15029
+ "moltnet.task.type": args.parentTaskType,
15030
+ "moltnet.task.attempt": args.parentAttemptN,
15031
+ "moltnet.subagent.contract": output_schema,
15032
+ "moltnet.subagent.index": callIndex
15033
+ }
15034
+ });
15035
+ let abortReason = null;
15036
+ let abortInvoked = false;
15037
+ const fireAbort = (reason) => {
15038
+ if (abortInvoked) return;
15039
+ abortInvoked = true;
15040
+ abortReason = reason;
15041
+ session.abort().catch((err) => {
15042
+ const message = err instanceof Error ? err.message : String(err);
15043
+ process.stderr.write(`[subagent] inner session.abort() failed: ${message}\n`);
15044
+ });
15045
+ };
15046
+ const cancelListener = args.parentCancelSignal ? (() => {
15047
+ const signal = args.parentCancelSignal;
15048
+ const listener = () => fireAbort("parent_cancelled");
15049
+ if (signal.aborted) listener();
15050
+ else signal.addEventListener("abort", listener, { once: true });
15051
+ return () => signal.removeEventListener("abort", listener);
15052
+ })() : null;
15053
+ const timeoutMs = args.timeoutMs === void 0 || args.timeoutMs < 0 ? DEFAULT_SUBAGENT_TIMEOUT_MS : args.timeoutMs;
15054
+ const timeoutHandle = timeoutMs > 0 ? setTimeout(() => fireAbort("subagent_timed_out"), timeoutMs) : null;
15055
+ try {
15056
+ await session.prompt(task);
15057
+ } catch (err) {
15058
+ return toolError(`subagent: inner session.prompt() threw: ${err instanceof Error ? err.message : String(err)}`);
15059
+ } finally {
15060
+ if (timeoutHandle) clearTimeout(timeoutHandle);
15061
+ if (cancelListener) cancelListener();
15062
+ }
15063
+ if (abortReason !== null) return toolError(`subagent: ${abortReason === "subagent_timed_out" ? `subagent timed out after ${timeoutMs}ms` : "parent task was cancelled"}. The parent should fail this task or retry with a clearer scope.`);
15064
+ if (captured === null) return toolError(`subagent: inner session ended without calling ${SUBAGENT_SUBMIT_TOOL_NAME}. The parent should retry with clearer instructions or fail the task.`);
15065
+ return {
15066
+ content: [{
15067
+ type: "text",
15068
+ text: JSON.stringify(captured)
15069
+ }],
15070
+ details: {
15071
+ captured: true,
15072
+ contract: output_schema,
15073
+ callIndex
15074
+ }
15075
+ };
15076
+ }
15077
+ }),
15078
+ getCallCount: () => callCount
15079
+ };
15080
+ }
15081
+ function subagentToolDescription() {
15082
+ return [
15083
+ "Delegate a sub-task to a fresh subagent session with isolated context.",
15084
+ "",
15085
+ "The subagent starts with no conversation history and only the `task` ",
15086
+ "string you provide as its instructions. It runs in the same VM with ",
15087
+ "the same tools you have (Gondolin-routed Read/Write/Edit/Bash, ",
15088
+ "moltnet_* tools), and is expected to call ",
15089
+ `\`${SUBAGENT_SUBMIT_TOOL_NAME}\` with a payload matching the named `,
15090
+ "contract before its session ends.",
15091
+ "",
15092
+ "On success, the tool result is the JSON-stringified subagent payload.",
15093
+ "On failure (unknown contract, validation error, subagent did not ",
15094
+ "submit) the tool returns isError:true with a recoverable message."
15095
+ ].join("\n");
15096
+ }
15097
+ function buildSubagentInstructor(args) {
15098
+ return [
15099
+ "# You are a subagent",
15100
+ "",
15101
+ `Parent task: \`${args.parentTaskId}\` (subagent call #${args.callIndex}).`,
15102
+ "",
15103
+ `Your assigned output contract is \`${args.contractName}\`:`,
15104
+ `${args.contractDescription}`,
15105
+ "",
15106
+ "Rules for this session:",
15107
+ "",
15108
+ `- You MUST call \`${SUBAGENT_SUBMIT_TOOL_NAME}\` exactly once with a `,
15109
+ " payload matching the contract above. Your session terminates on ",
15110
+ " the valid call.",
15111
+ "- The parent's message above is your task. Do not invent additional ",
15112
+ " steps the parent did not request.",
15113
+ "- All MoltNet runtime invariants from the parent runtime instructor ",
15114
+ " apply (diary discipline, gh-auth pattern, etc.) IF you take any ",
15115
+ " action that would trigger them. Most subagents do not commit code ",
15116
+ " or open PRs — only do so if your task message explicitly requires it.",
15117
+ "- You do NOT have access to the `subagent` tool. Do not attempt nested ",
15118
+ " delegation; do the work yourself."
15119
+ ].join("\n");
15120
+ }
15121
+ function toolError(text) {
15122
+ return {
15123
+ content: [{
15124
+ type: "text",
15125
+ text
15126
+ }],
15127
+ details: { captured: false },
15128
+ isError: true
15129
+ };
15130
+ }
15131
+ //#endregion
14887
15132
  //#region ../../libs/pi-extension/src/runtime/task-output.ts
14888
15133
  var METER_NAME = "@themoltnet/pi-extension/task-output";
14889
15134
  var parseResultCounter = null;
@@ -15195,6 +15440,7 @@ async function executePiTask(claimedTask, reporter, opts) {
15195
15440
  const taskTeamId = task.teamId ?? "";
15196
15441
  let reporterOpen = false;
15197
15442
  let session = null;
15443
+ let subagentHandle = null;
15198
15444
  const finalUsage = emptyUsage(opts.provider, opts.model);
15199
15445
  let cancelListener = null;
15200
15446
  const makeFailedOutput = (code, message, usage = finalUsage) => ({
@@ -15312,47 +15558,55 @@ async function executePiTask(claimedTask, reporter, opts) {
15312
15558
  });
15313
15559
  const piAuthDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
15314
15560
  const modelHandle = getModel(opts.provider, opts.model);
15315
- const piOtelExtension = createPiOtelExtension({
15316
- agentName: opts.agentName,
15317
- spanAttributes: {
15318
- "moltnet.task.id": task.id,
15319
- "moltnet.task.attempt": attemptN,
15320
- "moltnet.task.type": task.taskType
15321
- }
15322
- });
15323
- const appendSystemPrompt = [buildRuntimeInstructor({
15561
+ const runtimeInstructor = buildRuntimeInstructor({
15324
15562
  taskId: task.id,
15325
15563
  taskType: task.taskType,
15326
15564
  attemptN,
15327
15565
  diaryId,
15328
15566
  agentName: opts.agentName,
15329
15567
  correlationId: task.correlationId ?? null
15330
- })];
15568
+ });
15569
+ const appendSystemPrompt = [runtimeInstructor];
15331
15570
  if (injectedContext.systemPromptPrefix) appendSystemPrompt.push(injectedContext.systemPromptPrefix);
15332
15571
  const injectedSkills = injectedContext.skills;
15333
- const resourceLoader = new DefaultResourceLoader({
15334
- cwd: mountPath,
15335
- agentDir: piAuthDir,
15336
- extensionFactories: [piOtelExtension],
15572
+ const parentSubagentTools = [];
15573
+ if (taskTypeUsesSubagents(task.taskType)) {
15574
+ subagentHandle = createSubagentTool({
15575
+ mountPath,
15576
+ piAuthDir,
15577
+ modelHandle,
15578
+ agentName: opts.agentName,
15579
+ inheritedCustomTools: [...gondolinCustomTools, ...moltnetTools],
15580
+ parentRuntimeInstructor: runtimeInstructor,
15581
+ parentTaskId: task.id,
15582
+ parentTaskType: task.taskType,
15583
+ parentAttemptN: attemptN,
15584
+ parentCancelSignal: reporter.cancelSignal
15585
+ });
15586
+ parentSubagentTools.push(subagentHandle.tool);
15587
+ }
15588
+ session = await buildAgentSession({
15589
+ mountPath,
15590
+ piAuthDir,
15591
+ modelHandle,
15592
+ agentName: opts.agentName,
15593
+ customTools: [
15594
+ ...gondolinCustomTools,
15595
+ ...moltnetTools,
15596
+ ...submitTools,
15597
+ ...parentSubagentTools
15598
+ ],
15337
15599
  appendSystemPrompt,
15338
15600
  skillsOverride: () => ({
15339
15601
  skills: injectedSkills,
15340
15602
  diagnostics: []
15341
- })
15603
+ }),
15604
+ otelSpanAttrs: {
15605
+ "moltnet.task.id": task.id,
15606
+ "moltnet.task.attempt": attemptN,
15607
+ "moltnet.task.type": task.taskType
15608
+ }
15342
15609
  });
15343
- await resourceLoader.reload();
15344
- session = (await createAgentSession({
15345
- agentDir: piAuthDir,
15346
- cwd: mountPath,
15347
- model: modelHandle,
15348
- customTools: [
15349
- ...gondolinCustomTools,
15350
- ...moltnetTools,
15351
- ...submitTools
15352
- ],
15353
- sessionManager: SessionManager.inMemory(),
15354
- resourceLoader
15355
- })).session;
15356
15610
  } catch (err) {
15357
15611
  const message = err instanceof Error ? err.message : String(err);
15358
15612
  await emit("error", {
@@ -15423,6 +15677,10 @@ async function executePiTask(claimedTask, reporter, opts) {
15423
15677
  phase: "session_prompt"
15424
15678
  });
15425
15679
  }
15680
+ if (subagentHandle && subagentHandle.getCallCount() > 0) await emit("info", {
15681
+ event: "subagent_summary",
15682
+ callCount: subagentHandle.getCallCount()
15683
+ });
15426
15684
  await Promise.all(recordingPromise);
15427
15685
  const cancelled = reporter.cancelSignal.aborted;
15428
15686
  let parsedOutput = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.5.0",
3
+ "version": "0.5.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,8 +33,8 @@
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.12.0",
37
- "@themoltnet/pi-extension": "0.14.0",
36
+ "@themoltnet/agent-runtime": "0.13.0",
37
+ "@themoltnet/pi-extension": "0.15.0",
38
38
  "@themoltnet/sdk": "0.100.0"
39
39
  },
40
40
  "devDependencies": {
@@ -44,8 +44,8 @@
44
44
  "vitest": "^3.0.0",
45
45
  "@moltnet/bootstrap": "0.1.0",
46
46
  "@moltnet/crypto-service": "0.1.0",
47
- "@moltnet/tasks": "0.1.0",
48
- "@moltnet/database": "0.1.0"
47
+ "@moltnet/database": "0.1.0",
48
+ "@moltnet/tasks": "0.1.0"
49
49
  },
50
50
  "nx": {
51
51
  "tags": [