@hyperdrive.bot/paseo-server 0.3.42 → 0.3.44

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 (28) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +6 -0
  2. package/dist/server/server/agent/agent-manager.js +7 -1
  3. package/dist/server/server/agent/import-sessions.d.ts +6 -1
  4. package/dist/server/server/agent/import-sessions.js +27 -2
  5. package/dist/server/server/agent/mcp-server.js +6 -1
  6. package/dist/server/server/agent/providers/claude/agent.d.ts +16 -0
  7. package/dist/server/server/agent/providers/claude/agent.js +21 -2
  8. package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +20 -0
  9. package/dist/server/server/agent/providers/claude/background-work-kinds.js +14 -0
  10. package/dist/server/server/bootstrap.js +1 -0
  11. package/dist/server/server/session.js +1 -0
  12. package/dist/server/server/workflow/workflow-agent-resolution.d.ts +82 -0
  13. package/dist/server/server/workflow/workflow-agent-resolution.js +105 -0
  14. package/dist/server/server/workflow/workflow-manager.d.ts +155 -20
  15. package/dist/server/server/workflow/workflow-manager.js +439 -31
  16. package/dist/server/server/workflow/workflow-progress.d.ts +53 -0
  17. package/dist/server/server/workflow/workflow-progress.js +96 -0
  18. package/dist/server/web-ui/_expo/static/js/web/{index-9c3bcdc334cf1c08001b6bea510e0292.js → index-6a99048b3a9acca9bc30efd44300414e.js} +7 -7
  19. package/dist/server/web-ui/_expo/static/js/web/index-6a99048b3a9acca9bc30efd44300414e.js.br +0 -0
  20. package/dist/server/web-ui/_expo/static/js/web/index-6a99048b3a9acca9bc30efd44300414e.js.gz +0 -0
  21. package/dist/server/web-ui/_expo/static/js/web/{index-9c3bcdc334cf1c08001b6bea510e0292.js.map.br → index-6a99048b3a9acca9bc30efd44300414e.js.map.br} +0 -0
  22. package/dist/server/web-ui/_expo/static/js/web/{index-9c3bcdc334cf1c08001b6bea510e0292.js.map.gz → index-6a99048b3a9acca9bc30efd44300414e.js.map.gz} +0 -0
  23. package/dist/server/web-ui/index.html +1 -1
  24. package/dist/server/web-ui/index.html.br +0 -0
  25. package/dist/server/web-ui/index.html.gz +0 -0
  26. package/package.json +6 -6
  27. package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.br +0 -0
  28. package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.gz +0 -0
@@ -322,6 +322,12 @@ export declare class AgentManager {
322
322
  cwd: string;
323
323
  workspaceId: string;
324
324
  labels?: Record<string, string>;
325
+ /**
326
+ * Config carried over from a previous agent record for the same provider
327
+ * session. Without it an import falls back to daemon defaults, which can
328
+ * demote a session to a smaller-context model and force a compaction.
329
+ */
330
+ config?: Partial<AgentSessionConfig>;
325
331
  }): Promise<ManagedAgent>;
