agentxjs 2.9.0-dev-20260317033749 → 2.9.0-dev-20260317035728
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxjs",
|
|
3
|
-
"version": "2.9.0-dev-
|
|
3
|
+
"version": "2.9.0-dev-20260317035728",
|
|
4
4
|
"description": "AgentX Client SDK - Local and remote AI agent management",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"test": "bun test bdd/"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@agentxjs/core": "2.9.0-dev-
|
|
26
|
+
"@agentxjs/core": "2.9.0-dev-20260317035728",
|
|
27
27
|
"@deepracticex/id": "^0.2.0",
|
|
28
28
|
"@deepracticex/logger": "^1.2.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@agentxjs/devtools": "2.2.2-dev-
|
|
31
|
+
"@agentxjs/devtools": "2.2.2-dev-20260317035728",
|
|
32
32
|
"@deepracticex/bdd": "^0.3.0",
|
|
33
33
|
"tsx": "^4.19.0",
|
|
34
34
|
"typescript": "^5.3.3"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Call End-to-End Round-Trip Test
|
|
3
|
+
*
|
|
4
|
+
* Uses REAL MonoDriver + Runtime + SQLite persistence.
|
|
5
|
+
* Sends a message that triggers a tool call, then restores
|
|
6
|
+
* from persistence and verifies tool calls survive.
|
|
7
|
+
*
|
|
8
|
+
* Requires ANTHROPIC_API_KEY or DEEPRACTICE_API_KEY in .env.local
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
12
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import type { Message } from "@agentxjs/core/agent";
|
|
16
|
+
import type { DriverConfig } from "@agentxjs/core/driver";
|
|
17
|
+
import { EventBusImpl } from "@agentxjs/core/event";
|
|
18
|
+
import { createAgentXRuntime } from "@agentxjs/core/runtime";
|
|
19
|
+
import { createMonoDriver } from "@agentxjs/mono-driver";
|
|
20
|
+
import { createPersistence, sqliteDriver } from "@agentxjs/node-platform/persistence";
|
|
21
|
+
import { messagesToConversations } from "../reducer";
|
|
22
|
+
import type { AssistantConversation, ToolBlock } from "../types";
|
|
23
|
+
|
|
24
|
+
let tempDir: string;
|
|
25
|
+
let persistence: Awaited<ReturnType<typeof createPersistence>>;
|
|
26
|
+
|
|
27
|
+
const apiKey = process.env.ANTHROPIC_API_KEY || process.env.DEEPRACTICE_API_KEY;
|
|
28
|
+
const baseUrl = process.env.DEEPRACTICE_BASE_URL;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentx-tool-e2e-"));
|
|
32
|
+
persistence = await createPersistence(sqliteDriver({ path: join(tempDir, "test.db") }));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("Tool call E2E round-trip", () => {
|
|
40
|
+
test("tool call survives persist → restore", async () => {
|
|
41
|
+
if (!apiKey) {
|
|
42
|
+
console.log("Skipping: no API key");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Create runtime with a simple bash tool
|
|
47
|
+
const createDriver = (config: DriverConfig) =>
|
|
48
|
+
createMonoDriver({
|
|
49
|
+
...config,
|
|
50
|
+
apiKey: apiKey!,
|
|
51
|
+
baseUrl,
|
|
52
|
+
model: process.env.DEEPRACTICE_MODEL || "claude-sonnet-4-20250514",
|
|
53
|
+
provider: "anthropic",
|
|
54
|
+
maxSteps: 5,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const eventBus = new EventBusImpl();
|
|
58
|
+
const runtime = createAgentXRuntime(
|
|
59
|
+
{
|
|
60
|
+
containerRepository: persistence.containers,
|
|
61
|
+
imageRepository: persistence.images,
|
|
62
|
+
sessionRepository: persistence.sessions,
|
|
63
|
+
eventBus,
|
|
64
|
+
bashProvider: {
|
|
65
|
+
type: "test",
|
|
66
|
+
execute: async (cmd: string) => ({
|
|
67
|
+
stdout: `executed: ${cmd}`,
|
|
68
|
+
stderr: "",
|
|
69
|
+
exitCode: 0,
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
createDriver
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Create image
|
|
77
|
+
const { createImage } = await import("@agentxjs/core/image");
|
|
78
|
+
const image = await createImage(
|
|
79
|
+
{
|
|
80
|
+
containerId: "default",
|
|
81
|
+
name: "Tool Test Agent",
|
|
82
|
+
embody: {
|
|
83
|
+
systemPrompt:
|
|
84
|
+
"You have a bash tool. When asked to run a command, use the bash tool. Be brief.",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
imageRepository: persistence.images,
|
|
89
|
+
sessionRepository: persistence.sessions,
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const imageId = image.toRecord().imageId;
|
|
94
|
+
const sessionId = image.toRecord().sessionId;
|
|
95
|
+
|
|
96
|
+
// Create agent and send a message that should trigger tool use
|
|
97
|
+
const agent = await runtime.createAgent({ imageId });
|
|
98
|
+
|
|
99
|
+
// Debug: track ALL events
|
|
100
|
+
const allEvents: string[] = [];
|
|
101
|
+
eventBus.onAny((event: any) => {
|
|
102
|
+
allEvents.push(event.type);
|
|
103
|
+
if (event.type === "assistant_message") {
|
|
104
|
+
const content = event.data?.content;
|
|
105
|
+
const hasToolCall =
|
|
106
|
+
Array.isArray(content) && content.some((p: any) => p.type === "tool-call");
|
|
107
|
+
console.log(
|
|
108
|
+
` [EVENT] assistant_message: hasToolCall=${hasToolCall}, content=${JSON.stringify(content).substring(0, 80)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Wait for response completion
|
|
114
|
+
const responsePromise = new Promise<void>((resolve) => {
|
|
115
|
+
eventBus.onAny((event: any) => {
|
|
116
|
+
if (event.type === "message_stop" && event.data?.stopReason === "end_turn") {
|
|
117
|
+
setTimeout(resolve, 500);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Send message that triggers bash tool
|
|
123
|
+
await runtime.receive(
|
|
124
|
+
agent.instanceId,
|
|
125
|
+
'Run the command "echo hello world" using the bash tool.'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Wait for completion
|
|
129
|
+
await responsePromise;
|
|
130
|
+
|
|
131
|
+
// Debug: show event sequence
|
|
132
|
+
console.log("\n=== Event sequence ===");
|
|
133
|
+
console.log(` ${allEvents.join(" → ")}`);
|
|
134
|
+
|
|
135
|
+
// === Phase 1: Check persisted messages ===
|
|
136
|
+
const messages = await persistence.sessions.getMessages(sessionId);
|
|
137
|
+
|
|
138
|
+
console.log("\n=== Persisted messages ===");
|
|
139
|
+
for (const msg of messages) {
|
|
140
|
+
const content = (msg as any).content ?? (msg as any).toolResult;
|
|
141
|
+
const preview =
|
|
142
|
+
typeof content === "string"
|
|
143
|
+
? content.substring(0, 80)
|
|
144
|
+
: JSON.stringify(content).substring(0, 120);
|
|
145
|
+
console.log(` ${msg.subtype}: ${preview}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Should have: user + assistant(tool-call) + tool-result + assistant(text)
|
|
149
|
+
const assistantMsgs = messages.filter((m) => m.subtype === "assistant");
|
|
150
|
+
const toolResultMsgs = messages.filter((m) => m.subtype === "tool-result");
|
|
151
|
+
|
|
152
|
+
console.log(
|
|
153
|
+
`\n Total: ${messages.length} messages (${assistantMsgs.length} assistant, ${toolResultMsgs.length} tool-result)`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Verify assistant message has tool-call content parts
|
|
157
|
+
const assistantWithToolCall = assistantMsgs.find((m) => {
|
|
158
|
+
const content = (m as any).content;
|
|
159
|
+
if (Array.isArray(content)) {
|
|
160
|
+
return content.some((p: any) => p.type === "tool-call");
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log(
|
|
166
|
+
` Assistant with tool-call: ${assistantWithToolCall ? "YES" : "NO - BUG IN PERSISTENCE"}`
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// === Phase 2: Restore from persistence (simulate page refresh) ===
|
|
170
|
+
const conversations = messagesToConversations(messages);
|
|
171
|
+
|
|
172
|
+
console.log("\n=== Restored conversations ===");
|
|
173
|
+
for (const conv of conversations) {
|
|
174
|
+
if (conv.role !== "error") {
|
|
175
|
+
const blocks = (conv as any).blocks;
|
|
176
|
+
console.log(
|
|
177
|
+
` ${conv.role}: [${blocks.map((b: any) => `${b.type}${b.type === "tool" ? `(${b.toolName})` : ""}`).join(", ")}]`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Find tool blocks
|
|
183
|
+
const assistantConvs = conversations.filter(
|
|
184
|
+
(c) => c.role === "assistant"
|
|
185
|
+
) as AssistantConversation[];
|
|
186
|
+
const allBlocks = assistantConvs.flatMap((c) => c.blocks);
|
|
187
|
+
const toolBlocks = allBlocks.filter((b) => b.type === "tool") as ToolBlock[];
|
|
188
|
+
|
|
189
|
+
console.log(`\n Tool blocks after restore: ${toolBlocks.length}`);
|
|
190
|
+
for (const tb of toolBlocks) {
|
|
191
|
+
console.log(
|
|
192
|
+
` ${tb.toolName}: status=${tb.status}, result=${tb.toolResult?.substring(0, 50)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// THE CRITICAL ASSERTION
|
|
197
|
+
expect(toolBlocks.length).toBeGreaterThanOrEqual(1);
|
|
198
|
+
expect(toolBlocks[0].status).toBe("completed");
|
|
199
|
+
expect(toolBlocks[0].toolResult).toBeDefined();
|
|
200
|
+
|
|
201
|
+
// Cleanup
|
|
202
|
+
await runtime.destroyAgent(agent.instanceId);
|
|
203
|
+
await runtime.shutdown();
|
|
204
|
+
}, 60_000);
|
|
205
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Call Round-Trip Test
|
|
3
|
+
*
|
|
4
|
+
* Simulates the FULL lifecycle:
|
|
5
|
+
* 1. MonoDriver emits stream events (tool call flow)
|
|
6
|
+
* 2. MessageAssembler processes into Messages
|
|
7
|
+
* 3. Messages are "persisted" (collected)
|
|
8
|
+
* 4. messagesToConversations restores from "persistence"
|
|
9
|
+
* 5. Verify tool calls survive the round-trip
|
|
10
|
+
*
|
|
11
|
+
* This reproduces the page-refresh bug: tool calls lost after reconnection.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import type { Message } from "@agentxjs/core/agent";
|
|
16
|
+
import {
|
|
17
|
+
createInitialMessageAssemblerState,
|
|
18
|
+
messageAssemblerProcessor,
|
|
19
|
+
} from "@agentxjs/core/agent";
|
|
20
|
+
import { messagesToConversations } from "../reducer";
|
|
21
|
+
import type { AssistantConversation, ToolBlock } from "../types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helper: create a stream event
|
|
25
|
+
*/
|
|
26
|
+
function streamEvent(type: string, data: Record<string, unknown>) {
|
|
27
|
+
return { type, timestamp: Date.now(), data };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Helper: process events through MessageAssembler, collect output messages
|
|
32
|
+
*/
|
|
33
|
+
function processEvents(events: Array<{ type: string; timestamp: number; data: any }>) {
|
|
34
|
+
let state = createInitialMessageAssemblerState();
|
|
35
|
+
const allMessages: Message[] = [];
|
|
36
|
+
|
|
37
|
+
for (const event of events) {
|
|
38
|
+
const [newState, outputs] = messageAssemblerProcessor(state, event);
|
|
39
|
+
state = newState;
|
|
40
|
+
|
|
41
|
+
for (const output of outputs) {
|
|
42
|
+
// Collect message-type outputs (these are what Runtime persists)
|
|
43
|
+
if (output.type === "assistant_message" || output.type === "tool_result_message") {
|
|
44
|
+
allMessages.push(output.data as Message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return allMessages;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("Tool call round-trip (persist → restore)", () => {
|
|
53
|
+
test("single tool call: events → messages → conversations", () => {
|
|
54
|
+
// Simulate MonoDriver event sequence for a tool call
|
|
55
|
+
const events = [
|
|
56
|
+
// Step 1: LLM decides to call a tool
|
|
57
|
+
streamEvent("message_start", { messageId: "msg_1", model: "claude" }),
|
|
58
|
+
streamEvent("text_delta", { text: "Let me check that." }),
|
|
59
|
+
streamEvent("tool_use_start", { toolCallId: "call_1", toolName: "bash" }),
|
|
60
|
+
streamEvent("input_json_delta", { partialJson: '{"command":' }),
|
|
61
|
+
streamEvent("input_json_delta", { partialJson: '"ls /tmp"}' }),
|
|
62
|
+
streamEvent("tool_use_stop", {
|
|
63
|
+
toolCallId: "call_1",
|
|
64
|
+
toolName: "bash",
|
|
65
|
+
input: { command: "ls /tmp" },
|
|
66
|
+
}),
|
|
67
|
+
// MonoDriver injects message_stop before tool_result
|
|
68
|
+
streamEvent("message_stop", { stopReason: "tool_use" }),
|
|
69
|
+
// Tool result
|
|
70
|
+
streamEvent("tool_result", {
|
|
71
|
+
toolCallId: "call_1",
|
|
72
|
+
result: "file1.txt\nfile2.txt",
|
|
73
|
+
isError: false,
|
|
74
|
+
}),
|
|
75
|
+
// Step 2: LLM responds with text
|
|
76
|
+
streamEvent("message_start", { messageId: "msg_2", model: "claude" }),
|
|
77
|
+
streamEvent("text_delta", { text: "Found 2 files." }),
|
|
78
|
+
streamEvent("message_stop", { stopReason: "end_turn" }),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Phase 1: Process events → get persisted messages
|
|
82
|
+
const messages: Message[] = [
|
|
83
|
+
// User message (always persisted separately by Runtime)
|
|
84
|
+
{
|
|
85
|
+
id: "msg_user",
|
|
86
|
+
role: "user",
|
|
87
|
+
subtype: "user",
|
|
88
|
+
content: "List files in /tmp",
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
} as any,
|
|
91
|
+
// Messages from MessageAssembler
|
|
92
|
+
...processEvents(events),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
console.log("\n=== Persisted messages ===");
|
|
96
|
+
for (const msg of messages) {
|
|
97
|
+
console.log(
|
|
98
|
+
` ${msg.subtype}: ${JSON.stringify((msg as any).content ?? (msg as any).toolResult).substring(0, 100)}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Phase 2: Restore from "persistence" (page refresh scenario)
|
|
103
|
+
const conversations = messagesToConversations(messages);
|
|
104
|
+
|
|
105
|
+
console.log("\n=== Restored conversations ===");
|
|
106
|
+
for (const conv of conversations) {
|
|
107
|
+
console.log(
|
|
108
|
+
` ${conv.role}: blocks=[${conv.role !== "error" ? (conv as any).blocks.map((b: any) => b.type).join(", ") : conv.message}]`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Verify: user message exists
|
|
113
|
+
expect(conversations[0].role).toBe("user");
|
|
114
|
+
|
|
115
|
+
// Verify: assistant conversation has tool block
|
|
116
|
+
const assistantConvs = conversations.filter(
|
|
117
|
+
(c) => c.role === "assistant"
|
|
118
|
+
) as AssistantConversation[];
|
|
119
|
+
expect(assistantConvs.length).toBeGreaterThan(0);
|
|
120
|
+
|
|
121
|
+
// Find tool block in any assistant conversation
|
|
122
|
+
const allBlocks = assistantConvs.flatMap((c) => c.blocks);
|
|
123
|
+
const toolBlocks = allBlocks.filter((b) => b.type === "tool") as ToolBlock[];
|
|
124
|
+
|
|
125
|
+
console.log("\n=== Tool blocks ===");
|
|
126
|
+
for (const tb of toolBlocks) {
|
|
127
|
+
console.log(
|
|
128
|
+
` ${tb.toolName}: id=${tb.toolUseId}, status=${tb.status}, result=${tb.toolResult}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// THIS IS THE CRITICAL ASSERTION
|
|
133
|
+
// If this fails, tool calls are lost on page refresh
|
|
134
|
+
expect(toolBlocks.length).toBeGreaterThanOrEqual(1);
|
|
135
|
+
expect(toolBlocks[0].toolName).toBe("bash");
|
|
136
|
+
expect(toolBlocks[0].toolUseId).toBe("call_1");
|
|
137
|
+
expect(toolBlocks[0].status).toBe("completed");
|
|
138
|
+
expect(toolBlocks[0].toolResult).toBe("file1.txt\nfile2.txt");
|
|
139
|
+
|
|
140
|
+
// Verify text is also present
|
|
141
|
+
const textBlocks = allBlocks.filter((b) => b.type === "text");
|
|
142
|
+
expect(textBlocks.length).toBeGreaterThanOrEqual(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("multi-step tool call: second step text survives", () => {
|
|
146
|
+
// This tests the MonoDriver multi-step flow where message_start
|
|
147
|
+
// is only emitted once but there are multiple steps
|
|
148
|
+
const events = [
|
|
149
|
+
// Single message_start for entire multi-step
|
|
150
|
+
streamEvent("message_start", { messageId: "msg_1", model: "claude" }),
|
|
151
|
+
// Step 1: tool call
|
|
152
|
+
streamEvent("tool_use_start", { toolCallId: "call_1", toolName: "write" }),
|
|
153
|
+
streamEvent("input_json_delta", { partialJson: '{"path":"test.txt","content":"hello"}' }),
|
|
154
|
+
streamEvent("tool_use_stop", {
|
|
155
|
+
toolCallId: "call_1",
|
|
156
|
+
toolName: "write",
|
|
157
|
+
input: { path: "test.txt", content: "hello" },
|
|
158
|
+
}),
|
|
159
|
+
// MonoDriver injects message_stop before tool_result
|
|
160
|
+
streamEvent("message_stop", { stopReason: "tool_use" }),
|
|
161
|
+
streamEvent("tool_result", {
|
|
162
|
+
toolCallId: "call_1",
|
|
163
|
+
result: { success: true },
|
|
164
|
+
isError: false,
|
|
165
|
+
}),
|
|
166
|
+
// Step 2: NO new message_start (MonoDriver only emits once)
|
|
167
|
+
// This is where the bug might be
|
|
168
|
+
streamEvent("message_start", { messageId: "msg_2", model: "claude" }),
|
|
169
|
+
streamEvent("text_delta", { text: "File written successfully." }),
|
|
170
|
+
streamEvent("message_stop", { stopReason: "end_turn" }),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const messages: Message[] = [
|
|
174
|
+
{
|
|
175
|
+
id: "msg_user",
|
|
176
|
+
role: "user",
|
|
177
|
+
subtype: "user",
|
|
178
|
+
content: "Write hello to test.txt",
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
} as any,
|
|
181
|
+
...processEvents(events),
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
console.log("\n=== Multi-step persisted messages ===");
|
|
185
|
+
for (const msg of messages) {
|
|
186
|
+
console.log(
|
|
187
|
+
` ${msg.subtype}: ${JSON.stringify((msg as any).content ?? (msg as any).toolResult).substring(0, 120)}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const conversations = messagesToConversations(messages);
|
|
192
|
+
|
|
193
|
+
console.log("\n=== Multi-step restored conversations ===");
|
|
194
|
+
for (const conv of conversations) {
|
|
195
|
+
if (conv.role !== "error") {
|
|
196
|
+
const ac = conv as any;
|
|
197
|
+
console.log(
|
|
198
|
+
` ${conv.role}: blocks=[${ac.blocks.map((b: any) => `${b.type}${b.type === "tool" ? `(${b.toolName})` : ""}`).join(", ")}]`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// User + at least one assistant
|
|
204
|
+
expect(conversations[0].role).toBe("user");
|
|
205
|
+
|
|
206
|
+
const assistantConvs = conversations.filter(
|
|
207
|
+
(c) => c.role === "assistant"
|
|
208
|
+
) as AssistantConversation[];
|
|
209
|
+
const allBlocks = assistantConvs.flatMap((c) => c.blocks);
|
|
210
|
+
|
|
211
|
+
// Tool call should be present
|
|
212
|
+
const toolBlocks = allBlocks.filter((b) => b.type === "tool") as ToolBlock[];
|
|
213
|
+
expect(toolBlocks.length).toBe(1);
|
|
214
|
+
expect(toolBlocks[0].toolName).toBe("write");
|
|
215
|
+
|
|
216
|
+
// Final text should also be present
|
|
217
|
+
const textBlocks = allBlocks.filter((b) => b.type === "text");
|
|
218
|
+
expect(textBlocks.length).toBeGreaterThanOrEqual(1);
|
|
219
|
+
const hasSuccessText = textBlocks.some((b) => (b as any).content.includes("successfully"));
|
|
220
|
+
expect(hasSuccessText).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
});
|