@dogpile/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +16 -0
  3. package/README.md +842 -0
  4. package/dist/browser/index.d.ts +8 -0
  5. package/dist/browser/index.d.ts.map +1 -0
  6. package/dist/browser/index.js +4493 -0
  7. package/dist/browser/index.js.map +1 -0
  8. package/dist/index.d.ts +17 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +14 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/providers/openai-compatible.d.ts +44 -0
  13. package/dist/providers/openai-compatible.d.ts.map +1 -0
  14. package/dist/providers/openai-compatible.js +305 -0
  15. package/dist/providers/openai-compatible.js.map +1 -0
  16. package/dist/runtime/broadcast.d.ts +18 -0
  17. package/dist/runtime/broadcast.d.ts.map +1 -0
  18. package/dist/runtime/broadcast.js +335 -0
  19. package/dist/runtime/broadcast.js.map +1 -0
  20. package/dist/runtime/cancellation.d.ts +6 -0
  21. package/dist/runtime/cancellation.d.ts.map +1 -0
  22. package/dist/runtime/cancellation.js +35 -0
  23. package/dist/runtime/cancellation.js.map +1 -0
  24. package/dist/runtime/coordinator.d.ts +18 -0
  25. package/dist/runtime/coordinator.d.ts.map +1 -0
  26. package/dist/runtime/coordinator.js +434 -0
  27. package/dist/runtime/coordinator.js.map +1 -0
  28. package/dist/runtime/decisions.d.ts +5 -0
  29. package/dist/runtime/decisions.d.ts.map +1 -0
  30. package/dist/runtime/decisions.js +31 -0
  31. package/dist/runtime/decisions.js.map +1 -0
  32. package/dist/runtime/defaults.d.ts +63 -0
  33. package/dist/runtime/defaults.d.ts.map +1 -0
  34. package/dist/runtime/defaults.js +426 -0
  35. package/dist/runtime/defaults.js.map +1 -0
  36. package/dist/runtime/engine.d.ts +79 -0
  37. package/dist/runtime/engine.d.ts.map +1 -0
  38. package/dist/runtime/engine.js +723 -0
  39. package/dist/runtime/engine.js.map +1 -0
  40. package/dist/runtime/model.d.ts +14 -0
  41. package/dist/runtime/model.d.ts.map +1 -0
  42. package/dist/runtime/model.js +82 -0
  43. package/dist/runtime/model.js.map +1 -0
  44. package/dist/runtime/sequential.d.ts +18 -0
  45. package/dist/runtime/sequential.d.ts.map +1 -0
  46. package/dist/runtime/sequential.js +277 -0
  47. package/dist/runtime/sequential.js.map +1 -0
  48. package/dist/runtime/shared.d.ts +18 -0
  49. package/dist/runtime/shared.d.ts.map +1 -0
  50. package/dist/runtime/shared.js +288 -0
  51. package/dist/runtime/shared.js.map +1 -0
  52. package/dist/runtime/termination.d.ts +77 -0
  53. package/dist/runtime/termination.d.ts.map +1 -0
  54. package/dist/runtime/termination.js +355 -0
  55. package/dist/runtime/termination.js.map +1 -0
  56. package/dist/runtime/tools.d.ts +314 -0
  57. package/dist/runtime/tools.d.ts.map +1 -0
  58. package/dist/runtime/tools.js +969 -0
  59. package/dist/runtime/tools.js.map +1 -0
  60. package/dist/runtime/validation.d.ts +23 -0
  61. package/dist/runtime/validation.d.ts.map +1 -0
  62. package/dist/runtime/validation.js +656 -0
  63. package/dist/runtime/validation.js.map +1 -0
  64. package/dist/types.d.ts +2434 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +81 -0
  67. package/dist/types.js.map +1 -0
  68. package/package.json +157 -0
  69. package/src/browser/index.ts +7 -0
  70. package/src/index.ts +195 -0
  71. package/src/providers/openai-compatible.ts +406 -0
  72. package/src/runtime/broadcast.test.ts +355 -0
  73. package/src/runtime/broadcast.ts +428 -0
  74. package/src/runtime/cancellation.ts +40 -0
  75. package/src/runtime/coordinator.test.ts +468 -0
  76. package/src/runtime/coordinator.ts +581 -0
  77. package/src/runtime/decisions.ts +38 -0
  78. package/src/runtime/defaults.ts +547 -0
  79. package/src/runtime/engine.ts +880 -0
  80. package/src/runtime/model.ts +117 -0
  81. package/src/runtime/sequential.test.ts +262 -0
  82. package/src/runtime/sequential.ts +357 -0
  83. package/src/runtime/shared.test.ts +265 -0
  84. package/src/runtime/shared.ts +367 -0
  85. package/src/runtime/termination.ts +463 -0
  86. package/src/runtime/tools.ts +1518 -0
  87. package/src/runtime/validation.ts +771 -0
  88. package/src/types.ts +2729 -0