326
332
  /**
327
333
  * Hot-swap a running agent's provider to a compatible alternative
@@ -614,6 +614,7 @@ export class AgentManager {
614
614
  throw new Error(`Provider '${input.provider}' does not support importing sessions`);
615
615
  }
616
616
  const { storedConfig, launchConfig } = await this.prepareSessionConfig({
617
+ ...input.config,
617
618
  provider: input.provider,
618
619
  cwd: input.cwd,
619
620
  }, resolvedAgentId);
@@ -2827,7 +2828,12 @@ export class AgentManager {
2827
2828
  const activeBackgroundTaskCount = this.getActiveBackgroundTaskCount(agent);
2828
2829
  if (activeBackgroundTaskCount > 0) {
2829
2830
  agent.suppressedAttentionCount = (agent.suppressedAttentionCount ?? 0) + 1;
2830
- this.logger.debug({
2831
+ // info, not debug: this is the ONLY production signal that the
2832
+ // suppression fired. The daemon logs at level 30 and emits zero debug
2833
+ // lines, so at debug this was unobservable — and an absent log then
2834
+ // looked identical to a working feature. Verify with:
2835
+ // journalctl -u paseo.service | grep "Suppressed finished-attention"
2836
+ this.logger.info({
2831
2837
  agentId: agent.id,
2832
2838
  activeBackgroundTaskCount,
2833
2839
  suppressedAttentionCount: agent.suppressedAttentionCount,
@@ -2,7 +2,7 @@ import type { z } from "zod";
2
2
  import type { Logger } from "pino";
3
3
  import type { ProviderSnapshotManager } from "./provider-snapshot-manager.js";
4
4
  import type { AgentManager, ManagedAgent } from "./agent-manager.js";
5
- import type { AgentStorage } from "./agent-storage.js";
5
+ import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
6
6
  import type { AgentProvider } from "./agent-sdk-types.js";
7
7
  import type { FetchRecentProviderSessionsRequestMessage, ImportAgentRequestMessageSchema, RecentProviderSessionDescriptorPayload } from "@hyperdrive.bot/paseo-protocol/messages";
8
8
  type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>;
@@ -43,5 +43,10 @@ export declare function normalizeImportAgentRequest(msg: ImportAgentRequestMessa
43
43
  };
44
44
  export declare function listImportableProviderSessions(input: ListImportableProviderSessionsInput): Promise<ListImportableProviderSessionsResult>;
45
45
  export declare function importProviderSession(input: ImportProviderSessionInput): Promise<ImportProviderSessionResult>;
46
+ export declare function inheritedImportConfig(record: StoredAgentRecord | undefined): {
47
+ model?: string;
48
+ thinkingOptionId?: string;
49
+ modeId?: string;
50
+ } | undefined;
46
51
  export {};
47
52
  //# sourceMappingURL=import-sessions.d.ts.map
@@ -72,13 +72,18 @@ export async function importProviderSession(input) {
72
72
  throw new Error("Import requires cwd from the selected provider session");
73
73
  }
74
74
  const handle = buildImportPersistenceHandle({ provider, providerHandleId, cwd });
75
- await unarchiveAgentByHandle(input.agentStorage, input.agentManager, handle);
75
+ const priorRecord = await unarchiveAgentByHandle(input.agentStorage, input.agentManager, handle);
76
+ const inheritedConfig = inheritedImportConfig(priorRecord);
77
+ if (inheritedConfig) {
78
+ input.logger?.info?.({ providerHandleId, ...inheritedConfig }, "Import inheriting config from the session's previous agent record");
79
+ }
76
80
  const snapshot = await input.agentManager.importProviderSession({
77
81
  provider,
78
82
  providerHandleId,
79
83
  cwd,
80
84
  workspaceId: input.workspaceId,
81
85
  labels,
86
+ ...(inheritedConfig ? { config: inheritedConfig } : {}),
82
87
  });
83
88
  await unarchiveAgentState(input.agentStorage, input.agentManager, snapshot.id);
84
89
  return {
@@ -92,9 +97,29 @@ async function unarchiveAgentByHandle(agentStorage, agentManager, handle) {
92
97
  (record.persistence.sessionId === handle.sessionId ||
93
98
  record.persistence.nativeHandle === handle.nativeHandle));
94
99
  if (!matched) {
95
- return;
100
+ return undefined;
96
101
  }
97
102
  await unarchiveAgentState(agentStorage, agentManager, matched.id);
103
+ return matched;
104
+ }
105
+ // An import that lands on a session paseo has seen before must not silently
106
+ // downgrade it. The prior record already carries the model, thinking option and
107
+ // permission mode the session was running under; without this the imported agent
108
+ // falls back to daemon defaults, which has demoted a 1M-context session to the
109
+ // 200k model and auto-compacted its transcript on the first prompt.
110
+ export function inheritedImportConfig(record) {
111
+ if (!record) {
112
+ return undefined;
113
+ }
114
+ const modeId = record.lastModeId ?? record.config?.modeId ?? undefined;
115
+ const inherited = {
116
+ ...(record.config?.model ? { model: record.config.model } : {}),
117
+ ...(record.config?.thinkingOptionId
118
+ ? { thinkingOptionId: record.config.thinkingOptionId }
119
+ : {}),
120
+ ...(modeId ? { modeId } : {}),
121
+ };
122
+ return Object.keys(inherited).length > 0 ? inherited : undefined;
98
123
  }
99
124
  function parseRecentProviderSessionsSince(since) {
100
125
  if (!since) {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { WorkflowNotFoundError } from "../workflow/workflow-manager.js";
4
- import { WorkflowSnapshotSchema, WorkflowStatusSchema, WorkflowTaskGraphSchema, SessionDigestSchema, } from "../messages.js";
4
+ import { WorkflowAgentPresetSchema, WorkflowSnapshotSchema, WorkflowStatusSchema, WorkflowTaskGraphSchema, SessionDigestSchema, } from "../messages.js";
5
5
  import { AgentStatusEnum } from "./mcp-shared.js";
6
6
  import { expandUserPath } from "../path-utils.js";
7
7
  import { ensureValidJson } from "../json-utils.js";
@@ -28,6 +28,10 @@ const workflowStartArgsSchema = z.object({
28
28
  .record(z.string(), z.string())
29
29
  .optional()
30
30
  .describe("Optional labels copied onto every spawned child alongside the workflow id"),
31
+ agentPresets: z
32
+ .record(z.string(), WorkflowAgentPresetSchema)
33
+ .optional()
34
+ .describe("Optional map of agent name to partial session config; a task's agentType selects one"),
31
35
  });
32
36
  const workflowIdArgsSchema = z.object({
33
37
  workflowId: z.string().describe("Workflow id returned by workflow_start"),
@@ -238,6 +242,7 @@ export async function createAgentMcpServer(options) {
238
242
  baseConfig,
239
243
  ...(args.title ? { title: args.title } : {}),
240
244
  ...(args.labels ? { labels: args.labels } : {}),
245
+ ...(args.agentPresets ? { agentPresets: args.agentPresets } : {}),
241
246
  ...(callerAgentId ? { parentAgentId: callerAgentId } : {}),
242
247
  });
243
248
  const started = await workflowManager.startWorkflow(workflow.id);
@@ -79,6 +79,22 @@ export declare class ClaudeAgentClient implements AgentClient {
79
79
  }>;
80
80
  private assertConfig;
81
81
  }
82
+ /**
83
+ * Tool names whose tool_result can ANNOUNCE background work that outlives the turn.
84
+ *
85
+ * This gate is the reason the tracker sees anything at all. It was previously
86
+ * `toolName !== "Bash"`, which made the monitor and cron start-patterns in
87
+ * `background-task-tracker.ts` unreachable: a `Monitor` tool_result returned
88
+ * here before `noteToolResultText` was ever called, so a monitor-only session
89
+ * reported zero background tasks, bucketed as "done", and kept lighting the
90
+ * unread chip on every tick.
91
+ *
92
+ * ⚠️ The parser having a pattern is NOT enough — the tool name must be in this
93
+ * set too. Any new kind of background work needs BOTH, and an integration test
94
+ * that drives `handleToolResult` (not the parser directly), or the same hole
95
+ * reopens silently. See `background-work-kinds.ts`.
96
+ */
97
+ export declare const BACKGROUND_WORK_TOOL_NAMES: Set<string>;
82
98
  export declare class ClaudeAgentSession implements AgentSession {
83
99
  readonly provider: "claude";
84
100
  readonly capabilities: AgentCapabilityFlags;
@@ -1514,6 +1514,22 @@ class ClaudeContextUsageState {
1514
1514
  };
1515
1515
  }
