macro-agent 0.1.8 → 0.1.10
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/CLAUDE.md +166 -33
- package/README.md +781 -131
- package/dist/acp/claude-code-replay.d.ts +11 -0
- package/dist/acp/claude-code-replay.d.ts.map +1 -0
- package/dist/acp/claude-code-replay.js +190 -0
- package/dist/acp/claude-code-replay.js.map +1 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +155 -6
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +9 -0
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager-v2.d.ts +21 -0
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +234 -43
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +12 -0
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +15 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/boot-v2.d.ts +41 -0
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +16 -1
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/index.js +56 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
- package/dist/cognitive/macro-agent-backend.js +40 -22
- package/dist/cognitive/macro-agent-backend.js.map +1 -1
- package/dist/integrations/skilltree.d.ts.map +1 -1
- package/dist/integrations/skilltree.js +1 -0
- package/dist/integrations/skilltree.js.map +1 -1
- package/dist/lifecycle/cleanup.d.ts +33 -2
- package/dist/lifecycle/cleanup.d.ts.map +1 -1
- package/dist/lifecycle/cleanup.js +28 -6
- package/dist/lifecycle/cleanup.js.map +1 -1
- package/dist/lifecycle/handlers-v2.d.ts +7 -0
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +28 -2
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/lifecycle/types.d.ts +11 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/map/acp-bridge.d.ts +9 -0
- package/dist/map/acp-bridge.d.ts.map +1 -1
- package/dist/map/acp-bridge.js +15 -2
- package/dist/map/acp-bridge.js.map +1 -1
- package/dist/map/cascade-bridge.d.ts +44 -0
- package/dist/map/cascade-bridge.d.ts.map +1 -0
- package/dist/map/cascade-bridge.js +257 -0
- package/dist/map/cascade-bridge.js.map +1 -0
- package/dist/map/lifecycle-bridge.d.ts +1 -1
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +58 -23
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +47 -6
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +33 -2
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +20 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +8 -0
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/teams/team-manager-v2.d.ts.map +1 -1
- package/dist/teams/team-manager-v2.js +26 -0
- package/dist/teams/team-manager-v2.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +16 -3
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/dist/workspace/config.d.ts +10 -10
- package/dist/workspace/config.d.ts.map +1 -1
- package/dist/workspace/config.js +4 -4
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/git-cascade-adapter.d.ts +510 -0
- package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
- package/dist/workspace/git-cascade-adapter.js +908 -0
- package/dist/workspace/git-cascade-adapter.js.map +1 -0
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +4 -4
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/landing/direct-push.d.ts +20 -0
- package/dist/workspace/landing/direct-push.d.ts.map +1 -0
- package/dist/workspace/landing/direct-push.js +74 -0
- package/dist/workspace/landing/direct-push.js.map +1 -0
- package/dist/workspace/landing/index.d.ts +29 -0
- package/dist/workspace/landing/index.d.ts.map +1 -0
- package/dist/workspace/landing/index.js +37 -0
- package/dist/workspace/landing/index.js.map +1 -0
- package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
- package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
- package/dist/workspace/landing/merge-to-parent.js +185 -0
- package/dist/workspace/landing/merge-to-parent.js.map +1 -0
- package/dist/workspace/landing/optimistic-push.d.ts +16 -0
- package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
- package/dist/workspace/landing/optimistic-push.js +27 -0
- package/dist/workspace/landing/optimistic-push.js.map +1 -0
- package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
- package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
- package/dist/workspace/landing/queue-to-branch.js +79 -0
- package/dist/workspace/landing/queue-to-branch.js.map +1 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
- package/dist/workspace/merge-queue/merge-queue.js +10 -0
- package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
- package/dist/workspace/merge-queue/types.d.ts +16 -2
- package/dist/workspace/merge-queue/types.d.ts.map +1 -1
- package/dist/workspace/merge-queue/types.js +9 -0
- package/dist/workspace/merge-queue/types.js.map +1 -1
- package/dist/workspace/pool/types.d.ts +1 -0
- package/dist/workspace/pool/types.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.js +1 -0
- package/dist/workspace/pool/worktree-pool.js.map +1 -1
- package/dist/workspace/recovery/abandon.d.ts +15 -0
- package/dist/workspace/recovery/abandon.d.ts.map +1 -0
- package/dist/workspace/recovery/abandon.js +45 -0
- package/dist/workspace/recovery/abandon.js.map +1 -0
- package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
- package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
- package/dist/workspace/recovery/auto-resolve.js +99 -0
- package/dist/workspace/recovery/auto-resolve.js.map +1 -0
- package/dist/workspace/recovery/defer.d.ts +15 -0
- package/dist/workspace/recovery/defer.d.ts.map +1 -0
- package/dist/workspace/recovery/defer.js +16 -0
- package/dist/workspace/recovery/defer.js.map +1 -0
- package/dist/workspace/recovery/escalate.d.ts +16 -0
- package/dist/workspace/recovery/escalate.d.ts.map +1 -0
- package/dist/workspace/recovery/escalate.js +24 -0
- package/dist/workspace/recovery/escalate.js.map +1 -0
- package/dist/workspace/recovery/index.d.ts +32 -0
- package/dist/workspace/recovery/index.d.ts.map +1 -0
- package/dist/workspace/recovery/index.js +45 -0
- package/dist/workspace/recovery/index.js.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.js +111 -0
- package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
- package/dist/workspace/recovery/types.d.ts +63 -0
- package/dist/workspace/recovery/types.d.ts.map +1 -0
- package/dist/workspace/recovery/types.js +12 -0
- package/dist/workspace/recovery/types.js.map +1 -0
- package/dist/workspace/topology/index.d.ts +9 -0
- package/dist/workspace/topology/index.d.ts.map +1 -0
- package/dist/workspace/topology/index.js +8 -0
- package/dist/workspace/topology/index.js.map +1 -0
- package/dist/workspace/topology/no-workspace.d.ts +18 -0
- package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
- package/dist/workspace/topology/no-workspace.js +25 -0
- package/dist/workspace/topology/no-workspace.js.map +1 -0
- package/dist/workspace/topology/types.d.ts +97 -0
- package/dist/workspace/topology/types.d.ts.map +1 -0
- package/dist/workspace/topology/types.js +20 -0
- package/dist/workspace/topology/types.js.map +1 -0
- package/dist/workspace/topology/yaml-driven.d.ts +69 -0
- package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
- package/dist/workspace/topology/yaml-driven.js +273 -0
- package/dist/workspace/topology/yaml-driven.js.map +1 -0
- package/dist/workspace/types-v3.d.ts +110 -0
- package/dist/workspace/types-v3.d.ts.map +1 -0
- package/dist/workspace/types-v3.js +20 -0
- package/dist/workspace/types-v3.js.map +1 -0
- package/dist/workspace/types.d.ts +145 -17
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +92 -13
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +373 -13
- package/dist/workspace/workspace-manager.js.map +1 -1
- package/dist/workspace/yaml-schema.d.ts +254 -0
- package/dist/workspace/yaml-schema.d.ts.map +1 -0
- package/dist/workspace/yaml-schema.js +170 -0
- package/dist/workspace/yaml-schema.js.map +1 -0
- package/docs/conflict-recovery.md +472 -0
- package/docs/git-cascade-integration-gaps.md +678 -0
- package/docs/workspace-interfaces.md +731 -0
- package/docs/workspace-redesign-plan.md +302 -0
- package/package.json +4 -4
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
- package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
- package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
- package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
- package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
- package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
- package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
- package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
- package/src/acp/__tests__/macro-agent.test.ts +39 -1
- package/src/acp/claude-code-replay.ts +208 -0
- package/src/acp/macro-agent.ts +167 -9
- package/src/acp/types.ts +10 -0
- package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
- package/src/agent/__tests__/agent-manager-v2.test.ts +66 -0
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +293 -48
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +68 -1
- package/src/cli/index.ts +61 -0
- package/src/cognitive/macro-agent-backend.ts +45 -29
- package/src/integrations/skilltree.ts +1 -0
- package/src/lifecycle/cleanup.ts +52 -3
- package/src/lifecycle/handlers-v2.ts +40 -3
- package/src/lifecycle/types.ts +12 -0
- package/src/map/__tests__/cascade-bridge.test.ts +229 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-bridge.ts +301 -0
- package/src/map/lifecycle-bridge.ts +52 -17
- package/src/map/server.ts +47 -6
- package/src/map/sidecar.ts +31 -1
- package/src/map/types.ts +20 -0
- package/src/mcp/tools/done-v2.ts +9 -0
- package/src/teams/team-manager-v2.ts +37 -0
- package/src/teams/team-runtime-v2.ts +23 -3
- package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
- package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
- package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
- package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
- package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
- package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
- package/src/workspace/config.ts +11 -11
- package/src/workspace/git-cascade-adapter.ts +1186 -0
- package/src/workspace/index.ts +11 -11
- package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
- package/src/workspace/landing/direct-push.ts +91 -0
- package/src/workspace/landing/index.ts +40 -0
- package/src/workspace/landing/merge-to-parent.ts +228 -0
- package/src/workspace/landing/optimistic-push.ts +36 -0
- package/src/workspace/landing/queue-to-branch.ts +108 -0
- package/src/workspace/merge-queue/merge-queue.ts +10 -0
- package/src/workspace/merge-queue/types.ts +16 -2
- package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
- package/src/workspace/pool/types.ts +1 -0
- package/src/workspace/pool/worktree-pool.ts +1 -0
- package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
- package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
- package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
- package/src/workspace/recovery/abandon.ts +51 -0
- package/src/workspace/recovery/auto-resolve.ts +119 -0
- package/src/workspace/recovery/defer.ts +23 -0
- package/src/workspace/recovery/escalate.ts +30 -0
- package/src/workspace/recovery/index.ts +58 -0
- package/src/workspace/recovery/spawn-resolver.ts +145 -0
- package/src/workspace/recovery/types.ts +54 -0
- package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
- package/src/workspace/topology/index.ts +18 -0
- package/src/workspace/topology/no-workspace.ts +39 -0
- package/src/workspace/topology/types.ts +116 -0
- package/src/workspace/topology/yaml-driven.ts +316 -0
- package/src/workspace/types-v3.ts +155 -0
- package/src/workspace/types.ts +191 -20
- package/src/workspace/workspace-manager.ts +474 -19
- package/src/workspace/yaml-schema.ts +216 -0
- package/src/workspace/dataplane-adapter.ts +0 -546
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AgentManagerV2's three-level TaskRef resolution:
|
|
3
|
+
* 1. Explicit SpawnAgentOptions.taskRef wins.
|
|
4
|
+
* 2. resolveTaskRef(opts) runs when no explicit ref.
|
|
5
|
+
* 3. Fallback to taskResourceId + options.task_id when both above yield nothing.
|
|
6
|
+
*
|
|
7
|
+
* Verifies the G3/G14 closure: single-graph default via boot config + multi-graph
|
|
8
|
+
* resolver for deployments that touch more than one opentasks graph.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { createAgentManagerV2 } from "../agent-manager-v2.js";
|
|
13
|
+
import { AgentStore } from "../agent-store.js";
|
|
14
|
+
import type { AgentManager } from "../agent-manager.js";
|
|
15
|
+
import type { InboxAdapter, TasksAdapter } from "../../adapters/types.js";
|
|
16
|
+
import type { SpawnAgentOptions } from "../types.js";
|
|
17
|
+
import type { TaskRef } from "git-cascade/events";
|
|
18
|
+
|
|
19
|
+
vi.mock("acp-factory", () => ({
|
|
20
|
+
AgentFactory: {
|
|
21
|
+
spawn: vi.fn().mockResolvedValue({
|
|
22
|
+
createSession: vi.fn().mockResolvedValue({
|
|
23
|
+
id: "provider-session-1",
|
|
24
|
+
prompt: vi.fn().mockReturnValue({
|
|
25
|
+
[Symbol.asyncIterator]: () => ({
|
|
26
|
+
next: () => Promise.resolve({ done: true, value: undefined }),
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
forkWithFlush: vi.fn().mockResolvedValue({ id: "forked-session-1" }),
|
|
30
|
+
}),
|
|
31
|
+
loadSession: vi.fn(),
|
|
32
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
isRunning: vi.fn().mockReturnValue(true),
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
function mockInbox(): InboxAdapter {
|
|
39
|
+
return {
|
|
40
|
+
registerAgent: vi.fn().mockResolvedValue(undefined),
|
|
41
|
+
deregisterAgent: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
send: vi.fn().mockResolvedValue("msg-1"),
|
|
43
|
+
onDelivery: vi.fn(),
|
|
44
|
+
offDelivery: vi.fn(),
|
|
45
|
+
checkInbox: vi.fn().mockResolvedValue([]),
|
|
46
|
+
readThread: vi.fn().mockResolvedValue([]),
|
|
47
|
+
setSignalFilter: vi.fn(),
|
|
48
|
+
setEmissionValidator: vi.fn(),
|
|
49
|
+
socketPath: "/tmp/test-inbox.sock",
|
|
50
|
+
stop: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
} as unknown as InboxAdapter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mockTasks(): TasksAdapter {
|
|
55
|
+
return {
|
|
56
|
+
createTask: vi.fn().mockResolvedValue("ot-task-1"),
|
|
57
|
+
assignTask: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
transitionTask: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
getTask: vi.fn().mockResolvedValue({ id: "t-1", title: "x", status: "open" }),
|
|
60
|
+
queryReady: vi.fn().mockResolvedValue([]),
|
|
61
|
+
listTasks: vi.fn().mockResolvedValue([]),
|
|
62
|
+
addBlocker: vi.fn().mockResolvedValue(undefined),
|
|
63
|
+
removeBlocker: vi.fn().mockResolvedValue(undefined),
|
|
64
|
+
claimTask: vi.fn().mockResolvedValue(null),
|
|
65
|
+
unclaimTask: vi.fn().mockResolvedValue(undefined),
|
|
66
|
+
listClaimable: vi.fn().mockResolvedValue([]),
|
|
67
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
68
|
+
disconnect: vi.fn(),
|
|
69
|
+
connected: true,
|
|
70
|
+
} as unknown as TasksAdapter;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("AgentManagerV2 taskRef resolution", () => {
|
|
74
|
+
let agentStore: AgentStore;
|
|
75
|
+
let inbox: InboxAdapter;
|
|
76
|
+
let tasks: TasksAdapter;
|
|
77
|
+
let manager: AgentManager;
|
|
78
|
+
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
await manager?.close();
|
|
81
|
+
agentStore?.close();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
agentStore = new AgentStore(":memory:");
|
|
86
|
+
inbox = mockInbox();
|
|
87
|
+
tasks = mockTasks();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function getStoredTaskRef(agentId: string): TaskRef | undefined {
|
|
91
|
+
const record = agentStore.getAgent(agentId);
|
|
92
|
+
const meta = record?.metadata as Record<string, unknown> | undefined;
|
|
93
|
+
return meta?.task_ref as TaskRef | undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it("uses taskResourceId + task_id when no other source resolves a ref", async () => {
|
|
97
|
+
manager = createAgentManagerV2(agentStore, inbox, tasks, {
|
|
98
|
+
defaultCwd: "/tmp/t",
|
|
99
|
+
taskResourceId: "resource-default",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const result = await manager.spawn({
|
|
103
|
+
task: "work",
|
|
104
|
+
task_id: "node-42" as unknown as SpawnAgentOptions["task_id"],
|
|
105
|
+
role: "worker",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(getStoredTaskRef(result.id)).toEqual({
|
|
109
|
+
resource_id: "resource-default",
|
|
110
|
+
node_id: "node-42",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("falls through to no taskRef when neither resolver nor taskResourceId apply", async () => {
|
|
115
|
+
manager = createAgentManagerV2(agentStore, inbox, tasks, {
|
|
116
|
+
defaultCwd: "/tmp/t",
|
|
117
|
+
// No taskResourceId, no resolveTaskRef.
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await manager.spawn({ task: "work", role: "worker" });
|
|
121
|
+
|
|
122
|
+
expect(getStoredTaskRef(result.id)).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("resolveTaskRef wins over the taskResourceId fallback", async () => {
|
|
126
|
+
const resolveTaskRef = vi
|
|
127
|
+
.fn()
|
|
128
|
+
.mockReturnValue({ resource_id: "resource-from-resolver", node_id: "n1" });
|
|
129
|
+
|
|
130
|
+
manager = createAgentManagerV2(agentStore, inbox, tasks, {
|
|
131
|
+
defaultCwd: "/tmp/t",
|
|
132
|
+
taskResourceId: "resource-default",
|
|
133
|
+
resolveTaskRef,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await manager.spawn({ task: "work", role: "worker" });
|
|
137
|
+
|
|
138
|
+
expect(resolveTaskRef).toHaveBeenCalledOnce();
|
|
139
|
+
expect(getStoredTaskRef(result.id)).toEqual({
|
|
140
|
+
resource_id: "resource-from-resolver",
|
|
141
|
+
node_id: "n1",
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("resolveTaskRef returning undefined falls through to taskResourceId", async () => {
|
|
146
|
+
const resolveTaskRef = vi.fn().mockReturnValue(undefined);
|
|
147
|
+
|
|
148
|
+
manager = createAgentManagerV2(agentStore, inbox, tasks, {
|
|
149
|
+
defaultCwd: "/tmp/t",
|
|
150
|
+
taskResourceId: "resource-default",
|
|
151
|
+
resolveTaskRef,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await manager.spawn({
|
|
155
|
+
task: "work",
|
|
156
|
+
task_id: "node-9" as unknown as SpawnAgentOptions["task_id"],
|
|
157
|
+
role: "worker",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(resolveTaskRef).toHaveBeenCalledOnce();
|
|
161
|
+
expect(getStoredTaskRef(result.id)).toEqual({
|
|
162
|
+
resource_id: "resource-default",
|
|
163
|
+
node_id: "node-9",
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("explicit SpawnAgentOptions.taskRef wins over resolver AND taskResourceId", async () => {
|
|
168
|
+
const resolveTaskRef = vi
|
|
169
|
+
.fn()
|
|
170
|
+
.mockReturnValue({ resource_id: "resource-from-resolver", node_id: "r1" });
|
|
171
|
+
|
|
172
|
+
manager = createAgentManagerV2(agentStore, inbox, tasks, {
|
|
173
|
+
defaultCwd: "/tmp/t",
|
|
174
|
+
taskResourceId: "resource-default",
|
|
175
|
+
resolveTaskRef,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const explicit: TaskRef = { resource_id: "resource-explicit", node_id: "e1" };
|
|
179
|
+
const result = await manager.spawn({
|
|
180
|
+
task: "work",
|
|
181
|
+
role: "worker",
|
|
182
|
+
taskRef: explicit,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Resolver should NOT be called when caller already supplied taskRef.
|
|
186
|
+
expect(resolveTaskRef).not.toHaveBeenCalled();
|
|
187
|
+
expect(getStoredTaskRef(result.id)).toEqual(explicit);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("resolver receives the intercepted SpawnAgentOptions (post-interceptor)", async () => {
|
|
191
|
+
const resolveTaskRef = vi.fn().mockReturnValue(undefined);
|
|
192
|
+
|
|
193
|
+
manager = createAgentManagerV2(agentStore, inbox, tasks, {
|
|
194
|
+
defaultCwd: "/tmp/t",
|
|
195
|
+
resolveTaskRef,
|
|
196
|
+
});
|
|
197
|
+
manager.setSpawnInterceptor((opts) => ({ ...opts, cwd: "/overridden/cwd" }));
|
|
198
|
+
|
|
199
|
+
await manager.spawn({ task: "x", role: "worker" });
|
|
200
|
+
|
|
201
|
+
expect(resolveTaskRef).toHaveBeenCalledOnce();
|
|
202
|
+
const passed = resolveTaskRef.mock.calls[0][0];
|
|
203
|
+
expect(passed.cwd).toBe("/overridden/cwd");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("resolver throwing does not block spawn; falls through to taskResourceId", async () => {
|
|
207
|
+
const resolveTaskRef = vi.fn().mockImplementation(() => {
|
|
208
|
+
throw new Error("resolver exploded");
|
|
209
|
+
});
|
|
210
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
211
|
+
|
|
212
|
+
manager = createAgentManagerV2(agentStore, inbox, tasks, {
|
|
213
|
+
defaultCwd: "/tmp/t",
|
|
214
|
+
taskResourceId: "resource-safe",
|
|
215
|
+
resolveTaskRef,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const result = await manager.spawn({
|
|
219
|
+
task: "x",
|
|
220
|
+
task_id: "safe-node" as unknown as SpawnAgentOptions["task_id"],
|
|
221
|
+
role: "worker",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
225
|
+
expect(getStoredTaskRef(result.id)).toEqual({
|
|
226
|
+
resource_id: "resource-safe",
|
|
227
|
+
node_id: "safe-node",
|
|
228
|
+
});
|
|
229
|
+
warnSpy.mockRestore();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -116,6 +116,29 @@ export interface AgentManagerV2Config {
|
|
|
116
116
|
serverToken?: string;
|
|
117
117
|
/** Control socket path for MCP subprocess lifecycle RPC */
|
|
118
118
|
controlSocketPath?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Default opentasks resource ID hosted on the OpenHive hub. When set,
|
|
121
|
+
* spawn paths build `taskRef = { resource_id: <this>, node_id: task_id }`
|
|
122
|
+
* automatically from `SpawnAgentOptions.task_id` (for any spawn where
|
|
123
|
+
* `resolveTaskRef` returned undefined AND the caller didn't supply an
|
|
124
|
+
* explicit `taskRef`).
|
|
125
|
+
*
|
|
126
|
+
* Operators set this once at swarm registration for the common
|
|
127
|
+
* single-graph case. Multi-graph deployments should use `resolveTaskRef`
|
|
128
|
+
* instead.
|
|
129
|
+
*/
|
|
130
|
+
taskResourceId?: string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Multi-graph resolver. Called at every spawn; return a `TaskRef` to set
|
|
134
|
+
* the binding or `undefined` to fall through to the `taskResourceId`
|
|
135
|
+
* default. Explicit `SpawnAgentOptions.taskRef` always wins over both.
|
|
136
|
+
*
|
|
137
|
+
* Keep cheap — runs per-spawn.
|
|
138
|
+
*/
|
|
139
|
+
resolveTaskRef?: (
|
|
140
|
+
spawnOptions: SpawnAgentOptions
|
|
141
|
+
) => import("git-cascade/events").TaskRef | undefined;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -139,6 +162,8 @@ export function createAgentManagerV2(
|
|
|
139
162
|
serverToken,
|
|
140
163
|
agentTokenManager,
|
|
141
164
|
controlSocketPath,
|
|
165
|
+
taskResourceId,
|
|
166
|
+
resolveTaskRef,
|
|
142
167
|
} = config;
|
|
143
168
|
|
|
144
169
|
// In-memory state
|
|
@@ -158,6 +183,12 @@ export function createAgentManagerV2(
|
|
|
158
183
|
// MAP sidecar reference for trajectory reporting (set via setSidecar)
|
|
159
184
|
let sidecarRef: { connected: boolean; reportCheckpoint(cp: any): Promise<any> } | null = null;
|
|
160
185
|
|
|
186
|
+
// TopologyPolicy for workspace allocation (Phase 3+); set via setTopologyPolicy.
|
|
187
|
+
// When null, createWorkspaceForRole falls back to legacy role-name dispatch.
|
|
188
|
+
let topologyPolicy:
|
|
189
|
+
| import('../workspace/topology/types.js').TopologyPolicy
|
|
190
|
+
| null = null;
|
|
191
|
+
|
|
161
192
|
// ── Helpers ──────────────────────────────────────────────────
|
|
162
193
|
|
|
163
194
|
function notifyLifecycle(event: AgentLifecycleEvent): void {
|
|
@@ -247,6 +278,109 @@ export function createAgentManagerV2(
|
|
|
247
278
|
|
|
248
279
|
// ── Workspace Helper ─────────────────────────────────────────
|
|
249
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Execute a TopologyPolicy decision against the WorkspaceManager.
|
|
283
|
+
*
|
|
284
|
+
* Translates declarative `WorkspaceDecision` into concrete workspace
|
|
285
|
+
* allocations. Returns a `Workspace` compatible with the legacy shape
|
|
286
|
+
* so the rest of AgentManagerV2 doesn't need to change.
|
|
287
|
+
*/
|
|
288
|
+
async function executeWorkspaceDecision(
|
|
289
|
+
agentId: AgentId,
|
|
290
|
+
decision: import('../workspace/topology/types.js').WorkspaceDecision,
|
|
291
|
+
role?: string,
|
|
292
|
+
spawnOptions?: SpawnAgentOptions
|
|
293
|
+
): Promise<Workspace | undefined> {
|
|
294
|
+
if (!workspaceManager) return undefined;
|
|
295
|
+
|
|
296
|
+
switch (decision.kind) {
|
|
297
|
+
case 'none':
|
|
298
|
+
case 'share-parent-cwd':
|
|
299
|
+
return undefined;
|
|
300
|
+
|
|
301
|
+
case 'share-with-agent': {
|
|
302
|
+
const worktree = workspaceManager.allocateWorktree({
|
|
303
|
+
agentId,
|
|
304
|
+
sharedWithAgent: decision.agentId,
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
agentId,
|
|
308
|
+
path: worktree.path,
|
|
309
|
+
branch: worktree.currentStream
|
|
310
|
+
? `stream/${worktree.currentStream}`
|
|
311
|
+
: 'unknown',
|
|
312
|
+
streamId: worktree.currentStream ?? '',
|
|
313
|
+
role: 'v3', // V3 path — bypass legacy worker task/merge-queue flows
|
|
314
|
+
createdAt: worktree.createdAt,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case 'attach-to-stream': {
|
|
319
|
+
// Record even if no worktree — the topology needs the stream↔role
|
|
320
|
+
// mapping for event-driven features like on_parent_advanced.
|
|
321
|
+
const attachPolicy = topologyPolicy as unknown as {
|
|
322
|
+
recordAgentStream?: (a: string, s: string, role?: string) => void;
|
|
323
|
+
};
|
|
324
|
+
attachPolicy.recordAgentStream?.(agentId, decision.streamId, role);
|
|
325
|
+
|
|
326
|
+
if (!decision.allocateWorktree) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
const worktree = workspaceManager.allocateWorktree({
|
|
330
|
+
agentId,
|
|
331
|
+
streamId: decision.streamId,
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
agentId,
|
|
335
|
+
path: worktree.path,
|
|
336
|
+
branch: `stream/${decision.streamId}`,
|
|
337
|
+
streamId: decision.streamId,
|
|
338
|
+
role: 'v3', // V3 path — attach-to-team-root
|
|
339
|
+
createdAt: worktree.createdAt,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case 'new-stream': {
|
|
344
|
+
// If the spawning agent has a taskRef and the streamSpec doesn't
|
|
345
|
+
// already carry one, weave it into metadata so the resulting stream
|
|
346
|
+
// binds to the OpenTasks node. Explicit streamSpec.metadata.task_ref
|
|
347
|
+
// wins.
|
|
348
|
+
const taskRef = spawnOptions?.taskRef;
|
|
349
|
+
const existingMeta = decision.streamSpec.metadata as
|
|
350
|
+
| Record<string, unknown>
|
|
351
|
+
| undefined;
|
|
352
|
+
const streamSpec = taskRef && !existingMeta?.task_ref
|
|
353
|
+
? {
|
|
354
|
+
...decision.streamSpec,
|
|
355
|
+
metadata: { ...(existingMeta ?? {}), task_ref: taskRef },
|
|
356
|
+
}
|
|
357
|
+
: decision.streamSpec;
|
|
358
|
+
const streamId = workspaceManager.createStreamV3(streamSpec);
|
|
359
|
+
// Record the mapping in the topology if it supports it (for share-with lookup).
|
|
360
|
+
const policy = topologyPolicy as unknown as {
|
|
361
|
+
recordAgentStream?: (a: string, s: string, role?: string) => void;
|
|
362
|
+
};
|
|
363
|
+
policy.recordAgentStream?.(agentId, streamId, role);
|
|
364
|
+
|
|
365
|
+
if (!decision.allocateWorktree) {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
const worktree = workspaceManager.allocateWorktree({
|
|
369
|
+
agentId,
|
|
370
|
+
streamId,
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
agentId,
|
|
374
|
+
path: worktree.path,
|
|
375
|
+
branch: `stream/${streamId}`,
|
|
376
|
+
streamId,
|
|
377
|
+
role: 'v3', // V3 path — new-stream
|
|
378
|
+
createdAt: worktree.createdAt,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
250
384
|
async function createWorkspaceForRole(
|
|
251
385
|
agentId: AgentId,
|
|
252
386
|
role: string,
|
|
@@ -254,56 +388,92 @@ export function createAgentManagerV2(
|
|
|
254
388
|
): Promise<Workspace | undefined> {
|
|
255
389
|
if (!workspaceManager) return undefined;
|
|
256
390
|
|
|
391
|
+
// V3 path — TopologyPolicy-driven. Set by boot-v2 when team YAML has
|
|
392
|
+
// `macro_agent.workspace`. When set, this takes precedence over the legacy
|
|
393
|
+
// capability/role-name dispatch below.
|
|
394
|
+
if (topologyPolicy) {
|
|
395
|
+
const decision = await topologyPolicy.onAgentSpawn({
|
|
396
|
+
agentId,
|
|
397
|
+
role,
|
|
398
|
+
parentAgentId: options.parent ?? undefined,
|
|
399
|
+
parentStreamId: options.streamId,
|
|
400
|
+
teamStreamId: (() => {
|
|
401
|
+
const stream = (
|
|
402
|
+
topologyPolicy as { getAgentStream?: (a: AgentId) => string | null }
|
|
403
|
+
).getAgentStream?.(agentId);
|
|
404
|
+
return stream ?? undefined;
|
|
405
|
+
})(),
|
|
406
|
+
workspaceManager,
|
|
407
|
+
getAgentByRole: (r: string) => {
|
|
408
|
+
for (const [aid, ws] of agentWorkspaces) {
|
|
409
|
+
const rec = agentStore.getAgent(aid);
|
|
410
|
+
if (rec?.role === r) return aid;
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
return executeWorkspaceDecision(agentId, decision, role, options);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Capability-based dispatch for programmatic callers that don't use
|
|
419
|
+
// team YAML. This is the supported path for libraries that construct
|
|
420
|
+
// WorkspaceManager + GitCascadeAdapter directly and spawn agents with
|
|
421
|
+
// explicit `capabilities` + `streamId` arguments. It coexists with the
|
|
422
|
+
// V3 topology path above.
|
|
423
|
+
return capabilityBasedDispatch(agentId, options, workspaceManager);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Capability-based workspace allocation for programmatic callers.
|
|
428
|
+
*
|
|
429
|
+
* Matches on `workspace.stream` / `workspace.integrate` / `workspace.worktree`
|
|
430
|
+
* capabilities + corresponding streamId/streamConfig args. Delegates to the
|
|
431
|
+
* role-shaped WorkspaceManager methods (createWorkerWorkspace,
|
|
432
|
+
* createIntegratorWorkspace, createCoordinatorWorkspace).
|
|
433
|
+
*
|
|
434
|
+
* Not used by team-YAML-driven teams — those go through TopologyPolicy above.
|
|
435
|
+
*/
|
|
436
|
+
async function capabilityBasedDispatch(
|
|
437
|
+
agentId: AgentId,
|
|
438
|
+
options: SpawnAgentOptions,
|
|
439
|
+
ws: WorkspaceManager
|
|
440
|
+
): Promise<Workspace | undefined> {
|
|
257
441
|
const capabilities = options.capabilities ?? [];
|
|
258
442
|
const streamId = options.streamId;
|
|
259
|
-
|
|
260
|
-
|
|
443
|
+
// Merge taskRef (if set at spawn time) into streamConfig.metadata so that
|
|
444
|
+
// adapter.createStream → x-cascade/stream.opened carries the binding to
|
|
445
|
+
// OpenTasks. Explicit streamConfig.metadata.task_ref wins if already set.
|
|
446
|
+
const streamConfig = options.streamConfig
|
|
447
|
+
? options.taskRef &&
|
|
448
|
+
!(options.streamConfig.metadata &&
|
|
449
|
+
(options.streamConfig.metadata as Record<string, unknown>).task_ref)
|
|
450
|
+
? {
|
|
451
|
+
...options.streamConfig,
|
|
452
|
+
metadata: {
|
|
453
|
+
...(options.streamConfig.metadata ?? {}),
|
|
454
|
+
task_ref: options.taskRef,
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
: options.streamConfig
|
|
458
|
+
: undefined;
|
|
459
|
+
const gitCascadeTaskId = options.gitCascadeTaskId;
|
|
261
460
|
|
|
262
|
-
// Capability-based dispatch
|
|
263
461
|
if (capabilities.includes("workspace.stream") && streamConfig) {
|
|
264
|
-
const newStreamId =
|
|
265
|
-
|
|
266
|
-
streamConfig
|
|
267
|
-
);
|
|
268
|
-
return workspaceManager.createCoordinatorWorkspace(agentId, newStreamId);
|
|
462
|
+
const newStreamId = ws.createIntegrationStream(agentId, streamConfig);
|
|
463
|
+
return ws.createCoordinatorWorkspace(agentId, newStreamId);
|
|
269
464
|
}
|
|
270
465
|
|
|
271
466
|
if (capabilities.includes("workspace.integrate") && streamId) {
|
|
272
|
-
return
|
|
467
|
+
return ws.createIntegratorWorkspace(agentId, streamId);
|
|
273
468
|
}
|
|
274
469
|
|
|
275
470
|
if (capabilities.includes("workspace.worktree") && streamId) {
|
|
276
|
-
const taskId =
|
|
277
|
-
return
|
|
471
|
+
const taskId = gitCascadeTaskId ?? agentId;
|
|
472
|
+
return ws.createWorkerWorkspace(agentId, taskId, streamId);
|
|
278
473
|
}
|
|
279
474
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
case "coordinator":
|
|
283
|
-
if (streamConfig) {
|
|
284
|
-
const sid = workspaceManager.createIntegrationStream(
|
|
285
|
-
agentId,
|
|
286
|
-
streamConfig
|
|
287
|
-
);
|
|
288
|
-
return workspaceManager.createCoordinatorWorkspace(agentId, sid);
|
|
289
|
-
}
|
|
290
|
-
return undefined;
|
|
291
|
-
case "integrator":
|
|
292
|
-
if (streamId) {
|
|
293
|
-
return workspaceManager.createIntegratorWorkspace(agentId, streamId);
|
|
294
|
-
}
|
|
295
|
-
return undefined;
|
|
296
|
-
case "worker":
|
|
297
|
-
case "worker.resolver": {
|
|
298
|
-
if (streamId) {
|
|
299
|
-
const tid = dataplaneTaskId ?? agentId;
|
|
300
|
-
return workspaceManager.createWorkerWorkspace(agentId, tid, streamId);
|
|
301
|
-
}
|
|
302
|
-
return undefined;
|
|
303
|
-
}
|
|
304
|
-
default:
|
|
305
|
-
return undefined;
|
|
306
|
-
}
|
|
475
|
+
// No matching capability — agent inherits parent cwd (no workspace)
|
|
476
|
+
return undefined;
|
|
307
477
|
}
|
|
308
478
|
|
|
309
479
|
// ── Core Lifecycle ───────────────────────────────────────────
|
|
@@ -317,10 +487,40 @@ export function createAgentManagerV2(
|
|
|
317
487
|
}
|
|
318
488
|
|
|
319
489
|
// Apply spawn interceptor (set by TeamRuntime)
|
|
320
|
-
const
|
|
490
|
+
const interceptedOptions = spawnInterceptor
|
|
321
491
|
? await spawnInterceptor(rawOptions)
|
|
322
492
|
: rawOptions;
|
|
323
493
|
|
|
494
|
+
// Resolve taskRef with three-level precedence:
|
|
495
|
+
// 1. Explicit `options.taskRef` (caller knows exactly what graph).
|
|
496
|
+
// 2. `resolveTaskRef(opts)` (multi-graph deployments decide per spawn).
|
|
497
|
+
// 3. `taskResourceId` + `options.task_id` (single-graph default).
|
|
498
|
+
// If none resolves, spawn proceeds with no taskRef — cascade events
|
|
499
|
+
// land without a task binding (hub back-fills from first commit that
|
|
500
|
+
// carries one, if any).
|
|
501
|
+
let resolvedTaskRef = interceptedOptions.taskRef;
|
|
502
|
+
if (!resolvedTaskRef && resolveTaskRef) {
|
|
503
|
+
try {
|
|
504
|
+
resolvedTaskRef = resolveTaskRef(interceptedOptions);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
// Resolver failures must not block spawn. Log + fall through.
|
|
507
|
+
// eslint-disable-next-line no-console
|
|
508
|
+
console.warn(
|
|
509
|
+
"[agent-manager-v2] resolveTaskRef threw; falling back to taskResourceId default:",
|
|
510
|
+
err instanceof Error ? err.message : err
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (!resolvedTaskRef && taskResourceId && interceptedOptions.task_id) {
|
|
515
|
+
resolvedTaskRef = {
|
|
516
|
+
resource_id: taskResourceId,
|
|
517
|
+
node_id: String(interceptedOptions.task_id),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const options = resolvedTaskRef === interceptedOptions.taskRef
|
|
521
|
+
? interceptedOptions
|
|
522
|
+
: { ...interceptedOptions, taskRef: resolvedTaskRef };
|
|
523
|
+
|
|
324
524
|
const {
|
|
325
525
|
task,
|
|
326
526
|
task_id,
|
|
@@ -404,7 +604,9 @@ export function createAgentManagerV2(
|
|
|
404
604
|
systemPrompt += `\n\n${interactionPatterns.join("\n\n")}`;
|
|
405
605
|
}
|
|
406
606
|
|
|
407
|
-
// Persist agent in store
|
|
607
|
+
// Persist agent in store. Stash taskRef in metadata so done()'s
|
|
608
|
+
// lifecycle context can read it without separate plumbing — this is the
|
|
609
|
+
// path that makes per-commit task_ref binding work end-to-end.
|
|
408
610
|
const now = Date.now() as Timestamp;
|
|
409
611
|
const agentRecord: AgentRecord = {
|
|
410
612
|
id: agentId,
|
|
@@ -422,7 +624,7 @@ export function createAgentManagerV2(
|
|
|
422
624
|
created_at: now,
|
|
423
625
|
started_at: now,
|
|
424
626
|
config: agentConfig as Record<string, unknown>,
|
|
425
|
-
metadata: {},
|
|
627
|
+
metadata: options.taskRef ? { task_ref: options.taskRef } : {},
|
|
426
628
|
};
|
|
427
629
|
agentStore.putAgent(agentRecord);
|
|
428
630
|
|
|
@@ -456,13 +658,13 @@ export function createAgentManagerV2(
|
|
|
456
658
|
if (workspace) {
|
|
457
659
|
agentWorkspaces.set(agentId, workspace);
|
|
458
660
|
|
|
459
|
-
// Create and claim
|
|
661
|
+
// Create and claim git-cascade task for workers
|
|
460
662
|
if (
|
|
461
663
|
workspace.role === "worker" &&
|
|
462
664
|
workspace.streamId &&
|
|
463
665
|
workspaceManager
|
|
464
666
|
) {
|
|
465
|
-
const dpTaskId = options.
|
|
667
|
+
const dpTaskId = options.gitCascadeTaskId ?? agentId;
|
|
466
668
|
workspaceManager.createTask(workspace.streamId, {
|
|
467
669
|
title: task ?? `Task for ${agentId}`,
|
|
468
670
|
});
|
|
@@ -588,9 +790,11 @@ export function createAgentManagerV2(
|
|
|
588
790
|
created_at: now,
|
|
589
791
|
});
|
|
590
792
|
|
|
591
|
-
// Update agent with provider session ID
|
|
793
|
+
// Update agent with provider session ID. Merge with existing metadata
|
|
794
|
+
// so fields set at spawn time (e.g. task_ref) aren't clobbered.
|
|
795
|
+
const existingMeta = agentStore.getAgent(agentId)?.metadata ?? {};
|
|
592
796
|
agentStore.updateAgent(agentId, {
|
|
593
|
-
metadata: { provider_session_id: session.id },
|
|
797
|
+
metadata: { ...existingMeta, provider_session_id: session.id },
|
|
594
798
|
});
|
|
595
799
|
|
|
596
800
|
// Register agent in inbox
|
|
@@ -1134,12 +1338,21 @@ export function createAgentManagerV2(
|
|
|
1134
1338
|
async function getOrCreateHeadManager(
|
|
1135
1339
|
options: HeadManagerOptions
|
|
1136
1340
|
): Promise<SpawnedAgent> {
|
|
1137
|
-
// Check for existing head manager
|
|
1341
|
+
// Check for an existing head manager matching this cwd that ALSO has a
|
|
1342
|
+
// live session in this process. The activeSessions check has to be inside
|
|
1343
|
+
// the predicate (not after .find) — the agentStore is persistent across
|
|
1344
|
+
// process restarts, so without this filter we'd match stale "running"
|
|
1345
|
+
// records from previous processes whose sessions are gone, then fall
|
|
1346
|
+
// through to spawn() and create a duplicate coordinator.
|
|
1138
1347
|
const existing = agentStore
|
|
1139
1348
|
.listAgents({ parent_id: null, state: "running" })
|
|
1140
|
-
.find(
|
|
1349
|
+
.find(
|
|
1350
|
+
(a) =>
|
|
1351
|
+
a.cwd === options.cwd &&
|
|
1352
|
+
activeSessions.has(a.id as AgentId),
|
|
1353
|
+
);
|
|
1141
1354
|
|
|
1142
|
-
if (existing
|
|
1355
|
+
if (existing) {
|
|
1143
1356
|
const sessionEntry = activeSessions.get(existing.id as AgentId)!;
|
|
1144
1357
|
const storedSession = agentStore.getSession(existing.id as AgentId);
|
|
1145
1358
|
return {
|
|
@@ -1167,6 +1380,30 @@ export function createAgentManagerV2(
|
|
|
1167
1380
|
.map(agentRecordToAgent);
|
|
1168
1381
|
}
|
|
1169
1382
|
|
|
1383
|
+
/**
|
|
1384
|
+
* Look up the spawned-agent shape for any agent that's still alive in this
|
|
1385
|
+
* process (any role, not just coordinators). Returns null if the agent
|
|
1386
|
+
* doesn't exist, isn't running, or has no live session in `activeSessions`.
|
|
1387
|
+
*
|
|
1388
|
+
* Used by the ACP layer to bind a session to a specific agent when the MAP
|
|
1389
|
+
* stream targets one explicitly — preserving the routing intent that
|
|
1390
|
+
* cwd-based head-manager lookup would otherwise lose in multi-coordinator
|
|
1391
|
+
* scenarios.
|
|
1392
|
+
*/
|
|
1393
|
+
function getActiveAgentSession(agentId: AgentId): SpawnedAgent | null {
|
|
1394
|
+
if (!activeSessions.has(agentId)) return null;
|
|
1395
|
+
const record = agentStore.getAgent(agentId);
|
|
1396
|
+
if (!record || record.state !== "running") return null;
|
|
1397
|
+
const sessionEntry = activeSessions.get(agentId)!;
|
|
1398
|
+
const storedSession = agentStore.getSession(agentId);
|
|
1399
|
+
return {
|
|
1400
|
+
id: agentId,
|
|
1401
|
+
session_id: storedSession?.session_id ?? sessionEntry.session.id ?? "",
|
|
1402
|
+
agent: agentRecordToAgent(record),
|
|
1403
|
+
session: sessionEntry.session,
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1170
1407
|
// ── Session Interaction ──────────────────────────────────────
|
|
1171
1408
|
|
|
1172
1409
|
async function* prompt(
|
|
@@ -1380,6 +1617,12 @@ export function createAgentManagerV2(
|
|
|
1380
1617
|
sidecarRef = sidecar;
|
|
1381
1618
|
}
|
|
1382
1619
|
|
|
1620
|
+
function setTopologyPolicyFn(
|
|
1621
|
+
policy: import('../workspace/topology/types.js').TopologyPolicy | null
|
|
1622
|
+
): void {
|
|
1623
|
+
topologyPolicy = policy;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1383
1626
|
function setMailServices(): void {
|
|
1384
1627
|
// No-op: agent-inbox handles conversation tracking
|
|
1385
1628
|
}
|
|
@@ -1422,6 +1665,7 @@ export function createAgentManagerV2(
|
|
|
1422
1665
|
getHierarchy,
|
|
1423
1666
|
getOrCreateHeadManager,
|
|
1424
1667
|
listHeadManagers,
|
|
1668
|
+
getActiveAgentSession,
|
|
1425
1669
|
prompt,
|
|
1426
1670
|
promptUntilDone,
|
|
1427
1671
|
getSession,
|
|
@@ -1441,6 +1685,7 @@ export function createAgentManagerV2(
|
|
|
1441
1685
|
setIntegrationConfigs,
|
|
1442
1686
|
setSkillLoadout,
|
|
1443
1687
|
setSidecar,
|
|
1688
|
+
setTopologyPolicy: setTopologyPolicyFn,
|
|
1444
1689
|
setMailServices,
|
|
1445
1690
|
close,
|
|
1446
1691
|
} as AgentManager;
|