@@ -0,0 +1,357 @@
1
+ import type {
2
+ AgentSpec,
3
+ ConfiguredModelProvider,
4
+ CostSummary,
5
+ DogpileOptions,
6
+ ModelRequest,
7
+ ModelResponse,
8
+ ReplayTraceProtocolDecision,
9
+ ReplayTraceProviderCall,
10
+ RuntimeTool,
11
+ RunEvent,
12
+ RunResult,
13
+ JsonObject,
14
+ JsonValue,
15
+ SequentialProtocolConfig,
16
+ TerminationCondition,
17
+ TerminationStopRecord,
18
+ Tier,
19
+ TranscriptEntry
20
+ } from "../types.js";
21
+ import {
22
+ addCost,
23
+ createReplayTraceBudget,
24
+ createReplayTraceBudgetStateChanges,
25
+ createReplayTraceFinalOutput,
26
+ createReplayTraceProtocolDecision,
27
+ createReplayTraceRunInputs,
28
+ createReplayTraceSeed,
29
+ createRunAccounting,
30
+ createRunEventLog,
31
+ createRunMetadata,
32
+ createRunUsage,
33
+ createTranscriptLink,
34
+ emptyCost,
35
+ nextProviderCallId
36
+ } from "./defaults.js";
37
+ import { throwIfAborted } from "./cancellation.js";
38
+ import { isParticipatingDecision, parseAgentDecision } from "./decisions.js";
39
+ import { generateModelTurn } from "./model.js";
40
+ import { evaluateTerminationStop } from "./termination.js";
41
+ import { createRuntimeToolExecutor, executeModelResponseToolRequests, runtimeToolAvailability } from "./tools.js";
42
+
43
+ interface SequentialRunOptions {
44
+ readonly intent: string;
45
+ readonly protocol: SequentialProtocolConfig;
46
+ readonly tier: Tier;
47
+ readonly model: ConfiguredModelProvider;
48
+ readonly agents: readonly AgentSpec[];
49
+ readonly tools: readonly RuntimeTool<JsonObject, JsonValue>[];
50
+ readonly temperature: number;
51
+ readonly budget?: DogpileOptions["budget"];
52
+ readonly seed?: string | number;
53
+ readonly signal?: AbortSignal;
54
+ readonly terminate?: TerminationCondition;
55
+ readonly emit?: (event: RunEvent) => void;
56
+ }
57
+
58
+ export async function runSequential(options: SequentialRunOptions): Promise<RunResult> {
59
+ const runId = createRunId();
60
+ const events: RunEvent[] = [];
61
+ const transcript: TranscriptEntry[] = [];
62
+ const protocolDecisions: ReplayTraceProtocolDecision[] = [];
63
+ const providerCalls: ReplayTraceProviderCall[] = [];
64
+ let totalCost = emptyCost();
65
+ const maxTurns = options.protocol.maxTurns ?? options.agents.length;
66
+ const activeAgents = options.agents.slice(0, maxTurns);
67
+ const startedAtMs = nowMs();
68
+ let stopped = false;
69
+ let termination: TerminationStopRecord | undefined;
70
+
71
+ const emit = (event: RunEvent): void => {
72
+ events.push(event);
73
+ options.emit?.(event);
74
+ };
75
+
76
+ const recordProtocolDecision = (
77
+ event: RunEvent,
78
+ decisionOptions?: Parameters<typeof createReplayTraceProtocolDecision>[3]
79
+ ): void => {
80
+ protocolDecisions.push(
81
+ createReplayTraceProtocolDecision("sequential", event, events.length - 1, decisionOptions)
82
+ );
83
+ };
84
+
85
+ const toolExecutor = createRuntimeToolExecutor({
86
+ runId,
87
+ protocol: "sequential",
88
+ tier: options.tier,
89
+ tools: options.tools,
90
+ emit(event): void {
91
+ emit(event);
92
+ recordProtocolDecision(event);
93
+ },
94
+ getTrace: () => ({ events, transcript }),
95
+ ...(options.signal !== undefined ? { abortSignal: options.signal } : {})
96
+ });
97
+ const toolAvailability = runtimeToolAvailability(toolExecutor.tools);
98
+
99
+ throwIfAborted(options.signal, options.model.id);
100
+
101
+ for (const agent of activeAgents) {
102
+ const event: RunEvent = {
103
+ type: "role-assignment",
104
+ runId,
105
+ at: new Date().toISOString(),
106
+ agentId: agent.id,
107
+ role: agent.role
108
+ };
109
+ emit(event);
110
+ recordProtocolDecision(event);
111
+ }
112
+
113
+ for (const [index, agent] of activeAgents.entries()) {
114
+ if (stopIfNeeded()) {
115
+ break;
116
+ }
117
+
118
+ const turn = index + 1;
119
+ const input = buildSequentialInput(options.intent, transcript);
120
+ const request: ModelRequest = {
121
+ temperature: options.temperature,
122
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
123
+ metadata: {
124
+ runId,
125
+ protocol: "sequential",
126
+ agentId: agent.id,
127
+ role: agent.role,
128
+ tier: options.tier,
129
+ turn,
130
+ ...toolAvailability
131
+ },
132
+ messages: [
133
+ {
134
+ role: "system",
135
+ content: buildSystemPrompt(agent)
136
+ },
137
+ {
138
+ role: "user",
139
+ content: input
140
+ }
141
+ ]
142
+ };
143
+ const response = await generateModelTurn({
144
+ model: options.model,
145
+ request,
146
+ runId,
147
+ agent,
148
+ input,
149
+ emit,
150
+ callId: nextProviderCallId(runId, providerCalls),
151
+ onProviderCall(call): void {
152
+ providerCalls.push(call);
153
+ }
154
+ });
155
+ const turnCost = responseCost(response);
156
+ totalCost = addCost(totalCost, turnCost);
157
+ const decision = parseAgentDecision(response.text);
158
+ const toolCalls = await executeModelResponseToolRequests({
159
+ response,
160
+ executor: toolExecutor,
161
+ agentId: agent.id,
162
+ role: agent.role,
163
+ turn
164
+ });
165
+ throwIfAborted(options.signal, options.model.id);
166
+
167
+ transcript.push({
168
+ agentId: agent.id,
169
+ role: agent.role,
170
+ input,
171
+ output: response.text,
172
+ ...(decision !== undefined ? { decision } : {}),
173
+ ...(toolCalls.length > 0 ? { toolCalls } : {})
174
+ });
175
+
176
+ const event: RunEvent = {
177
+ type: "agent-turn",
178
+ runId,
179
+ at: new Date().toISOString(),
180
+ agentId: agent.id,
181
+ role: agent.role,
182
+ input,
183
+ output: response.text,
184
+ ...(decision !== undefined ? { decision } : {}),
185
+ cost: totalCost
186
+ };
187
+ emit(event);
188
+ recordProtocolDecision(event, {
189
+ turn,
190
+ transcriptEntryCount: transcript.length
191
+ });
192
+
193
+ if (stopIfNeeded()) {
194
+ break;
195
+ }
196
+ }
197
+
198
+ const output = [...transcript].reverse().find((entry) => isParticipatingDecision(entry.decision))?.output ?? "";
199
+ throwIfAborted(options.signal, options.model.id);
200
+ const final: RunEvent = {
201
+ type: "final",
202
+ runId,
203
+ at: new Date().toISOString(),
204
+ output,
205
+ cost: totalCost,
206
+ transcript: createTranscriptLink(transcript),
207
+ ...(termination !== undefined ? { termination } : {})
208
+ };
209
+ emit(final);
210
+ recordProtocolDecision(final, {
211
+ transcriptEntryCount: transcript.length
212
+ });
213
+ const finalEvent = events.at(-1);
214
+
215
+ return {
216
+ output,
217
+ eventLog: createRunEventLog(runId, "sequential", events),
218
+ trace: {
219
+ schemaVersion: "1.0",
220
+ runId,
221
+ protocol: "sequential",
222
+ tier: options.tier,
223
+ modelProviderId: options.model.id,
224
+ agentsUsed: activeAgents,
225
+ inputs: createReplayTraceRunInputs({
226
+ intent: options.intent,
227
+ protocol: options.protocol,
228
+ tier: options.tier,
229
+ modelProviderId: options.model.id,
230
+ agents: activeAgents,
231
+ temperature: options.temperature
232
+ }),
233
+ budget: createReplayTraceBudget({
234
+ tier: options.tier,
235
+ ...(options.budget ? { caps: options.budget } : {}),
236
+ ...(options.terminate ? { termination: options.terminate } : {})
237
+ }),
238
+ budgetStateChanges: createReplayTraceBudgetStateChanges(events),
239
+ seed: createReplayTraceSeed(options.seed),
240
+ protocolDecisions,
241
+ providerCalls,
242
+ finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? events[0] ?? {
243
+ type: "final",
244
+ runId,
245
+ at: "",
246
+ output,
247
+ cost: totalCost,
248
+ transcript: createTranscriptLink(transcript)
249
+ }),
250
+ events,
251
+ transcript
252
+ },
253
+ transcript,
254
+ usage: createRunUsage(totalCost),
255
+ metadata: createRunMetadata({
256
+ runId,
257
+ protocol: "sequential",
258
+ tier: options.tier,
259
+ modelProviderId: options.model.id,
260
+ agentsUsed: activeAgents,
261
+ events
262
+ }),
263
+ accounting: createRunAccounting({
264
+ tier: options.tier,
265
+ ...(options.budget ? { budget: options.budget } : {}),
266
+ ...(options.terminate ? { termination: options.terminate } : {}),
267
+ cost: totalCost,
268
+ events
269
+ }),
270
+ cost: totalCost
271
+ };
272
+
273
+ function stopIfNeeded(): boolean {
274
+ throwIfAborted(options.signal, options.model.id);
275
+
276
+ if (stopped || !options.terminate) {
277
+ return stopped;
278
+ }
279
+
280
+ const stopRecord = evaluateTerminationStop(options.terminate, {
281
+ runId,
282
+ protocol: "sequential",
283
+ tier: options.tier,
284
+ cost: totalCost,
285
+ events,
286
+ transcript,
287
+ iteration: transcript.length,
288
+ elapsedMs: elapsedMs(startedAtMs)
289
+ });
290
+
291
+ if (!stopRecord) {
292
+ return false;
293
+ }
294
+
295
+ stopped = true;
296
+ termination = stopRecord;
297
+ if (stopRecord.reason === "budget") {
298
+ emitBudgetStop(stopRecord);
299
+ }
300
+ return true;
301
+ }
302
+
303
+ function emitBudgetStop(record: TerminationStopRecord): void {
304
+ const event: RunEvent = {
305
+ type: "budget-stop",
306
+ runId,
307
+ at: new Date().toISOString(),
308
+ reason: record.budgetReason ?? "cost",
309
+ cost: totalCost,
310
+ iteration: transcript.length,
311
+ elapsedMs: elapsedMs(startedAtMs),
312
+ detail: record.detail ?? {}
313
+ };
314
+ emit(event);
315
+ recordProtocolDecision(event, {
316
+ transcriptEntryCount: transcript.length
317
+ });
318
+ }
319
+ }
320
+
321
+ function buildSystemPrompt(agent: AgentSpec): string {
322
+ const instruction = agent.instructions ? `\nInstructions: ${agent.instructions}` : "";
323
+ return `You are ${agent.id}, acting as ${agent.role} in a Sequential multi-agent protocol.${instruction}`;
324
+ }
325
+
326
+ function buildSequentialInput(intent: string, transcript: readonly TranscriptEntry[]): string {
327
+ if (transcript.length === 0) {
328
+ return `Mission: ${intent}\nProvide your contribution.`;
329
+ }
330
+
331
+ const prior = transcript
332
+ .map((entry) => `${entry.role} (${entry.agentId}): ${entry.output}`)
333
+ .join("\n\n");
334
+ return `Mission: ${intent}\n\nPrior contributions:\n${prior}\n\nContinue the sequence with a useful next contribution.`;
335
+ }
336
+
337
+ function responseCost(response: ModelResponse): CostSummary {
338
+ return {
339
+ usd: response.costUsd ?? 0,
340
+ inputTokens: response.usage?.inputTokens ?? 0,
341
+ outputTokens: response.usage?.outputTokens ?? 0,
342
+ totalTokens: response.usage?.totalTokens ?? 0
343
+ };
344
+ }
345
+
346
+ function createRunId(): string {
347
+ const random = globalThis.crypto?.randomUUID?.();
348
+ return random ?? `run-${Date.now().toString(36)}`;
349
+ }
350
+
351
+ function nowMs(): number {
352
+ return globalThis.performance?.now() ?? Date.now();
353
+ }
354
+
355
+ function elapsedMs(startedAtMs: number): number {
356
+ return Math.max(0, nowMs() - startedAtMs);
357
+ }
@@ -0,0 +1,265 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createDeterministicSharedTestMission } from "../internal.js";
3
+ import { Dogpile, run, runtimeToolManifest } from "../index.js";
4
+ import type {
5
+ ConfiguredModelProvider,
6
+ JsonObject,
7
+ ModelRequest,
8
+ ModelResponse,
9
+ RunEvent,
10
+ RuntimeTool
11
+ } from "../index.js";
12
+
13
+ describe("shared protocol", () => {
14
+ it("executes end-to-end through the branded high-level SDK call", async () => {
15
+ const result = await Dogpile.pile(createDeterministicSharedTestMission());
16
+
17
+ expect(result.output).toBe(
18
+ [
19
+ "state-initializer:agent-1 => state-initializer:agent-1 initialized the shared state.",
20
+ "state-reviewer:agent-2 => state-reviewer:agent-2 initialized the shared state.",
21
+ "state-synthesizer:agent-3 => state-synthesizer:agent-3 initialized the shared state."
22
+ ].join("\n")
23
+ );
24
+ expect(result.trace.protocol).toBe("shared");
25
+ expect(result.trace.modelProviderId).toBe("deterministic-shared-model");
26
+ expect(result.trace.events.map((event) => event.type)).toEqual([
27
+ "role-assignment",
28
+ "role-assignment",
29
+ "role-assignment",
30
+ "agent-turn",
31
+ "agent-turn",
32
+ "agent-turn",
33
+ "final"
34
+ ]);
35
+ expect(result.transcript).toHaveLength(3);
36
+ expect(result.trace.transcript).toEqual(result.transcript);
37
+ expect(JSON.parse(JSON.stringify(result.trace))).toEqual(result.trace);
38
+ expect(result.cost.totalTokens).toBeGreaterThan(0);
39
+ });
40
+
41
+ it("runs a deterministic shared-state test mission against the configured model provider", async () => {
42
+ const requests: ModelRequest[] = [];
43
+ const model: ConfiguredModelProvider = {
44
+ id: "configured-shared-model",
45
+ async generate(request: ModelRequest): Promise<ModelResponse> {
46
+ requests.push(request);
47
+ const agentId = String(request.metadata.agentId);
48
+ const role = String(request.metadata.role);
49
+ const turn = Number(request.metadata.turn);
50
+
51
+ return {
52
+ text: `${role}:${agentId} wrote shared turn ${turn}.`,
53
+ usage: {
54
+ inputTokens: 9,
55
+ outputTokens: 5,
56
+ totalTokens: 14
57
+ },
58
+ costUsd: 0.001
59
+ };
60
+ }
61
+ };
62
+
63
+ const result = await run(createDeterministicSharedTestMission(model));
64
+
65
+ expect(requests).toHaveLength(3);
66
+ expect(result.trace.protocol).toBe("shared");
67
+ expect(result.trace.modelProviderId).toBe("configured-shared-model");
68
+ expect(result.trace.events.map((event) => event.type)).toEqual([
69
+ "role-assignment",
70
+ "role-assignment",
71
+ "role-assignment",
72
+ "agent-turn",
73
+ "agent-turn",
74
+ "agent-turn",
75
+ "final"
76
+ ]);
77
+
78
+ for (const [index, request] of requests.entries()) {
79
+ const turn = index + 1;
80
+ const userMessage = request.messages.find((message) => message.role === "user");
81
+ const systemMessage = request.messages.find((message) => message.role === "system");
82
+
83
+ expect(request.metadata).toMatchObject({
84
+ protocol: "shared",
85
+ tier: "fast",
86
+ turn
87
+ });
88
+ expect(userMessage?.content).toContain("Decide whether the shared protocol can support portable replay.");
89
+ expect(userMessage?.content).toContain(`Shared turn ${turn}`);
90
+ expect(userMessage?.content).toContain("Shared state:");
91
+ expect(systemMessage?.content).toContain("Shared multi-agent protocol");
92
+ }
93
+
94
+ expect(requests[0]?.messages.find((message) => message.role === "user")?.content).toContain("(empty)");
95
+ expect(requests[1]?.messages.find((message) => message.role === "user")?.content).toContain("(empty)");
96
+ expect(requests[2]?.messages.find((message) => message.role === "user")?.content).toContain("(empty)");
97
+ expect(requests[1]?.messages.find((message) => message.role === "user")?.content).not.toContain(
98
+ "state-initializer:agent-1 =>"
99
+ );
100
+ expect(result.output).toBe(
101
+ [
102
+ "state-initializer:agent-1 => state-initializer:agent-1 wrote shared turn 1.",
103
+ "state-reviewer:agent-2 => state-reviewer:agent-2 wrote shared turn 2.",
104
+ "state-synthesizer:agent-3 => state-synthesizer:agent-3 wrote shared turn 3."
105
+ ].join("\n")
106
+ );
107
+ expect(result.transcript).toHaveLength(3);
108
+ expect(result.trace.transcript).toEqual(result.transcript);
109
+ expect(JSON.parse(JSON.stringify(result.trace))).toEqual(result.trace);
110
+ expect(result.cost.totalTokens).toBe(42);
111
+ });
112
+
113
+ it("streams shared coordination events through the high-level SDK handle", async () => {
114
+ const handle = Dogpile.stream(createDeterministicSharedTestMission());
115
+
116
+ const streamedEvents: RunEvent[] = [];
117
+ for await (const event of handle) {
118
+ if (event.type !== "error") {
119
+ streamedEvents.push(event as RunEvent);
120
+ }
121
+ }
122
+ const result = await handle.result;
123
+
124
+ const expectedOutput = [
125
+ "state-initializer:agent-1 => state-initializer:agent-1 initialized the shared state.",
126
+ "state-reviewer:agent-2 => state-reviewer:agent-2 initialized the shared state.",
127
+ "state-synthesizer:agent-3 => state-synthesizer:agent-3 initialized the shared state."
128
+ ].join("\n");
129
+
130
+ expect(result.output).toBe(expectedOutput);
131
+ expect(streamedEvents.map((event) => event.type)).toEqual([
132
+ "role-assignment",
133
+ "role-assignment",
134
+ "role-assignment",
135
+ "agent-turn",
136
+ "agent-turn",
137
+ "agent-turn",
138
+ "final"
139
+ ]);
140
+ expect(result.trace.events).toEqual(streamedEvents);
141
+ expect(result.transcript).toEqual([
142
+ {
143
+ agentId: "agent-1",
144
+ role: "state-initializer",
145
+ input:
146
+ "Mission: Decide whether the shared protocol can support portable replay.\nShared turn 1: read the shared state and return an improved shared-state update.\n\nShared state:\n(empty)",
147
+ output: "state-initializer:agent-1 initialized the shared state."
148
+ },
149
+ {
150
+ agentId: "agent-2",
151
+ role: "state-reviewer",
152
+ input:
153
+ "Mission: Decide whether the shared protocol can support portable replay.\nShared turn 2: read the shared state and return an improved shared-state update.\n\nShared state:\n(empty)",
154
+ output: "state-reviewer:agent-2 initialized the shared state."
155
+ },
156
+ {
157
+ agentId: "agent-3",
158
+ role: "state-synthesizer",
159
+ input:
160
+ "Mission: Decide whether the shared protocol can support portable replay.\nShared turn 3: read the shared state and return an improved shared-state update.\n\nShared state:\n(empty)",
161
+ output: "state-synthesizer:agent-3 initialized the shared state."
162
+ }
163
+ ]);
164
+ expect(result.trace.transcript).toEqual(result.transcript);
165
+ });
166
+
167
+ it("gives every shared agent the same organizational memory snapshot", async () => {
168
+ const requests: ModelRequest[] = [];
169
+ const model: ConfiguredModelProvider = {
170
+ id: "shared-memory-snapshot-model",
171
+ async generate(request) {
172
+ requests.push(request);
173
+ return { text: `output-${String(request.metadata.agentId)}` };
174
+ }
175
+ };
176
+
177
+ await run({
178
+ intent: "Coordinate from historical organization memory.",
179
+ protocol: {
180
+ kind: "shared",
181
+ maxTurns: 2,
182
+ organizationalMemory: "Prior task memory: uploader and verifier roles were useful."
183
+ },
184
+ tier: "fast",
185
+ model,
186
+ agents: [
187
+ { id: "agent-a", role: "autonomous-agent" },
188
+ { id: "agent-b", role: "autonomous-agent" }
189
+ ]
190
+ });
191
+
192
+ expect(requests).toHaveLength(2);
193
+ const userPrompts = requests.map((request) => request.messages.find((message) => message.role === "user")?.content);
194
+ expect(userPrompts).toEqual([
195
+ "Mission: Coordinate from historical organization memory.\nShared turn 1: read the shared state and return an improved shared-state update.\n\nShared state:\nPrior task memory: uploader and verifier roles were useful.",
196
+ "Mission: Coordinate from historical organization memory.\nShared turn 2: read the shared state and return an improved shared-state update.\n\nShared state:\nPrior task memory: uploader and verifier roles were useful."
197
+ ]);
198
+ expect(userPrompts[1]).not.toContain("output-agent-a");
199
+ });
200
+
201
+ it("threads runtime tool availability through every shared model turn", async () => {
202
+ interface LookupInput extends JsonObject {
203
+ readonly query: string;
204
+ }
205
+
206
+ interface LookupOutput extends JsonObject {
207
+ readonly answer: string;
208
+ }
209
+
210
+ const requests: ModelRequest[] = [];
211
+ const lookupTool: RuntimeTool<LookupInput, LookupOutput> = {
212
+ identity: {
213
+ id: "fixture.lookup",
214
+ name: "lookup",
215
+ description: "Lookup contextual facts for the active mission."
216
+ },
217
+ inputSchema: {
218
+ kind: "json-schema",
219
+ schema: {
220
+ type: "object",
221
+ properties: {
222
+ query: { type: "string" }
223
+ },
224
+ required: ["query"],
225
+ additionalProperties: false
226
+ }
227
+ },
228
+ execute(input, context) {
229
+ return {
230
+ type: "success",
231
+ toolCallId: context.toolCallId,
232
+ tool: this.identity,
233
+ output: {
234
+ answer: `found:${input.query}`
235
+ }
236
+ };
237
+ }
238
+ };
239
+ const model: ConfiguredModelProvider = {
240
+ id: "shared-tool-availability-model",
241
+ async generate(request) {
242
+ requests.push(request);
243
+ return { text: `turn-${requests.length}` };
244
+ }
245
+ };
246
+
247
+ await run({
248
+ intent: "Use available tools while updating shared state.",
249
+ protocol: { kind: "shared", maxTurns: 2 },
250
+ tier: "fast",
251
+ model,
252
+ agents: [
253
+ { id: "initializer-seat", role: "initializer" },
254
+ { id: "reviewer-seat", role: "reviewer" }
255
+ ],
256
+ tools: [lookupTool]
257
+ });
258
+
259
+ expect(requests).toHaveLength(2);
260
+ expect(requests.map((request) => request.metadata.tools)).toEqual([
261
+ runtimeToolManifest([lookupTool]),
262
+ runtimeToolManifest([lookupTool])
263
+ ]);
264
+ });
265
+ });