1516
1516
  }
1517
+ /**
1518
+ * Tool names whose tool_result can ANNOUNCE background work that outlives the turn.
1519
+ *
1520
+ * This gate is the reason the tracker sees anything at all. It was previously
1521
+ * `toolName !== "Bash"`, which made the monitor and cron start-patterns in
1522
+ * `background-task-tracker.ts` unreachable: a `Monitor` tool_result returned
1523
+ * here before `noteToolResultText` was ever called, so a monitor-only session
1524
+ * reported zero background tasks, bucketed as "done", and kept lighting the
1525
+ * unread chip on every tick.
1526
+ *
1527
+ * ⚠️ The parser having a pattern is NOT enough — the tool name must be in this
1528
+ * set too. Any new kind of background work needs BOTH, and an integration test
1529
+ * that drives `handleToolResult` (not the parser directly), or the same hole
1530
+ * reopens silently. See `background-work-kinds.ts`.
1531
+ */
1532
+ export const BACKGROUND_WORK_TOOL_NAMES = new Set(["Bash", "Monitor", "CronCreate", "CronDelete"]);
1517
1533
  export class ClaudeAgentSession {
1518
1534
  /**
1519
1535
  * Register a hook event handler for an agent. Stub in Epic 1 — Epic 3 wires
@@ -4122,7 +4138,7 @@ export class ClaudeAgentSession {
4122
4138
  * Both go into the record so the UI can show what is actually running.
4123
4139
  */
4124
4140
  trackBackgroundShellStart(block, toolName, entry) {
4125
- if (toolName !== "Bash" || block.is_error) {
4141
+ if (!BACKGROUND_WORK_TOOL_NAMES.has(toolName) || block.is_error) {
4126
4142
  return;
4127
4143
  }
4128
4144
  const resultText = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
@@ -4136,7 +4152,10 @@ export class ClaudeAgentSession {
4136
4152
  startedAt: resolveBackgroundShellStartedAt(extractBackgroundOutputFile(resultText)),
4137
4153
  });
4138
4154
  if (started.length > 0) {
4139
- this.logger.debug({ taskIds: started, command }, "Tracking Claude background shell(s)");
4155
+ // info, not debug: the daemon runs at level 30 and emits no debug lines,
4156
+ // so a debug log here is unobservable in production — which is exactly
4157
+ // how a silently-unreachable tracker went unnoticed.
4158
+ this.logger.info({ taskIds: started, toolName, command }, "Tracking background work");
4140
4159
  }
4141
4160
  }
4142
4161
  /**
@@ -21,6 +21,17 @@
21
21
  * fails. Add an entry claiming `tracked: true` without a start pattern that
22
22
  * actually extracts its id and `background-work-kinds.test.ts` fails.
23
23
  *
24
+ * ⚠️ A PATTERN IS NOT ENOUGH — THE TOOL NAME MUST BE GATED IN TOO
25
+ *
26
+ * `trackBackgroundShellStart` in `providers/claude/agent.ts` only forwards a
27
+ * tool_result to the parser when its tool name is in `BACKGROUND_WORK_TOOL_NAMES`.
28
+ * The first version of this file shipped monitor and cron patterns while that
29
+ * gate still read `toolName !== "Bash"`, so both were dead code in production
30
+ * and every test passed because the tests called the parser directly. A kind is
31
+ * only really tracked when it has: a start pattern, a retirement path, an entry
32
+ * here, AND its tool name in that set — proven by an integration test that goes
33
+ * through `handleToolResult`.
34
+ *
24
35
  * SAMPLES ARE CAPTURES, NOT TRANSCRIPTIONS
25
36
  *
26
37
  * Every `sample` below was pasted verbatim out of a live session's tool_result
@@ -43,6 +54,12 @@ interface TrackedKind {
43
54
  retiredBy: string;
44
55
  /** Which module owns the tracking. */
45
56
  trackedBy: string;
57
+ /**
58
+ * The tool name this kind's result arrives under. MUST be present in
59
+ * `BACKGROUND_WORK_TOOL_NAMES` (providers/claude/agent.ts) or the parser is
60
+ * never reached, however correct its pattern is.
61
+ */
62
+ toolName: string;
46
63
  }
