macro-agent 0.2.4 → 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/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/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 +1 -1
- 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/mail-bridge.ts +61 -13
- 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
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",
|
|
@@ -57,11 +57,14 @@ export class MacroAgentBackend {
|
|
|
57
57
|
this.onSessionComplete = config?.onSessionComplete;
|
|
58
58
|
this.inboxAdapter = config?.inboxAdapter;
|
|
59
59
|
|
|
60
|
-
// Register analyst role if not already present
|
|
60
|
+
// Register analyst role if not already present. Use getRole() (exact match,
|
|
61
|
+
// returns undefined on a miss) rather than resolveRole(): the real
|
|
62
|
+
// DefaultRoleRegistry.resolveRole() never throws on an unknown role — it
|
|
63
|
+
// logs "Role 'analyst' not found, falling back to 'generic'" and returns
|
|
64
|
+
// GenericRole — so a resolveRole()/try-catch guard would silently skip
|
|
65
|
+
// registration and every spawn would fall back to GenericRole.
|
|
61
66
|
const registry = this.agentManager.getRoleRegistry();
|
|
62
|
-
|
|
63
|
-
registry.resolveRole("analyst");
|
|
64
|
-
} catch {
|
|
67
|
+
if (!registry.getRole("analyst")) {
|
|
65
68
|
registry.registerRole(AnalystRole);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
@@ -189,6 +192,29 @@ export class MacroAgentBackend {
|
|
|
189
192
|
config: CognitiveAgentSpawnConfig,
|
|
190
193
|
taskId?: string,
|
|
191
194
|
): Promise<CognitiveAgentSession> {
|
|
195
|
+
// Per-attempt reset hook — runs BEFORE the agent is driven so each attempt
|
|
196
|
+
// (initial spawn AND each refinement re-spawn) starts from a clean external
|
|
197
|
+
// state (e.g. a deleted benchmark reward sink). Best-effort: a failing hook
|
|
198
|
+
// must not abort the spawn.
|
|
199
|
+
if (config.beforeSpawn) {
|
|
200
|
+
try {
|
|
201
|
+
await config.beforeSpawn();
|
|
202
|
+
} catch {
|
|
203
|
+
// Best effort — reset failure should not block the spawn.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Forward env and MCP servers into the raw macro spawn's `config`. Build
|
|
208
|
+
// the AgentConfig only when at least one field is present so non-MCP,
|
|
209
|
+
// non-env spawns keep passing `config: undefined` (byte-identical to M0).
|
|
210
|
+
const rawConfig =
|
|
211
|
+
config.env || config.mcpServers
|
|
212
|
+
? {
|
|
213
|
+
...(config.env ? { env: config.env } : {}),
|
|
214
|
+
...(config.mcpServers ? { mcpServers: config.mcpServers } : {}),
|
|
215
|
+
}
|
|
216
|
+
: undefined;
|
|
217
|
+
|
|
192
218
|
const spawned = await this.agentManager.spawn({
|
|
193
219
|
task: config.task.description,
|
|
194
220
|
task_id: taskId,
|
|
@@ -197,7 +223,7 @@ export class MacroAgentBackend {
|
|
|
197
223
|
? this.config.coordinatorAgentId
|
|
198
224
|
: null,
|
|
199
225
|
cwd: config.cwd,
|
|
200
|
-
config:
|
|
226
|
+
config: rawConfig,
|
|
201
227
|
customPrompt: config.systemPromptAdditions,
|
|
202
228
|
});
|
|
203
229
|
|
|
@@ -319,12 +345,18 @@ export class MacroAgentBackend {
|
|
|
319
345
|
updateSessionFromEvent(session, update);
|
|
320
346
|
config.onMessage?.(session.messages[session.messages.length - 1]!);
|
|
321
347
|
},
|
|
348
|
+
// Forward the caller's completion predicate (e.g. a benchmark reward
|
|
349
|
+
// sink wrote done:true). When it fires, promptUntilDone returns
|
|
350
|
+
// completedExternally:true instead of exhausting maxFollowUps.
|
|
351
|
+
isComplete: config.completionSignal,
|
|
322
352
|
},
|
|
323
353
|
);
|
|
324
354
|
|
|
325
355
|
if (session.state === "running") {
|
|
326
|
-
|
|
327
|
-
|
|
356
|
+
// External completion (predicate) is a success path, same as done().
|
|
357
|
+
const completed = result.doneCalled || result.completedExternally;
|
|
358
|
+
session.state = completed ? "completed" : "failed";
|
|
359
|
+
if (!completed) session.error = "Agent did not call done()";
|
|
328
360
|
session.endTime = new Date();
|
|
329
361
|
session.result = result.doneStatus;
|
|
330
362
|
}
|
package/src/cognitive/types.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { AgentId } from "../store/types/index.js";
|
|
13
13
|
import type { TasksAdapter, InboxAdapter } from "../adapters/types.js";
|
|
14
|
+
import type { MCPServerConfig } from "../agent/types.js";
|
|
14
15
|
|
|
15
16
|
// ─────────────────────────────────────────────────────────────────
|
|
16
17
|
// Agent Session Types (matches cognitive-core/src/runtime/types.ts)
|
|
@@ -78,6 +79,29 @@ export interface CognitiveAgentSpawnConfig {
|
|
|
78
79
|
cwd?: string;
|
|
79
80
|
timeout?: number;
|
|
80
81
|
onMessage?: (message: CognitiveAgentMessage) => void;
|
|
82
|
+
/**
|
|
83
|
+
* MCP servers to mount on the spawned agent (e.g. a per-task tau-env
|
|
84
|
+
* server). Forwarded verbatim into the macro `AgentManager.spawn`'s
|
|
85
|
+
* `config.mcpServers`. Optional / back-compat: non-MCP spawns are
|
|
86
|
+
* unaffected. Matches macro `AgentConfig.mcpServers` (the stdio/http union).
|
|
87
|
+
*/
|
|
88
|
+
mcpServers?: MCPServerConfig[];
|
|
89
|
+
/**
|
|
90
|
+
* Optional completion predicate forwarded into `promptUntilDone`. When it
|
|
91
|
+
* returns true the agent loop completes externally (without the macro done()
|
|
92
|
+
* tool) — e.g. a benchmark env wrote its reward sink with done:true. Generic:
|
|
93
|
+
* the backend has no knowledge of what it reads. Back-compat: absent →
|
|
94
|
+
* unchanged.
|
|
95
|
+
*/
|
|
96
|
+
completionSignal?: () => boolean | Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* Optional per-attempt hook invoked BEFORE the agent is driven (and again
|
|
99
|
+
* before each refinement re-spawn, since refine() spreads this config). Used
|
|
100
|
+
* to reset per-attempt external state — e.g. delete a benchmark reward sink so
|
|
101
|
+
* each attempt only sees its OWN completion signal. Back-compat: absent →
|
|
102
|
+
* unchanged.
|
|
103
|
+
*/
|
|
104
|
+
beforeSpawn?: () => void | Promise<void>;
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
// ─────────────────────────────────────────────────────────────────
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,8 @@ export {
|
|
|
55
55
|
AgentManagerError,
|
|
56
56
|
type AgentManagerErrorCode,
|
|
57
57
|
type MCPServerConfig as AgentMCPServerConfig,
|
|
58
|
+
type MCPServerStdioConfig as AgentMCPServerStdioConfig,
|
|
59
|
+
type MCPServerHttpConfig as AgentMCPServerHttpConfig,
|
|
58
60
|
type AgentConfig as SpawnAgentConfig,
|
|
59
61
|
} from './agent/types.js';
|
|
60
62
|
|
|
@@ -67,11 +67,12 @@ describe("setupMailBridge", () => {
|
|
|
67
67
|
);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it("
|
|
70
|
+
it("delivers plain-text turns into the inbox as a text message", async () => {
|
|
71
71
|
const logs: string[] = [];
|
|
72
72
|
await setupMailBridge({
|
|
73
73
|
connection: conn,
|
|
74
74
|
inboxAdapter: inbox as any,
|
|
75
|
+
dispatcherAgentId: "dispatcher:host:1234:abc",
|
|
75
76
|
log: (m) => logs.push(m),
|
|
76
77
|
});
|
|
77
78
|
|
|
@@ -81,10 +82,46 @@ describe("setupMailBridge", () => {
|
|
|
81
82
|
participant_id: "some-agent",
|
|
82
83
|
content_type: "text/plain",
|
|
83
84
|
content: "hello world",
|
|
85
|
+
thread_id: "thread-xyz",
|
|
86
|
+
importance: "high",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(inbox.send).toHaveBeenCalledOnce();
|
|
90
|
+
const [from, to, content, opts] = inbox.send.mock.calls[0];
|
|
91
|
+
expect(from).toBe("some-agent");
|
|
92
|
+
expect(to).toBe("dispatcher:host:1234:abc");
|
|
93
|
+
// Delivered as a text message that normalizeContent passes through,
|
|
94
|
+
// carrying the conversation id for reply threading.
|
|
95
|
+
expect(content).toMatchObject({
|
|
96
|
+
type: "text",
|
|
97
|
+
text: "hello world",
|
|
98
|
+
_conversationId: "conv-1",
|
|
99
|
+
});
|
|
100
|
+
// Not shaped as a work envelope — the classifier must not match it.
|
|
101
|
+
expect(content).not.toHaveProperty("schema");
|
|
102
|
+
expect(opts?.threadTag).toBe("thread-xyz");
|
|
103
|
+
expect(opts?.importance).toBe("high");
|
|
104
|
+
expect(logs.some((l) => l.includes("Forwarded text turn"))).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("skips empty / whitespace-only text turns", async () => {
|
|
108
|
+
const logs: string[] = [];
|
|
109
|
+
await setupMailBridge({
|
|
110
|
+
connection: conn,
|
|
111
|
+
inboxAdapter: inbox as any,
|
|
112
|
+
log: (m) => logs.push(m),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await conn._fire({
|
|
116
|
+
conversation_id: "conv-empty",
|
|
117
|
+
turn_id: "turn-empty",
|
|
118
|
+
participant_id: "some-agent",
|
|
119
|
+
content_type: "text/plain",
|
|
120
|
+
content: " ",
|
|
84
121
|
});
|
|
85
122
|
|
|
86
123
|
expect(inbox.send).not.toHaveBeenCalled();
|
|
87
|
-
expect(logs.some((l) => l.includes("
|
|
124
|
+
expect(logs.some((l) => l.includes("Skipping empty"))).toBe(true);
|
|
88
125
|
});
|
|
89
126
|
|
|
90
127
|
describe("with dispatcherAgentId", () => {
|
package/src/map/mail-bridge.ts
CHANGED
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* 2. The classifier recognizes `x-dispatch/work` schema and routes the
|
|
14
14
|
* prompt to a worker agent (existing or freshly spawned).
|
|
15
15
|
*
|
|
16
|
+
* Turns that aren't structured `x-dispatch/work` envelopes (e.g.
|
|
17
|
+
* natural-language spec-discussion replies) are delivered into the inbox as
|
|
18
|
+
* plain-text messages instead of being dropped. The classifier won't match
|
|
19
|
+
* them (so no worker spawns), but the delivery still wakes the agent via
|
|
20
|
+
* TriggerSystemV2 (importance → wake action) so it reads the reply on its
|
|
21
|
+
* next turn.
|
|
22
|
+
*
|
|
16
23
|
* Without this bridge, hub-side mail turns never reach macro-agent's
|
|
17
24
|
* dispatcher. The MessagePort is wired to local inbox events only.
|
|
18
25
|
*/
|
|
@@ -135,14 +142,64 @@ export async function setupMailBridge(
|
|
|
135
142
|
metadata: { source: "openhive-mail-forward" },
|
|
136
143
|
});
|
|
137
144
|
|
|
145
|
+
// Importance derivation shared by the structured-work and plain-text
|
|
146
|
+
// delivery paths. Defaults to "normal" when the hub doesn't tag the turn.
|
|
147
|
+
const VALID_IMPORTANCE = ["low", "normal", "high", "urgent"];
|
|
148
|
+
const deriveImportance = (
|
|
149
|
+
value: string | undefined,
|
|
150
|
+
): "low" | "normal" | "high" | "urgent" =>
|
|
151
|
+
typeof value === "string" && VALID_IMPORTANCE.includes(value)
|
|
152
|
+
? (value as "low" | "normal" | "high" | "urgent")
|
|
153
|
+
: "normal";
|
|
154
|
+
|
|
138
155
|
const handler = async (params: unknown): Promise<void> => {
|
|
139
156
|
const turn = (params ?? {}) as MailTurnReceivedParams;
|
|
157
|
+
const wireImportance = deriveImportance(turn.importance);
|
|
140
158
|
const raw = parseTurnContent(turn.content, turn.content_type);
|
|
159
|
+
|
|
160
|
+
// Plain-text (non-JSON) turn — e.g. a natural-language spec-discussion
|
|
161
|
+
// reply. Previously dropped; now delivered into the local inbox as a
|
|
162
|
+
// text message so the agent wakes (TriggerSystemV2 maps importance to a
|
|
163
|
+
// wake action) and reads it on its next turn. The dispatcher's
|
|
164
|
+
// MessagePort classifier won't match a text payload, so no worker is
|
|
165
|
+
// spawned — correct for a conversational message. The conversation id
|
|
166
|
+
// rides along (mirrors the structured path) so the agent's reply can be
|
|
167
|
+
// threaded back to the right hub conversation.
|
|
141
168
|
if (!raw) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
169
|
+
const text = typeof turn.content === "string" ? turn.content : "";
|
|
170
|
+
if (!text.trim()) {
|
|
171
|
+
log(
|
|
172
|
+
`[mail-bridge] Skipping empty/non-text turn (conv=${turn.conversation_id ?? "?"} ` +
|
|
173
|
+
`participant=${turn.participant_id ?? "?"})`,
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const textContent: Record<string, unknown> = {
|
|
178
|
+
type: "text",
|
|
179
|
+
text,
|
|
180
|
+
...(turn.conversation_id
|
|
181
|
+
? { _conversationId: turn.conversation_id }
|
|
182
|
+
: {}),
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
await inboxAdapter.send(
|
|
186
|
+
turn.participant_id ?? "openhive-hub",
|
|
187
|
+
recipientId,
|
|
188
|
+
textContent as never,
|
|
189
|
+
{
|
|
190
|
+
threadTag: turn.thread_id,
|
|
191
|
+
importance: wireImportance,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
log(
|
|
195
|
+
`[mail-bridge] Forwarded text turn ${turn.turn_id ?? "?"} into local inbox`,
|
|
196
|
+
);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log(
|
|
199
|
+
`[mail-bridge] Forward failed for text turn ${turn.turn_id ?? "?"}: ` +
|
|
200
|
+
`${(err as Error).message}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
146
203
|
return;
|
|
147
204
|
}
|
|
148
205
|
|
|
@@ -172,15 +229,6 @@ export async function setupMailBridge(
|
|
|
172
229
|
...(turn.conversation_id ? { _conversationId: turn.conversation_id } : {}),
|
|
173
230
|
};
|
|
174
231
|
|
|
175
|
-
// Derive importance from the hub's wire params. Default to "normal"
|
|
176
|
-
// when the hub doesn't tag the turn (backward compat).
|
|
177
|
-
const VALID_IMPORTANCE = ["low", "normal", "high", "urgent"];
|
|
178
|
-
const wireImportance =
|
|
179
|
-
typeof turn.importance === "string" &&
|
|
180
|
-
VALID_IMPORTANCE.includes(turn.importance)
|
|
181
|
-
? (turn.importance as "low" | "normal" | "high" | "urgent")
|
|
182
|
-
: "normal";
|
|
183
|
-
|
|
184
232
|
try {
|
|
185
233
|
await inboxAdapter.send(
|
|
186
234
|
turn.participant_id ?? "openhive-hub",
|
|
@@ -532,12 +532,35 @@ export class TeamRuntimeV2 {
|
|
|
532
532
|
...options.config,
|
|
533
533
|
mcpServers: [
|
|
534
534
|
...(options.config?.mcpServers ?? []),
|
|
535
|
-
...teamMcpServers.map((s) =>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
535
|
+
...teamMcpServers.map((s) => {
|
|
536
|
+
// openteams' static McpServerEntry is stdio-only, but a
|
|
537
|
+
// resolved loadout entry may carry a remote transport
|
|
538
|
+
// (type/url/headers — see openteams McpProviderSpec). Pass
|
|
539
|
+
// those through unchanged so http/sse loadout servers reach
|
|
540
|
+
// the spawn conversion; otherwise emit the stdio shape.
|
|
541
|
+
const remote = s as unknown as {
|
|
542
|
+
type?: "stdio" | "http" | "sse";
|
|
543
|
+
url?: string;
|
|
544
|
+
headers?: Record<string, string>;
|
|
545
|
+
};
|
|
546
|
+
if (
|
|
547
|
+
(remote.type === "http" || remote.type === "sse") &&
|
|
548
|
+
remote.url
|
|
549
|
+
) {
|
|
550
|
+
return {
|
|
551
|
+
name: s.name,
|
|
552
|
+
type: remote.type,
|
|
553
|
+
url: remote.url,
|
|
554
|
+
headers: remote.headers,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
name: s.name,
|
|
559
|
+
command: s.command,
|
|
560
|
+
args: s.args,
|
|
561
|
+
env: s.env,
|
|
562
|
+
};
|
|
563
|
+
}),
|
|
541
564
|
],
|
|
542
565
|
env: {
|
|
543
566
|
...options.config?.env,
|