@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.
- package/dist/server/server/agent/agent-manager.d.ts +6 -0
- package/dist/server/server/agent/agent-manager.js +7 -1
- package/dist/server/server/agent/import-sessions.d.ts +6 -1
- package/dist/server/server/agent/import-sessions.js +27 -2
- package/dist/server/server/agent/mcp-server.js +6 -1
- package/dist/server/server/agent/providers/claude/agent.d.ts +16 -0
- package/dist/server/server/agent/providers/claude/agent.js +21 -2
- package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +20 -0
- package/dist/server/server/agent/providers/claude/background-work-kinds.js +14 -0
- package/dist/server/server/bootstrap.js +1 -0
- package/dist/server/server/session.js +1 -0
- package/dist/server/server/workflow/workflow-agent-resolution.d.ts +82 -0
- package/dist/server/server/workflow/workflow-agent-resolution.js +105 -0
- package/dist/server/server/workflow/workflow-manager.d.ts +155 -20
- package/dist/server/server/workflow/workflow-manager.js +439 -31
- package/dist/server/server/workflow/workflow-progress.d.ts +53 -0
- package/dist/server/server/workflow/workflow-progress.js +96 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-9c3bcdc334cf1c08001b6bea510e0292.js → index-6a99048b3a9acca9bc30efd44300414e.js} +7 -7
- package/dist/server/web-ui/_expo/static/js/web/index-6a99048b3a9acca9bc30efd44300414e.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-6a99048b3a9acca9bc30efd44300414e.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-9c3bcdc334cf1c08001b6bea510e0292.js.map.br → index-6a99048b3a9acca9bc30efd44300414e.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-9c3bcdc334cf1c08001b6bea510e0292.js.map.gz → index-6a99048b3a9acca9bc30efd44300414e.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.br +0 -0
- 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
|
|
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
|
|
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
|
-
|
|
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
|