47
64
  interface SilencedKind {
48
65
  tracked: false;
@@ -63,6 +80,7 @@ export declare const BACKGROUND_WORK_KINDS: {
63
80
  readonly expectedId: "beuhoixae";
64
81
  readonly retiredBy: "<task-notification> with a terminal <status>";
65
82
  readonly trackedBy: "ClaudeBackgroundTaskTracker.noteToolResultText";
83
+ readonly toolName: "Bash";
66
84
  };
67
85
  readonly monitor: {
68
86
  readonly tracked: true;
@@ -71,6 +89,7 @@ export declare const BACKGROUND_WORK_KINDS: {
71
89
  readonly expectedId: "b6dxcqe9y";
72
90
  readonly retiredBy: "<task-notification> with a terminal <status>";
73
91
  readonly trackedBy: "ClaudeBackgroundTaskTracker.noteToolResultText";
92
+ readonly toolName: "Monitor";
74
93
  };
75
94
  readonly cron: {
76
95
  readonly tracked: true;
@@ -79,6 +98,7 @@ export declare const BACKGROUND_WORK_KINDS: {
79
98
  readonly expectedId: "b8df03d3";
80
99
  readonly retiredBy: "\"Cancelled job <id>.\" in a later tool_result";
81
100
  readonly trackedBy: "ClaudeBackgroundTaskTracker.noteToolResultText";
101
+ readonly toolName: "CronCreate";
82
102
  };
83
103
  readonly scheduled_wakeup: {
84
104
  readonly tracked: false;
@@ -21,6 +21,17 @@
21
21
  * fails. Add an entry claiming `tracked: true` without a start pattern that
22
22
  * actually extracts its id and `background-work-kinds.test.ts` fails.
23
23
  *
24
+ * ⚠️ A PATTERN IS NOT ENOUGH — THE TOOL NAME MUST BE GATED IN TOO
25
+ *
26
+ * `trackBackgroundShellStart` in `providers/claude/agent.ts` only forwards a
27
+ * tool_result to the parser when its tool name is in `BACKGROUND_WORK_TOOL_NAMES`.
28
+ * The first version of this file shipped monitor and cron patterns while that
29
+ * gate still read `toolName !== "Bash"`, so both were dead code in production
30
+ * and every test passed because the tests called the parser directly. A kind is
31
+ * only really tracked when it has: a start pattern, a retirement path, an entry
32
+ * here, AND its tool name in that set — proven by an integration test that goes
33
+ * through `handleToolResult`.
34
+ *
24
35
  * SAMPLES ARE CAPTURES, NOT TRANSCRIPTIONS
25
36
  *
26
37
  * Every `sample` below was pasted verbatim out of a live session's tool_result
@@ -37,6 +48,7 @@ export const BACKGROUND_WORK_KINDS = {
37
48
  expectedId: "beuhoixae",
38
49
  retiredBy: "<task-notification> with a terminal <status>",
39
50
  trackedBy: "ClaudeBackgroundTaskTracker.noteToolResultText",
51
+ toolName: "Bash",
40
52
  },
41
53
  monitor: {
42
54
  tracked: true,
@@ -48,6 +60,7 @@ export const BACKGROUND_WORK_KINDS = {
48
60
  // Only its START was ever invisible.
49
61
  retiredBy: "<task-notification> with a terminal <status>",
50
62
  trackedBy: "ClaudeBackgroundTaskTracker.noteToolResultText",
63
+ toolName: "Monitor",
51
64
  },
52
65
  cron: {
53
66
  tracked: true,
@@ -58,6 +71,7 @@ export const BACKGROUND_WORK_KINDS = {
58
71
  // tool_result is its only retirement signal — note the glued-on period.
59
72
  retiredBy: '"Cancelled job <id>." in a later tool_result',
60
73
  trackedBy: "ClaudeBackgroundTaskTracker.noteToolResultText",
74
+ toolName: "CronCreate",
61
75
  },
62
76
  scheduled_wakeup: {
63
77
  tracked: false,
@@ -669,6 +669,7 @@ export async function createPaseoDaemon(config, rootLogger) {
669
669
  const workflowManager = new WorkflowManager({
670
670
  agentManager,
671
671
  storage: workflowStorage,
672
+ logger,
672
673
  });
673
674
  // Story 2.1 — read-only HTTP status + SSE phase-transition routes. Gated on the
674
675
  // same WORKFLOWS_FEATURE_ENABLED constant as the server_info handshake flag.
@@ -1202,6 +1202,7 @@ export class Session {
1202
1202
  baseConfig,
1203
1203
  ...(msg.title ? { title: msg.title } : {}),
1204
1204
  ...(msg.labels ? { labels: msg.labels } : {}),
1205
+ ...(msg.agentPresets ? { agentPresets: msg.agentPresets } : {}),
1205
1206
  });
1206
1207
  const started = await manager.startWorkflow(workflow.id);
1207
1208
  this.emit({
@@ -0,0 +1,82 @@
1
+ import type { WorkflowAgentPreset } from "@hyperdrive.bot/paseo-protocol/messages";
2
+ import type { AgentSessionConfig } from "../agent/agent-sdk-types.js";
3
+ import type { TaskGraph, TaskNode } from "./workflow-manager.js";
4
+ /**
5
+ * workflow-agent-resolution — Epic 2, Story 2.1.
6
+ *
7
+ * Per-step agent selection. Before this module every child of a workflow was spawned
8
+ * with the one graph-level `baseConfig`, so `TaskNode.agentType` was a declared field
9
+ * with no reader: delegation to a different agent per step was simply not supported.
10
+ * This module is that reader.
11
+ *
12
+ * Why it is PURE and lives outside `workflow-manager.ts` (the `workflow-progress.ts`
13
+ * precedent):
14
+ * - **Unit-testable with no harness.** Precedence is a function of three plain objects;
15
+ * proving it should not require an `AgentManager`, a temp dir or a spawned child.
16
+ * - **The manager stays an orchestrator, not a calculator.** One helper serves BOTH
17
+ * spawn call sites (`startWorkflow`'s readiness loop and `continueWorkflow`), so the
18
+ * precedence rule exists exactly once.
19
+ * - **No runtime cycle.** Every import above is type-only and therefore erased, so the
20
+ * manager can import this module as a VALUE. `WorkflowUnknownAgentError` is declared
21
+ * here (the leaf) and re-exported from the manager, exactly as `WorkflowCycleError`
22
+ * is declared in `workflow-progress.ts` and re-exported.
23
+ *
24
+ * Why the merge copies only DEFINED keys rather than spreading:
25
+ * `{ ...base, ...preset, ...override }` lets an explicit `undefined` in a higher tier
26
+ * ERASE a defined lower-tier value — `{ model: undefined }` on a task would blank the
27
+ * workflow's model instead of leaving it alone. A tier states what it overrides; a key
28
+ * it does not set must be invisible.
29
+ *
30
+ * Why `cwd` is forced back to `baseConfig.cwd` as the final statement:
31
+ * the working directory is workflow-owned. A step relocating its own child would break
32
+ * the containment every other surface assumes. There are two independent guards, on
33
+ * purpose: `WorkflowAgentPresetSchema` rejects `cwd` at parse time (so it can never
34
+ * arrive over the wire), and this assignment wins over any hand-built object literal
35
+ * that bypassed the schema.
36
+ *
37
+ * Determinism rules, mirroring `workflow-progress.ts`: synchronous, no I/O, no
38
+ * `Date.now()` / `new Date()`, no snapshot read, no module-level mutable state.
39
+ */
40
+ /** Thrown when a task's `agentType` names an agent absent from the workflow's presets. */
41
+ export declare class WorkflowUnknownAgentError extends Error {
42
+ readonly workflowId: string | undefined;
43
+ readonly taskId: string;
44
+ readonly agentName: string;
45
+ readonly knownAgents: string[];
46
+ constructor(workflowId: string | undefined, taskId: string, agentName: string, knownAgents: string[]);
47
+ }
48
+ /**
49
+ * Create-time validation: reject a graph whose any task names an agent nobody defined.
50
+ *
51
+ * Reports the FIRST offender in `graph.tasks` array order, so the error is stable for
52
+ * a given graph rather than dependent on object-key iteration. The message names the
53
+ * task id, the unknown name and the known names, because both launch boundaries
54
+ * (`session.ts` RPC, `mcp-server.ts` tool) forward `error.message` verbatim to the
55
+ * operator and that string is the whole diagnosis.
56
+ *
57
+ * Own-key lookup, never a bare `in`: a preset named `toString` or `constructor` must
58
+ * not be satisfied by `Object.prototype`.
59
+ */
60
+ export declare function assertKnownAgents(args: {
61
+ graph: TaskGraph;
62
+ agentPresets: Record<string, WorkflowAgentPreset>;
63
+ workflowId?: string;
64
+ }): void;
65
+ /**
66
+ * Resolve the `AgentSessionConfig` one task's child is spawned with.
67
+ *
68
+ * Precedence, lowest to highest: `baseConfig`, then `agentPresets[node.agentType]`,
69
+ * then `node.agentConfig`. Only keys whose value is `!== undefined` are copied from a
70
+ * higher tier, so a preset that sets only `model` leaves `baseConfig.provider` intact.
71
+ *
72
+ * TOTAL by design: an `agentType` naming a missing preset returns `{ ...baseConfig }`
73
+ * rather than throwing. That path is unreachable after `assertKnownAgents` runs at
74
+ * create time, and a helper that threw on it would convert a validation gap into a
75
+ * mid-graph spawn crash.
76
+ */
77
+ export declare function resolveTaskConfig(args: {
78
+ node: TaskNode;
79
+ baseConfig: AgentSessionConfig;
80
+ agentPresets: Record<string, WorkflowAgentPreset>;
81
+ }): AgentSessionConfig;
82
+ //# sourceMappingURL=workflow-agent-resolution.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * workflow-agent-resolution — Epic 2, Story 2.1.
3
+ *
4
+ * Per-step agent selection. Before this module every child of a workflow was spawned
5
+ * with the one graph-level `baseConfig`, so `TaskNode.agentType` was a declared field
6
+ * with no reader: delegation to a different agent per step was simply not supported.
7
+ * This module is that reader.
8
+ *
9
+ * Why it is PURE and lives outside `workflow-manager.ts` (the `workflow-progress.ts`
10
+ * precedent):
11
+ * - **Unit-testable with no harness.** Precedence is a function of three plain objects;
12
+ * proving it should not require an `AgentManager`, a temp dir or a spawned child.
13
+ * - **The manager stays an orchestrator, not a calculator.** One helper serves BOTH
14
+ * spawn call sites (`startWorkflow`'s readiness loop and `continueWorkflow`), so the
15
+ * precedence rule exists exactly once.
16
+ * - **No runtime cycle.** Every import above is type-only and therefore erased, so the
17
+ * manager can import this module as a VALUE. `WorkflowUnknownAgentError` is declared
18
+ * here (the leaf) and re-exported from the manager, exactly as `WorkflowCycleError`
19
+ * is declared in `workflow-progress.ts` and re-exported.
20
+ *
21
+ * Why the merge copies only DEFINED keys rather than spreading:
22
+ * `{ ...base, ...preset, ...override }` lets an explicit `undefined` in a higher tier
23
+ * ERASE a defined lower-tier value — `{ model: undefined }` on a task would blank the
24
+ * workflow's model instead of leaving it alone. A tier states what it overrides; a key
25
+ * it does not set must be invisible.
26
+ *
27
+ * Why `cwd` is forced back to `baseConfig.cwd` as the final statement:
28
+ * the working directory is workflow-owned. A step relocating its own child would break
29
+ * the containment every other surface assumes. There are two independent guards, on
30
+ * purpose: `WorkflowAgentPresetSchema` rejects `cwd` at parse time (so it can never
31
+ * arrive over the wire), and this assignment wins over any hand-built object literal
32
+ * that bypassed the schema.
33
+ *
34
+ * Determinism rules, mirroring `workflow-progress.ts`: synchronous, no I/O, no
35
+ * `Date.now()` / `new Date()`, no snapshot read, no module-level mutable state.
36
+ */
37
+ /** Thrown when a task's `agentType` names an agent absent from the workflow's presets. */
38
+ export class WorkflowUnknownAgentError extends Error {
39
+ constructor(workflowId, taskId, agentName, knownAgents) {
40
+ super(`Task ${taskId} names unknown agent '${agentName}'. Known agents: ${knownAgents.length > 0 ? [...knownAgents].sort().join(", ") : "none"}.`);
41
+ this.workflowId = workflowId;
42
+ this.taskId = taskId;
43
+ this.agentName = agentName;
44
+ this.knownAgents = knownAgents;
45
+ this.name = "WorkflowUnknownAgentError";
46
+ }
47
+ }
48
+ /**
49
+ * Create-time validation: reject a graph whose any task names an agent nobody defined.
50
+ *
51
+ * Reports the FIRST offender in `graph.tasks` array order, so the error is stable for
52
+ * a given graph rather than dependent on object-key iteration. The message names the
53
+ * task id, the unknown name and the known names, because both launch boundaries
54
+ * (`session.ts` RPC, `mcp-server.ts` tool) forward `error.message` verbatim to the
55
+ * operator and that string is the whole diagnosis.
56
+ *
57
+ * Own-key lookup, never a bare `in`: a preset named `toString` or `constructor` must
58
+ * not be satisfied by `Object.prototype`.
59
+ */
60
+ export function assertKnownAgents(args) {
61
+ for (const task of args.graph.tasks) {
62
+ const agentName = task.agentType;
63
+ if (!agentName) {
64
+ continue;
65
+ }
66
+ if (!Object.prototype.hasOwnProperty.call(args.agentPresets, agentName)) {
67
+ throw new WorkflowUnknownAgentError(args.workflowId, task.id, agentName, Object.keys(args.agentPresets));
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Resolve the `AgentSessionConfig` one task's child is spawned with.
73
+ *
74
+ * Precedence, lowest to highest: `baseConfig`, then `agentPresets[node.agentType]`,
75
+ * then `node.agentConfig`. Only keys whose value is `!== undefined` are copied from a
76
+ * higher tier, so a preset that sets only `model` leaves `baseConfig.provider` intact.
77
+ *
78
+ * TOTAL by design: an `agentType` naming a missing preset returns `{ ...baseConfig }`
79
+ * rather than throwing. That path is unreachable after `assertKnownAgents` runs at
80
+ * create time, and a helper that threw on it would convert a validation gap into a
81
+ * mid-graph spawn crash.
82
+ */
83
+ export function resolveTaskConfig(args) {
84
+ const resolved = { ...args.baseConfig };
85
+ // Keyed write surface for the tier copy below. `AgentSessionConfig` has no index
86
+ // signature, so a per-key assignment needs this widening; the preset schema is a
87
+ // closed subset of that interface, so every key copied is a real config key.
88
+ const target = resolved;
89
+ const preset = args.node.agentType ? args.agentPresets[args.node.agentType] : undefined;
90
+ for (const tier of [preset, args.node.agentConfig]) {
91
+ if (!tier) {
92
+ continue;
93
+ }
94
+ for (const [key, value] of Object.entries(tier)) {
95
+ if (value !== undefined) {
96
+ target[key] = value;
97
+ }
98
+ }
99
+ }
100
+ // `cwd` is workflow-owned and never overridable. Two independent guards: the schema
101
+ // rejects `cwd` at parse time, and this assignment wins over any hand-built object.
102
+ resolved.cwd = args.baseConfig.cwd;
103
+ return resolved;
104
+ }
105
+ //# sourceMappingURL=workflow-agent-resolution.js.map