macro-agent 0.2.3 → 0.2.5
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/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +182 -43
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +17 -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 +21 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/acp.js +0 -0
- package/dist/cli/index.js +0 -0
- package/dist/cli/mcp.js +0 -0
- package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
- package/dist/cognitive/macro-agent-backend.js +37 -8
- package/dist/cognitive/macro-agent-backend.js.map +1 -1
- package/dist/cognitive/types.d.ts +24 -0
- package/dist/cognitive/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/map/mail-bridge.d.ts +7 -0
- package/dist/map/mail-bridge.d.ts.map +1 -1
- package/dist/map/mail-bridge.js +39 -9
- package/dist/map/mail-bridge.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +19 -4
- package/dist/map/sidecar.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +23 -6
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/package.json +4 -2
- package/renovate.json5 +6 -0
- package/src/agent/__tests__/agent-manager-v2.completion-signal.test.ts +284 -0
- package/src/agent/agent-manager-v2.ts +216 -50
- package/src/agent/agent-manager.ts +17 -0
- package/src/agent/types.ts +23 -2
- package/src/cognitive/__tests__/macro-agent-backend.test.ts +158 -5
- package/src/cognitive/macro-agent-backend.ts +39 -7
- package/src/cognitive/types.ts +24 -0
- package/src/index.ts +2 -0
- package/src/map/__tests__/mail-bridge.test.ts +39 -2
- package/src/map/__tests__/sidecar-diff-install-smoke.test.ts +17 -2
- package/src/map/mail-bridge.ts +61 -13
- package/src/map/sidecar.ts +21 -4
- package/src/teams/team-runtime-v2.ts +29 -6
- package/dist/workspace/dataplane-adapter.d.ts +0 -260
- package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
- package/dist/workspace/dataplane-adapter.js +0 -416
- package/dist/workspace/dataplane-adapter.js.map +0 -1
|
@@ -48,7 +48,7 @@ import type {
|
|
|
48
48
|
AgentLifecycleEvent,
|
|
49
49
|
AgentConfig,
|
|
50
50
|
ContinueAgentOptions,
|
|
51
|
-
|
|
51
|
+
MCPServerStdioConfig,
|
|
52
52
|
} from "./types.js";
|
|
53
53
|
import { AgentManagerError } from "./types.js";
|
|
54
54
|
import type { RoleRegistry, Capability } from "../roles/types.js";
|
|
@@ -220,6 +220,12 @@ export function createAgentManagerV2(
|
|
|
220
220
|
const activeSessions = new Map<AgentId, ActiveSession>();
|
|
221
221
|
const agentWorkspaces = new Map<AgentId, Workspace>();
|
|
222
222
|
const lifecycleListeners = new Set<AgentLifecycleCallback>();
|
|
223
|
+
// In-flight fire-and-forget terminate() promises (e.g. the promptUntilDone
|
|
224
|
+
// completion poller tears the agent down without awaiting it, to keep
|
|
225
|
+
// completion latency low). close() awaits these so shutdown cannot return
|
|
226
|
+
// while a group-kill is still racing — which would let process.exit orphan
|
|
227
|
+
// the subtree.
|
|
228
|
+
const pendingTerminations = new Set<Promise<unknown>>();
|
|
223
229
|
let spawnInterceptor: SpawnInterceptor | null = null;
|
|
224
230
|
let isShuttingDown = false;
|
|
225
231
|
let mapServerUrl: string | undefined;
|
|
@@ -285,7 +291,7 @@ export function createAgentManagerV2(
|
|
|
285
291
|
lineage?: string[];
|
|
286
292
|
sessionId?: string;
|
|
287
293
|
streamId?: string;
|
|
288
|
-
}):
|
|
294
|
+
}): MCPServerStdioConfig {
|
|
289
295
|
const env: Record<string, string> = {
|
|
290
296
|
MACRO_AGENT_ID: opts.agentId,
|
|
291
297
|
MACRO_PARENT_ID: opts.parentId,
|
|
@@ -759,15 +765,28 @@ export function createAgentManagerV2(
|
|
|
759
765
|
value: v,
|
|
760
766
|
})),
|
|
761
767
|
},
|
|
762
|
-
...(agentConfig?.mcpServers?.map((s) =>
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
768
|
+
...(agentConfig?.mcpServers?.map((s) => {
|
|
769
|
+
if ("command" in s) {
|
|
770
|
+
return {
|
|
771
|
+
name: s.name,
|
|
772
|
+
command: s.command,
|
|
773
|
+
args: s.args ?? [],
|
|
774
|
+
env: Object.entries(s.env ?? {}).map(([k, v]) => ({
|
|
775
|
+
name: k,
|
|
776
|
+
value: v,
|
|
777
|
+
})),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
name: s.name,
|
|
782
|
+
type: s.type,
|
|
783
|
+
url: s.url,
|
|
784
|
+
headers: Object.entries(s.headers ?? {}).map(([k, v]) => ({
|
|
785
|
+
name: k,
|
|
786
|
+
value: v,
|
|
787
|
+
})),
|
|
788
|
+
};
|
|
789
|
+
}) ?? []),
|
|
771
790
|
];
|
|
772
791
|
|
|
773
792
|
// Always-on subsystem MCP servers (the "trinity"). The macro-agent
|
|
@@ -1753,63 +1772,189 @@ export function createAgentManagerV2(
|
|
|
1753
1772
|
options?: {
|
|
1754
1773
|
maxFollowUps?: number;
|
|
1755
1774
|
onUpdate?: (update: ExtendedSessionUpdate) => void;
|
|
1775
|
+
/**
|
|
1776
|
+
* Optional caller-supplied completion predicate. When it returns true the
|
|
1777
|
+
* loop stops consuming, terminates the agent as "completed", and returns
|
|
1778
|
+
* `completedExternally: true` — WITHOUT requiring the macro `done()` tool.
|
|
1779
|
+
*
|
|
1780
|
+
* Checked three ways (all fire terminate + return completedExternally):
|
|
1781
|
+
* (a) reactively after each streamed update (fast path),
|
|
1782
|
+
* (b) reactively after each attempt's stream ends (fast path),
|
|
1783
|
+
* (c) on a CONCURRENT ~2s timer while consuming the stream. (c) is the
|
|
1784
|
+
* critical path for the live τ rollout: the agent runs the whole
|
|
1785
|
+
* episode in ONE prompt() call, calls τ's finish, the env writes its
|
|
1786
|
+
* reward sink (done:true), and then the prompt() stream goes SILENT
|
|
1787
|
+
* and does NOT close — the async iterator just blocks. With only the
|
|
1788
|
+
* reactive checks (a)/(b) the predicate is never re-evaluated and
|
|
1789
|
+
* solve() hangs until the rollout backstop. The concurrent poller
|
|
1790
|
+
* observes done:true independent of update arrival, flips
|
|
1791
|
+
* completedExternally, and terminate()s the agent — which ends the
|
|
1792
|
+
* in-flight prompt() generator (via return or throw, both handled).
|
|
1793
|
+
*
|
|
1794
|
+
* Generic by design: this module has NO knowledge of what the predicate
|
|
1795
|
+
* reads (e.g. an external reward sink). Back-compat: when absent, NO poller
|
|
1796
|
+
* is started and behavior is unchanged — only `done()` detection drives
|
|
1797
|
+
* completion.
|
|
1798
|
+
*
|
|
1799
|
+
* @param pollIntervalMs Override the concurrent poll cadence (default
|
|
1800
|
+
* 2000ms). Primarily a test seam.
|
|
1801
|
+
*/
|
|
1802
|
+
isComplete?: () => boolean | Promise<boolean>;
|
|
1803
|
+
pollIntervalMs?: number;
|
|
1756
1804
|
}
|
|
1757
1805
|
): Promise<{
|
|
1758
1806
|
doneCalled: boolean;
|
|
1759
1807
|
doneStatus?: string;
|
|
1808
|
+
completedExternally: boolean;
|
|
1760
1809
|
updates: ExtendedSessionUpdate[];
|
|
1761
1810
|
}> {
|
|
1762
1811
|
const maxFollowUps = options?.maxFollowUps ?? 2;
|
|
1812
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
|
|
1763
1813
|
const allUpdates: ExtendedSessionUpdate[] = [];
|
|
1764
1814
|
let doneCalled = false;
|
|
1765
1815
|
let doneStatus: string | undefined;
|
|
1816
|
+
let completedExternally = false;
|
|
1766
1817
|
|
|
1767
1818
|
let currentMessage = message;
|
|
1768
1819
|
|
|
1769
1820
|
for (let attempt = 0; attempt <= maxFollowUps; attempt++) {
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1821
|
+
// ── Concurrent completion poller ─────────────────────────────────────
|
|
1822
|
+
// Only armed when the caller supplies a predicate (back-compat: no
|
|
1823
|
+
// predicate → no poller, identical behavior). The live failure mode is a
|
|
1824
|
+
// prompt() stream that has gone SILENT but not CLOSED after the episode
|
|
1825
|
+
// externally completed; the reactive per-update check never fires again
|
|
1826
|
+
// because no updates arrive. This timer re-evaluates isComplete()
|
|
1827
|
+
// independent of update arrival. On true it flips completedExternally and
|
|
1828
|
+
// terminate()s the agent, which ends the in-flight prompt() generator
|
|
1829
|
+
// (return or throw) so the for-await below unblocks. The interval is
|
|
1830
|
+
// always cleared in `finally` so no timer leaks across attempts.
|
|
1831
|
+
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
1832
|
+
let polling = false;
|
|
1833
|
+
|
|
1834
|
+
// Resolves the instant completion is detected (external predicate OR the
|
|
1835
|
+
// macro done() tool). The attempt is RACED against this signal so it
|
|
1836
|
+
// returns WITHOUT waiting for the prompt() generator to drain or for
|
|
1837
|
+
// terminate() to finish. The prior design relied on terminate() ending
|
|
1838
|
+
// the silent in-flight stream to unblock the for-await; when that teardown
|
|
1839
|
+
// stalled (e.g. a long, busy real episode whose subprocess is slow to die),
|
|
1840
|
+
// promptUntilDone never returned and the macro session stayed "running"
|
|
1841
|
+
// until the multi-hour hard-timer — the refinement "finalization hang".
|
|
1842
|
+
let signalDone: () => void = () => {};
|
|
1843
|
+
const doneSignal = new Promise<void>((resolve) => {
|
|
1844
|
+
signalDone = resolve;
|
|
1845
|
+
});
|
|
1773
1846
|
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1847
|
+
const startPoller = () => {
|
|
1848
|
+
if (!options?.isComplete) return;
|
|
1849
|
+
pollTimer = setInterval(() => {
|
|
1850
|
+
if (polling || completedExternally) return;
|
|
1851
|
+
polling = true;
|
|
1852
|
+
void (async () => {
|
|
1853
|
+
try {
|
|
1854
|
+
if (!completedExternally && options.isComplete && (await options.isComplete())) {
|
|
1855
|
+
completedExternally = true;
|
|
1856
|
+
signalDone();
|
|
1857
|
+
// Tear down the agent/session in the BACKGROUND — never await it
|
|
1858
|
+
// here, so a slow teardown cannot gate the return. Tracked in
|
|
1859
|
+
// pendingTerminations so close() awaits it before shutdown
|
|
1860
|
+
// returns (else a still-racing group-kill is abandoned by a
|
|
1861
|
+
// subsequent process.exit, orphaning the subtree). Best effort.
|
|
1862
|
+
{
|
|
1863
|
+
const t = terminate(agentId, "completed" as any).catch(() => {});
|
|
1864
|
+
pendingTerminations.add(t);
|
|
1865
|
+
void t.finally(() => pendingTerminations.delete(t));
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
} catch {
|
|
1869
|
+
// Predicate errored — leave completion to the reactive checks.
|
|
1870
|
+
} finally {
|
|
1871
|
+
polling = false;
|
|
1872
|
+
}
|
|
1873
|
+
})();
|
|
1874
|
+
}, pollIntervalMs);
|
|
1875
|
+
};
|
|
1777
1876
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
//
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1877
|
+
// Consume the stream as a background task so a silent-but-open generator
|
|
1878
|
+
// can be raced against the completion signal below.
|
|
1879
|
+
const consume = (async () => {
|
|
1880
|
+
for await (const update of prompt(agentId, currentMessage)) {
|
|
1881
|
+
allUpdates.push(update);
|
|
1882
|
+
options?.onUpdate?.(update);
|
|
1883
|
+
|
|
1884
|
+
// External completion predicate (reactive fast path).
|
|
1885
|
+
if (options?.isComplete && (await options.isComplete())) {
|
|
1886
|
+
completedExternally = true;
|
|
1887
|
+
signalDone();
|
|
1888
|
+
break;
|
|
1889
|
+
}
|
|
1890
|
+
if (completedExternally) break;
|
|
1891
|
+
|
|
1892
|
+
// Detect done() tool call from session updates.
|
|
1893
|
+
// acp-factory uses { sessionUpdate: "tool_call", title: "mcp__macro-agent__done" }
|
|
1894
|
+
const uAny = update as any;
|
|
1895
|
+
|
|
1896
|
+
// Check title field (primary detection)
|
|
1897
|
+
if (
|
|
1898
|
+
(uAny.sessionUpdate === "tool_call" || uAny.sessionUpdate === "tool_call_update") &&
|
|
1899
|
+
typeof uAny.title === "string" &&
|
|
1900
|
+
uAny.title.endsWith("__done")
|
|
1901
|
+
) {
|
|
1902
|
+
doneCalled = true;
|
|
1903
|
+
// Extract status from rawInput (may arrive across multiple updates —
|
|
1904
|
+
// first update has rawInput={}, subsequent has full input)
|
|
1905
|
+
try {
|
|
1906
|
+
const raw = uAny.rawInput;
|
|
1907
|
+
const input =
|
|
1908
|
+
typeof raw === "string" ? JSON.parse(raw) :
|
|
1909
|
+
typeof raw === "object" ? raw :
|
|
1910
|
+
uAny.input;
|
|
1911
|
+
if (input?.status) {
|
|
1912
|
+
doneStatus = input.status;
|
|
1913
|
+
}
|
|
1914
|
+
} catch {
|
|
1915
|
+
// Best effort — rawInput may not be parseable yet
|
|
1795
1916
|
}
|
|
1796
|
-
|
|
1797
|
-
// Best effort — rawInput may not be parseable yet
|
|
1917
|
+
signalDone();
|
|
1798
1918
|
}
|
|
1799
|
-
}
|
|
1800
1919
|
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1920
|
+
// Fallback: check older format
|
|
1921
|
+
if (
|
|
1922
|
+
uAny.type === "result" &&
|
|
1923
|
+
uAny.subtype === "tool_result" &&
|
|
1924
|
+
uAny.toolName === "done"
|
|
1925
|
+
) {
|
|
1926
|
+
doneCalled = true;
|
|
1927
|
+
doneStatus = uAny.result?.status;
|
|
1928
|
+
signalDone();
|
|
1929
|
+
}
|
|
1809
1930
|
}
|
|
1931
|
+
})();
|
|
1932
|
+
// Prevent an unhandled rejection if we return via doneSignal before the
|
|
1933
|
+
// now-orphaned stream throws from a background teardown.
|
|
1934
|
+
consume.catch(() => {});
|
|
1935
|
+
|
|
1936
|
+
try {
|
|
1937
|
+
startPoller();
|
|
1938
|
+
// Return as soon as EITHER the stream drains naturally OR completion is
|
|
1939
|
+
// signalled — whichever comes first. This is the key change: completion
|
|
1940
|
+
// no longer depends on the generator unblocking.
|
|
1941
|
+
await Promise.race([consume, doneSignal]);
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
// A genuine stream error (not a post-completion teardown) re-throws.
|
|
1944
|
+
if (!completedExternally && !doneCalled) throw err;
|
|
1945
|
+
} finally {
|
|
1946
|
+
// Always clear the concurrent poller so no timer leaks across attempts.
|
|
1947
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// Re-check after the stream ends in case completion landed exactly as the
|
|
1951
|
+
// attempt's stream drained (the inner break / poller already set the flag
|
|
1952
|
+
// when it fired mid-stream).
|
|
1953
|
+
if (!completedExternally && options?.isComplete && (await options.isComplete())) {
|
|
1954
|
+
completedExternally = true;
|
|
1810
1955
|
}
|
|
1811
1956
|
|
|
1812
|
-
if (doneCalled) break;
|
|
1957
|
+
if (doneCalled || completedExternally) break;
|
|
1813
1958
|
|
|
1814
1959
|
if (attempt < maxFollowUps) {
|
|
1815
1960
|
currentMessage =
|
|
@@ -1817,9 +1962,11 @@ export function createAgentManagerV2(
|
|
|
1817
1962
|
}
|
|
1818
1963
|
}
|
|
1819
1964
|
|
|
1820
|
-
// Auto-terminate when
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1965
|
+
// Auto-terminate when the agent completed — either via the macro done() tool
|
|
1966
|
+
// or via the caller's external completion predicate. This closes the
|
|
1967
|
+
// lifecycle gap: without this, agents stay in "running" state because
|
|
1968
|
+
// nothing triggers terminate(). The external path mirrors the done() path
|
|
1969
|
+
// (terminate as "completed").
|
|
1823
1970
|
if (doneCalled) {
|
|
1824
1971
|
const reason = doneStatus === "completed" ? "completed" : (doneStatus ?? "failed");
|
|
1825
1972
|
try {
|
|
@@ -1827,9 +1974,15 @@ export function createAgentManagerV2(
|
|
|
1827
1974
|
} catch {
|
|
1828
1975
|
// Best effort — agent may already be stopping
|
|
1829
1976
|
}
|
|
1977
|
+
} else if (completedExternally) {
|
|
1978
|
+
try {
|
|
1979
|
+
await terminate(agentId, "completed" as any);
|
|
1980
|
+
} catch {
|
|
1981
|
+
// Best effort — agent may already be stopping
|
|
1982
|
+
}
|
|
1830
1983
|
}
|
|
1831
1984
|
|
|
1832
|
-
return { doneCalled, doneStatus, updates: allUpdates };
|
|
1985
|
+
return { doneCalled, doneStatus, completedExternally, updates: allUpdates };
|
|
1833
1986
|
}
|
|
1834
1987
|
|
|
1835
1988
|
function getSession(agentId: AgentId): Session | null {
|
|
@@ -1974,6 +2127,19 @@ export function createAgentManagerV2(
|
|
|
1974
2127
|
});
|
|
1975
2128
|
}
|
|
1976
2129
|
activeSessions.clear();
|
|
2130
|
+
|
|
2131
|
+
// Await any in-flight fire-and-forget terminations (e.g. the completion
|
|
2132
|
+
// poller's background teardown) so shutdown does not return while a
|
|
2133
|
+
// group-kill is still racing — otherwise a follow-on process.exit abandons
|
|
2134
|
+
// it and orphans the acp/claude/mcp_env subtree. Bounded by a backstop so a
|
|
2135
|
+
// genuinely stuck close() cannot wedge shutdown.
|
|
2136
|
+
if (pendingTerminations.size > 0) {
|
|
2137
|
+
await Promise.race([
|
|
2138
|
+
Promise.allSettled([...pendingTerminations]),
|
|
2139
|
+
new Promise((resolve) => setTimeout(resolve, 6000)),
|
|
2140
|
+
]);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1977
2143
|
agentWorkspaces.clear();
|
|
1978
2144
|
lifecycleListeners.clear();
|
|
1979
2145
|
|
|
@@ -164,10 +164,27 @@ export interface AgentManager {
|
|
|
164
164
|
maxFollowUps?: number;
|
|
165
165
|
/** Callback for each update during prompting */
|
|
166
166
|
onUpdate?: (update: ExtendedSessionUpdate) => void;
|
|
167
|
+
/**
|
|
168
|
+
* Optional caller-supplied completion predicate. When it returns true the
|
|
169
|
+
* loop stops early and terminates the agent as "completed" WITHOUT
|
|
170
|
+
* requiring the macro done() tool. Back-compat: absent → unchanged.
|
|
171
|
+
*
|
|
172
|
+
* Evaluated reactively per-update/per-attempt AND on a concurrent timer
|
|
173
|
+
* (see implementation) so a silent-but-open prompt() stream still
|
|
174
|
+
* completes. No predicate → no poller, unchanged behavior.
|
|
175
|
+
*/
|
|
176
|
+
isComplete?: () => boolean | Promise<boolean>;
|
|
177
|
+
/**
|
|
178
|
+
* Override the concurrent isComplete() poll cadence in ms (default 2000).
|
|
179
|
+
* Only meaningful alongside `isComplete`; primarily a test seam.
|
|
180
|
+
*/
|
|
181
|
+
pollIntervalMs?: number;
|
|
167
182
|
},
|
|
168
183
|
): Promise<{
|
|
169
184
|
doneCalled: boolean;
|
|
170
185
|
doneStatus?: string;
|
|
186
|
+
/** True when the caller's isComplete() predicate ended the loop. */
|
|
187
|
+
completedExternally: boolean;
|
|
171
188
|
updates: ExtendedSessionUpdate[];
|
|
172
189
|
}>;
|
|
173
190
|
|
package/src/agent/types.ts
CHANGED
|
@@ -202,15 +202,36 @@ export interface AgentConfig {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
|
-
* MCP server configuration
|
|
205
|
+
* MCP server configuration.
|
|
206
|
+
*
|
|
207
|
+
* A discriminated union over transport. The legacy stdio shape (no `type`,
|
|
208
|
+
* or `type: "stdio"`) is preserved byte-for-byte; new `http`/`sse` variants
|
|
209
|
+
* carry a `url` + optional `headers` instead of a `command`. These map
|
|
210
|
+
* directly onto the ACP wire's `McpServerStdio` / `McpServerHttp` /
|
|
211
|
+
* `McpServerSse` shapes (see acp-factory `createSession({ mcpServers })`).
|
|
206
212
|
*/
|
|
207
|
-
export
|
|
213
|
+
export type MCPServerConfig = MCPServerStdioConfig | MCPServerHttpConfig;
|
|
214
|
+
|
|
215
|
+
/** Stdio transport (legacy default — preserved for M0 compatibility). */
|
|
216
|
+
export interface MCPServerStdioConfig {
|
|
208
217
|
name: string;
|
|
218
|
+
/** Optional transport discriminant; absent means stdio. */
|
|
219
|
+
type?: "stdio";
|
|
209
220
|
command: string;
|
|
210
221
|
args?: string[];
|
|
211
222
|
env?: Record<string, string>;
|
|
212
223
|
}
|
|
213
224
|
|
|
225
|
+
/** HTTP (streamable-http) or SSE transport for a remote MCP server. */
|
|
226
|
+
export interface MCPServerHttpConfig {
|
|
227
|
+
name: string;
|
|
228
|
+
type: "http" | "sse";
|
|
229
|
+
/** URL to the remote MCP server. */
|
|
230
|
+
url: string;
|
|
231
|
+
/** HTTP headers to set on requests to the server. */
|
|
232
|
+
headers?: Record<string, string>;
|
|
233
|
+
}
|
|
234
|
+
|
|
214
235
|
// ─────────────────────────────────────────────────────────────────
|
|
215
236
|
// Spawned Agent Result
|
|
216
237
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -6,16 +6,23 @@ import type { RoleRegistry, RoleDefinition } from "../../roles/types.js";
|
|
|
6
6
|
|
|
7
7
|
// ── Mock Helpers ─────────────────────────────────────────────────
|
|
8
8
|
|
|
9
|
+
// Stand-in for the built-in "generic" role the REAL DefaultRoleRegistry falls
|
|
10
|
+
// back to. Used by resolveRole() below so the mock matches production
|
|
11
|
+
// semantics (resolveRole NEVER throws on a miss).
|
|
12
|
+
const GENERIC_FALLBACK = { name: "generic" } as unknown as RoleDefinition;
|
|
13
|
+
|
|
9
14
|
function createMockRoleRegistry(): RoleRegistry {
|
|
10
15
|
const roles = new Map<string, RoleDefinition>();
|
|
11
16
|
return {
|
|
12
17
|
registerRole: vi.fn((role: RoleDefinition) => {
|
|
13
18
|
roles.set(role.name, role);
|
|
14
19
|
}),
|
|
20
|
+
// MUST mirror the real DefaultRoleRegistry.resolveRole(): on an unknown
|
|
21
|
+
// role it does NOT throw — it logs and returns the GenericRole fallback.
|
|
22
|
+
// A throwing mock (the old behavior) masked the constructor bug where
|
|
23
|
+
// analyst registration was gated on resolveRole() throwing.
|
|
15
24
|
resolveRole: vi.fn((name: string) => {
|
|
16
|
-
|
|
17
|
-
if (!role) throw new Error(`Role not found: ${name}`);
|
|
18
|
-
return role;
|
|
25
|
+
return roles.get(name) ?? GENERIC_FALLBACK;
|
|
19
26
|
}),
|
|
20
27
|
getRole: vi.fn((name: string) => roles.get(name)),
|
|
21
28
|
hasCapability: vi.fn(() => true),
|
|
@@ -92,10 +99,33 @@ describe("MacroAgentBackend", () => {
|
|
|
92
99
|
);
|
|
93
100
|
});
|
|
94
101
|
|
|
102
|
+
it("registers analyst against a real-semantics registry (resolveRole does NOT throw on miss)", () => {
|
|
103
|
+
// Regression guard for the live Arm-B bug: the constructor must NOT rely
|
|
104
|
+
// on resolveRole() throwing to detect a missing analyst role. With a
|
|
105
|
+
// non-throwing registry (production behavior), analyst must still be
|
|
106
|
+
// registered, and resolveRole("analyst") must then return analyst —
|
|
107
|
+
// NOT fall back to "generic" (which would give the spawned agent
|
|
108
|
+
// all built-in tools + own-workspace + persistent lifecycle, exactly
|
|
109
|
+
// the conditions that produced 0 env-tool calls and a 600s timeout).
|
|
110
|
+
const registry = createMockRoleRegistry();
|
|
111
|
+
const am = createMockAgentManager({
|
|
112
|
+
getRoleRegistry: vi.fn().mockReturnValue(registry),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
new MacroAgentBackend(am);
|
|
116
|
+
|
|
117
|
+
expect(registry.registerRole).toHaveBeenCalledWith(
|
|
118
|
+
expect.objectContaining({ name: "analyst" }),
|
|
119
|
+
);
|
|
120
|
+
// After construction, the role must resolve to analyst, not generic.
|
|
121
|
+
expect(registry.resolveRole("analyst").name).toBe("analyst");
|
|
122
|
+
});
|
|
123
|
+
|
|
95
124
|
it("skips registration if analyst role already exists", () => {
|
|
96
125
|
const registry = createMockRoleRegistry();
|
|
97
|
-
// Pre-register analyst
|
|
98
|
-
|
|
126
|
+
// Pre-register analyst via getRole (the exact-match existence check the
|
|
127
|
+
// constructor now uses), mirroring the real registry's API.
|
|
128
|
+
(registry.getRole as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
99
129
|
name: "analyst",
|
|
100
130
|
});
|
|
101
131
|
|
|
@@ -203,6 +233,129 @@ describe("MacroAgentBackend", () => {
|
|
|
203
233
|
);
|
|
204
234
|
});
|
|
205
235
|
|
|
236
|
+
it("forwards mcpServers into the raw macro spawn config (Arm B chain)", async () => {
|
|
237
|
+
const mcpServers = [
|
|
238
|
+
{
|
|
239
|
+
name: "tauenv",
|
|
240
|
+
command: "/venv/bin/python",
|
|
241
|
+
args: ["-m", "autonomation_tau_bench.mcp_env", "--split", "airline"],
|
|
242
|
+
env: { TAU_REWARD_FILE: "/tmp/reward.json" },
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
await backend.spawn({
|
|
247
|
+
agentType: "claude-code",
|
|
248
|
+
task: { description: "handle the customer" },
|
|
249
|
+
mcpServers,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(agentManager.spawn).toHaveBeenCalledWith(
|
|
253
|
+
expect.objectContaining({
|
|
254
|
+
config: expect.objectContaining({ mcpServers }),
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("forwards both env and mcpServers together into the raw spawn config", async () => {
|
|
260
|
+
const mcpServers = [
|
|
261
|
+
{ name: "tauenv", command: "/venv/bin/python", args: ["-m", "x"] },
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
await backend.spawn({
|
|
265
|
+
agentType: "claude-code",
|
|
266
|
+
task: { description: "test" },
|
|
267
|
+
env: { TAU_REWARD_FILE: "/tmp/r.json" },
|
|
268
|
+
mcpServers,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(agentManager.spawn).toHaveBeenCalledWith(
|
|
272
|
+
expect.objectContaining({
|
|
273
|
+
config: {
|
|
274
|
+
env: { TAU_REWARD_FILE: "/tmp/r.json" },
|
|
275
|
+
mcpServers,
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("invokes beforeSpawn BEFORE driving the agent (per-attempt reset hook)", async () => {
|
|
282
|
+
// beforeSpawn must run before agentManager.spawn so the env subprocess /
|
|
283
|
+
// reward sink is reset for THIS attempt. We assert ordering by recording
|
|
284
|
+
// the call sequence.
|
|
285
|
+
const order: string[] = [];
|
|
286
|
+
const beforeSpawn = vi.fn(() => {
|
|
287
|
+
order.push("beforeSpawn");
|
|
288
|
+
});
|
|
289
|
+
const am = createMockAgentManager({
|
|
290
|
+
spawn: vi.fn().mockImplementation(async () => {
|
|
291
|
+
order.push("spawn");
|
|
292
|
+
return { id: "agent_test123", session_id: "session_test123" };
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
const b = new MacroAgentBackend(am);
|
|
296
|
+
|
|
297
|
+
await b.spawn({
|
|
298
|
+
agentType: "claude-code",
|
|
299
|
+
task: { description: "tau episode" },
|
|
300
|
+
beforeSpawn,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(beforeSpawn).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(order).toEqual(["beforeSpawn", "spawn"]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("passes completionSignal into promptUntilDone (S1 threading)", async () => {
|
|
308
|
+
const completionSignal = vi.fn(() => false);
|
|
309
|
+
|
|
310
|
+
await backend.spawn({
|
|
311
|
+
agentType: "claude-code",
|
|
312
|
+
task: { description: "tau episode" },
|
|
313
|
+
completionSignal,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await vi.waitFor(() => {
|
|
317
|
+
expect(agentManager.promptUntilDone).toHaveBeenCalledWith(
|
|
318
|
+
expect.any(String),
|
|
319
|
+
expect.any(String),
|
|
320
|
+
expect.objectContaining({ isComplete: completionSignal }),
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("treats completedExternally as a success (session → completed) even without done()", async () => {
|
|
326
|
+
const am = createMockAgentManager({
|
|
327
|
+
promptUntilDone: vi.fn().mockResolvedValue({
|
|
328
|
+
doneCalled: false,
|
|
329
|
+
completedExternally: true,
|
|
330
|
+
updates: [],
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
const b = new MacroAgentBackend(am);
|
|
334
|
+
|
|
335
|
+
const session = await b.spawn({
|
|
336
|
+
agentType: "claude-code",
|
|
337
|
+
task: { description: "tau episode" },
|
|
338
|
+
completionSignal: () => true,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await vi.waitFor(async () => {
|
|
342
|
+
const s = await b.getSession(session.id);
|
|
343
|
+
expect(s!.state).toBe("completed");
|
|
344
|
+
expect(s!.error).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("passes config: undefined when neither env nor mcpServers set (M0 back-compat)", async () => {
|
|
349
|
+
await backend.spawn({
|
|
350
|
+
agentType: "claude-code",
|
|
351
|
+
task: { description: "test" },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(agentManager.spawn).toHaveBeenCalledWith(
|
|
355
|
+
expect.objectContaining({ config: undefined }),
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
206
359
|
it("stores macro agent ID in session metadata", async () => {
|
|
207
360
|
const session = await backend.spawn({
|
|
208
361
|
agentType: "claude-code",
|