@sandagent/runner-cli 0.1.1 → 0.1.2-beta.2
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/README.md +153 -0
- package/dist/__tests__/runner-cli.integration.test.d.ts +6 -0
- package/dist/__tests__/runner-cli.integration.test.d.ts.map +1 -0
- package/dist/__tests__/runner-cli.integration.test.js +76 -0
- package/dist/__tests__/runner-cli.integration.test.js.map +1 -0
- package/dist/__tests__/runner.test.js +7 -3
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/bundle.mjs +412 -212
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +34 -29
- package/dist/cli.js.map +1 -1
- package/dist/runner.d.ts +5 -13
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +31 -13
- package/dist/runner.js.map +1 -1
- package/package.json +29 -14
- package/LICENSE +0 -201
package/dist/bundle.mjs
CHANGED
|
@@ -3,12 +3,244 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
|
|
6
|
+
// ../../packages/runner-claude/dist/ai-sdk-stream.js
|
|
7
|
+
import { writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
var UNKNOWN_TOOL_NAME = "unknown-tool";
|
|
10
|
+
function formatDataStream(data) {
|
|
11
|
+
return `data: ${JSON.stringify(data)}
|
|
12
|
+
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
function generateId() {
|
|
16
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
17
|
+
}
|
|
18
|
+
function convertUsageToAISDK(usage) {
|
|
19
|
+
const inputTokens = usage.input_tokens ?? 0;
|
|
20
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
21
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
22
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
23
|
+
return {
|
|
24
|
+
inputTokens: {
|
|
25
|
+
total: inputTokens + cacheWrite + cacheRead,
|
|
26
|
+
noCache: inputTokens,
|
|
27
|
+
cacheRead,
|
|
28
|
+
cacheWrite
|
|
29
|
+
},
|
|
30
|
+
outputTokens: { total: outputTokens },
|
|
31
|
+
raw: usage
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function mapFinishReason(subtype, isError) {
|
|
35
|
+
if (isError)
|
|
36
|
+
return "error";
|
|
37
|
+
switch (subtype) {
|
|
38
|
+
case "success":
|
|
39
|
+
return "stop";
|
|
40
|
+
case "error_max_turns":
|
|
41
|
+
return "length";
|
|
42
|
+
case "error_during_execution":
|
|
43
|
+
case "error_max_structured_output_retries":
|
|
44
|
+
return "error";
|
|
45
|
+
case void 0:
|
|
46
|
+
return "stop";
|
|
47
|
+
default:
|
|
48
|
+
return "other";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function extractToolUses(content) {
|
|
52
|
+
if (!Array.isArray(content))
|
|
53
|
+
return [];
|
|
54
|
+
return content.filter((item) => typeof item === "object" && item !== null && "type" in item && item.type === "tool_use").map((item) => ({
|
|
55
|
+
id: item.id || generateId(),
|
|
56
|
+
name: item.name || UNKNOWN_TOOL_NAME,
|
|
57
|
+
input: item.input
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
var AISDKStreamConverter = class {
|
|
61
|
+
systemMessage;
|
|
62
|
+
hasEmittedStart = false;
|
|
63
|
+
accumulatedText = "";
|
|
64
|
+
textPartId;
|
|
65
|
+
streamedTextLength = 0;
|
|
66
|
+
// Track text already emitted via stream_events
|
|
67
|
+
hasReceivedStreamEvents = false;
|
|
68
|
+
sessionId;
|
|
69
|
+
partIdMap = /* @__PURE__ */ new Map();
|
|
70
|
+
/**
|
|
71
|
+
* Get the current session ID from the stream
|
|
72
|
+
*/
|
|
73
|
+
get currentSessionId() {
|
|
74
|
+
if (!this.sessionId) {
|
|
75
|
+
throw new Error("Session ID is not set");
|
|
76
|
+
}
|
|
77
|
+
return this.sessionId;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Helper to emit SSE data
|
|
81
|
+
*/
|
|
82
|
+
emit(data) {
|
|
83
|
+
return formatDataStream(data);
|
|
84
|
+
}
|
|
85
|
+
setPartId(index, partId) {
|
|
86
|
+
const partIdKey = `${this.currentSessionId}-${index}`;
|
|
87
|
+
this.partIdMap.set(partIdKey, partId);
|
|
88
|
+
}
|
|
89
|
+
getPartId(index) {
|
|
90
|
+
const partIdKey = `${this.currentSessionId}-${index}`;
|
|
91
|
+
if (this.partIdMap.has(partIdKey)) {
|
|
92
|
+
return this.partIdMap.get(partIdKey) ?? "";
|
|
93
|
+
}
|
|
94
|
+
throw new Error("Part ID not found");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Helper to emit tool call
|
|
98
|
+
*/
|
|
99
|
+
*emitToolCall(message) {
|
|
100
|
+
const event = message.event;
|
|
101
|
+
if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
|
|
102
|
+
const toolCallId = event.content_block.id;
|
|
103
|
+
this.setPartId(event.index, toolCallId);
|
|
104
|
+
yield this.emit({
|
|
105
|
+
type: "tool-input-start",
|
|
106
|
+
toolCallId,
|
|
107
|
+
toolName: event.content_block.name,
|
|
108
|
+
dynamic: true,
|
|
109
|
+
providerExecuted: true
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") {
|
|
113
|
+
yield this.emit({
|
|
114
|
+
type: "tool-input-delta",
|
|
115
|
+
toolCallId: this.getPartId(event.index),
|
|
116
|
+
inputTextDelta: event.delta.partial_json
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
*emitTextBlockEvent(event) {
|
|
121
|
+
if (event.type === "content_block_start" && event.content_block.type === "text") {
|
|
122
|
+
const partId = `text_${generateId()}`;
|
|
123
|
+
this.setPartId(event.index, partId);
|
|
124
|
+
yield this.emit({
|
|
125
|
+
type: "text-start",
|
|
126
|
+
id: partId
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
|
130
|
+
yield this.emit({
|
|
131
|
+
type: "text-delta",
|
|
132
|
+
id: this.getPartId(event.index),
|
|
133
|
+
delta: event.delta.text
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (event.type === "content_block_stop") {
|
|
137
|
+
const partId = this.getPartId(event.index);
|
|
138
|
+
if (partId.startsWith("text_")) {
|
|
139
|
+
yield this.emit({ type: "text-end", id: partId });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Stream SDK messages and convert to AI SDK UI Data Stream format
|
|
145
|
+
*/
|
|
146
|
+
async *stream(messageIterator, options) {
|
|
147
|
+
const debugMessages = [];
|
|
148
|
+
const debugFile = `ai-sdk-stream-debug-${Date.now()}.json`;
|
|
149
|
+
try {
|
|
150
|
+
for await (const message of messageIterator) {
|
|
151
|
+
debugMessages.push(message);
|
|
152
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
153
|
+
this.systemMessage = message;
|
|
154
|
+
this.sessionId = this.systemMessage.session_id;
|
|
155
|
+
}
|
|
156
|
+
if (message.type === "stream_event") {
|
|
157
|
+
const streamEvent = message;
|
|
158
|
+
const event = streamEvent.event;
|
|
159
|
+
if (event.type === "message_start" && !this.hasEmittedStart) {
|
|
160
|
+
this.hasEmittedStart = true;
|
|
161
|
+
yield this.emit({ type: "start", messageId: event.message.id });
|
|
162
|
+
yield this.emit({
|
|
163
|
+
type: "message-metadata",
|
|
164
|
+
messageMetadata: {
|
|
165
|
+
tools: this.systemMessage?.tools,
|
|
166
|
+
model: this.systemMessage?.model,
|
|
167
|
+
sessionId: this.systemMessage?.session_id,
|
|
168
|
+
agents: this.systemMessage?.agents,
|
|
169
|
+
skills: this.systemMessage?.skills
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
yield* this.emitTextBlockEvent(event);
|
|
174
|
+
yield* this.emitToolCall(streamEvent);
|
|
175
|
+
}
|
|
176
|
+
if (message.type === "assistant") {
|
|
177
|
+
const assistantMsg = message;
|
|
178
|
+
const content = assistantMsg.message?.content;
|
|
179
|
+
if (!content)
|
|
180
|
+
continue;
|
|
181
|
+
const tools = extractToolUses(content);
|
|
182
|
+
for (const tool of tools) {
|
|
183
|
+
yield this.emit({
|
|
184
|
+
type: "tool-input-available",
|
|
185
|
+
toolCallId: tool.id,
|
|
186
|
+
toolName: tool.name,
|
|
187
|
+
input: tool.input,
|
|
188
|
+
dynamic: true,
|
|
189
|
+
providerExecuted: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (message.type === "user") {
|
|
194
|
+
const userMsg = message;
|
|
195
|
+
const content = userMsg.message?.content;
|
|
196
|
+
for (const part of content) {
|
|
197
|
+
if (part.type === "tool_result") {
|
|
198
|
+
yield this.emit({
|
|
199
|
+
type: "tool-output-available",
|
|
200
|
+
toolCallId: part.tool_use_id,
|
|
201
|
+
output: message.tool_use_result || part.content,
|
|
202
|
+
dynamic: true,
|
|
203
|
+
providerExecuted: true
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (message.type === "result") {
|
|
209
|
+
const resultMsg = message;
|
|
210
|
+
const finishTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
211
|
+
console.error(`[AISDKStream] Processing result message at ${finishTime}`);
|
|
212
|
+
const finishEvent = this.emit({
|
|
213
|
+
type: "finish",
|
|
214
|
+
finishReason: mapFinishReason(resultMsg.subtype, resultMsg.is_error),
|
|
215
|
+
messageMetadata: {
|
|
216
|
+
usage: convertUsageToAISDK(resultMsg.usage ?? {})
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
console.error(`[AISDKStream] Emitting finish event at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
220
|
+
yield finishEvent;
|
|
221
|
+
console.error(`[AISDKStream] Emitted [DONE] at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
debugMessages.push(error);
|
|
226
|
+
} finally {
|
|
227
|
+
if (debugMessages.length > 0) {
|
|
228
|
+
writeFile(join(process.cwd(), debugFile), JSON.stringify(debugMessages, null, 2), "utf-8").catch((writeError) => {
|
|
229
|
+
console.error(`[AISDKStream] Failed to write debug file:`, writeError);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
options?.onCleanup?.();
|
|
233
|
+
yield `data: [DONE]
|
|
234
|
+
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
function streamSDKMessagesToAISDKUI(messageIterator, options) {
|
|
240
|
+
return new AISDKStreamConverter().stream(messageIterator, options);
|
|
241
|
+
}
|
|
242
|
+
|
|
6
243
|
// ../../packages/runner-claude/dist/claude-runner.js
|
|
7
|
-
var SettingSource;
|
|
8
|
-
(function(SettingSource2) {
|
|
9
|
-
SettingSource2["user"] = "user";
|
|
10
|
-
SettingSource2["project"] = "project";
|
|
11
|
-
})(SettingSource || (SettingSource = {}));
|
|
12
244
|
function createCanUseToolCallback(claudeOptions) {
|
|
13
245
|
return async (toolName, input, options) => {
|
|
14
246
|
const { toolUseID } = options;
|
|
@@ -48,7 +280,7 @@ function createCanUseToolCallback(claudeOptions) {
|
|
|
48
280
|
}
|
|
49
281
|
};
|
|
50
282
|
}
|
|
51
|
-
} catch
|
|
283
|
+
} catch {
|
|
52
284
|
}
|
|
53
285
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
54
286
|
}
|
|
@@ -83,22 +315,19 @@ var OPTIONAL_MODULES = {
|
|
|
83
315
|
};
|
|
84
316
|
function createClaudeRunner(options) {
|
|
85
317
|
return {
|
|
86
|
-
async *run(userInput
|
|
87
|
-
if (signal?.aborted) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
318
|
+
async *run(userInput) {
|
|
90
319
|
const apiKey = process.env.ANTHROPIC_API_KEY || process.env.AWS_BEARER_TOKEN_BEDROCK;
|
|
91
320
|
if (!apiKey) {
|
|
92
321
|
console.error("[SandAgent] Warning: ANTHROPIC_API_KEY or AWS_BEARER_TOKEN_BEDROCK not set. Using mock response.\nTo use the real Claude Agent SDK:\n1. Set ANTHROPIC_API_KEY or AWS_BEARER_TOKEN_BEDROCK environment variable\n2. Install the SDK: npm install @anthropic-ai/claude-agent-sdk");
|
|
93
|
-
yield* runMockAgent(options, userInput, signal);
|
|
322
|
+
yield* runMockAgent(options, userInput, options.abortController?.signal);
|
|
94
323
|
return;
|
|
95
324
|
}
|
|
96
325
|
const sdk = await loadClaudeAgentSDK();
|
|
97
326
|
if (sdk) {
|
|
98
|
-
yield* runWithClaudeAgentSDK(sdk, options, userInput
|
|
327
|
+
yield* runWithClaudeAgentSDK(sdk, options, userInput);
|
|
99
328
|
} else {
|
|
100
329
|
console.error("[SandAgent] Warning: @anthropic-ai/claude-agent-sdk not installed. Using mock response.\nInstall the SDK: npm install @anthropic-ai/claude-agent-sdk");
|
|
101
|
-
yield* runMockAgent(options, userInput, signal);
|
|
330
|
+
yield* runMockAgent(options, userInput, options.abortController?.signal);
|
|
102
331
|
}
|
|
103
332
|
}
|
|
104
333
|
};
|
|
@@ -115,31 +344,52 @@ async function loadClaudeAgentSDK() {
|
|
|
115
344
|
return null;
|
|
116
345
|
}
|
|
117
346
|
}
|
|
118
|
-
async function* runWithClaudeAgentSDK(sdk, options, userInput
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
347
|
+
async function* runWithClaudeAgentSDK(sdk, options, userInput) {
|
|
348
|
+
const outputFormat = options.outputFormat || "stream-json";
|
|
349
|
+
switch (outputFormat) {
|
|
350
|
+
case "text":
|
|
351
|
+
yield* runWithTextOutput(sdk, options, userInput);
|
|
352
|
+
break;
|
|
353
|
+
case "json":
|
|
354
|
+
yield* runWithJSONOutput(sdk, options, userInput);
|
|
355
|
+
break;
|
|
356
|
+
case "stream-json":
|
|
357
|
+
yield* runWithStreamJSONOutput(sdk, options, userInput);
|
|
358
|
+
break;
|
|
359
|
+
// case "stream":
|
|
360
|
+
default:
|
|
361
|
+
yield* runWithAISDKUIOutput(sdk, options, userInput);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function createSDKOptions(options) {
|
|
366
|
+
return {
|
|
123
367
|
model: options.model,
|
|
124
368
|
systemPrompt: options.systemPrompt,
|
|
125
369
|
maxTurns: options.maxTurns,
|
|
126
|
-
allowedTools: [
|
|
370
|
+
allowedTools: [
|
|
371
|
+
...options.allowedTools ?? [],
|
|
372
|
+
"Skill",
|
|
373
|
+
"WebSearch",
|
|
374
|
+
"WebFetch"
|
|
375
|
+
],
|
|
127
376
|
cwd: options.cwd,
|
|
128
377
|
env: options.env,
|
|
129
378
|
resume: options.resume,
|
|
130
|
-
settingSources: [
|
|
379
|
+
settingSources: ["project", "user"],
|
|
131
380
|
canUseTool: createCanUseToolCallback(options),
|
|
132
381
|
// Bypass all permission checks for automated execution
|
|
133
382
|
permissionMode: "bypassPermissions",
|
|
134
|
-
allowDangerouslySkipPermissions: true
|
|
383
|
+
allowDangerouslySkipPermissions: true,
|
|
384
|
+
// Enable partial messages for streaming
|
|
385
|
+
includePartialMessages: options.includePartialMessages
|
|
135
386
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
options: sdkOptions
|
|
139
|
-
});
|
|
387
|
+
}
|
|
388
|
+
function setupAbortHandler(queryIterator, signal) {
|
|
140
389
|
const abortHandler = async () => {
|
|
141
|
-
console.error("[ClaudeRunner]
|
|
390
|
+
console.error("[ClaudeRunner] Abort signal received, will call query.interrupt()...");
|
|
142
391
|
await queryIterator.interrupt();
|
|
392
|
+
console.error("[ClaudeRunner] query.interrupt() completed");
|
|
143
393
|
};
|
|
144
394
|
if (signal) {
|
|
145
395
|
console.error("[ClaudeRunner] Signal provided, adding abort listener");
|
|
@@ -150,163 +400,71 @@ async function* runWithClaudeAgentSDK(sdk, options, userInput, signal) {
|
|
|
150
400
|
} else {
|
|
151
401
|
console.error("[ClaudeRunner] No signal provided");
|
|
152
402
|
}
|
|
403
|
+
return abortHandler;
|
|
404
|
+
}
|
|
405
|
+
async function* runWithTextOutput(sdk, options, userInput, signal) {
|
|
406
|
+
const sdkOptions = createSDKOptions(options);
|
|
407
|
+
const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
|
|
408
|
+
const abortHandler = setupAbortHandler(queryIterator, options.abortController?.signal);
|
|
153
409
|
try {
|
|
410
|
+
let resultText = "";
|
|
154
411
|
for await (const message of queryIterator) {
|
|
155
|
-
if (message.type === "
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
messageId = message.message.id;
|
|
161
|
-
yield formatDataStream({
|
|
162
|
-
type: "start",
|
|
163
|
-
messageId
|
|
164
|
-
});
|
|
165
|
-
yield formatDataStream({
|
|
166
|
-
type: "message-metadata",
|
|
167
|
-
messageMetadata: {
|
|
168
|
-
tools: systemMessage.tools,
|
|
169
|
-
model: systemMessage.model,
|
|
170
|
-
sessionId: systemMessage.session_id
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
const chunks = convertSDKMessageToAISDKUI(message);
|
|
175
|
-
for (const chunk of chunks) {
|
|
176
|
-
yield chunk;
|
|
412
|
+
if (message.type === "result") {
|
|
413
|
+
const resultMsg = message;
|
|
414
|
+
if (resultMsg.subtype === "success") {
|
|
415
|
+
resultText = resultMsg.result || "";
|
|
416
|
+
}
|
|
177
417
|
}
|
|
178
418
|
}
|
|
179
|
-
|
|
180
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
181
|
-
console.error("[ClaudeRunner] Error:", errorMessage);
|
|
182
|
-
yield formatDataStream({ type: "error", errorText: errorMessage });
|
|
183
|
-
yield formatDataStream({
|
|
184
|
-
type: "finish",
|
|
185
|
-
finishReason: "error",
|
|
186
|
-
usage: {
|
|
187
|
-
promptTokens: usage.inputTokens,
|
|
188
|
-
completionTokens: usage.outputTokens
|
|
189
|
-
}
|
|
190
|
-
});
|
|
419
|
+
yield resultText;
|
|
191
420
|
} finally {
|
|
192
421
|
if (signal) {
|
|
193
422
|
signal.removeEventListener("abort", abortHandler);
|
|
194
423
|
}
|
|
195
|
-
yield `data: [DONE]
|
|
196
|
-
|
|
197
|
-
`;
|
|
198
424
|
}
|
|
199
425
|
}
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
errorText: `${assistantMsg.error}: ${errorDetail}`
|
|
210
|
-
}));
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
if (assistantMsg.message) {
|
|
214
|
-
if (typeof assistantMsg.message === "string") {
|
|
215
|
-
const textId = generateId();
|
|
216
|
-
chunks.push(formatDataStream({ type: "text-start", id: textId }));
|
|
217
|
-
chunks.push(formatDataStream({
|
|
218
|
-
type: "text-delta",
|
|
219
|
-
id: textId,
|
|
220
|
-
delta: assistantMsg.message
|
|
221
|
-
}));
|
|
222
|
-
chunks.push(formatDataStream({ type: "text-end", id: textId }));
|
|
223
|
-
} else if (assistantMsg.message.content && Array.isArray(assistantMsg.message.content)) {
|
|
224
|
-
for (const block of assistantMsg.message.content) {
|
|
225
|
-
if (block.type === "text" && block.text) {
|
|
226
|
-
const textId = generateId();
|
|
227
|
-
chunks.push(formatDataStream({ type: "text-start", id: textId }));
|
|
228
|
-
chunks.push(formatDataStream({
|
|
229
|
-
type: "text-delta",
|
|
230
|
-
id: textId,
|
|
231
|
-
delta: block.text
|
|
232
|
-
}));
|
|
233
|
-
chunks.push(formatDataStream({ type: "text-end", id: textId }));
|
|
234
|
-
} else if (block.type === "tool_use") {
|
|
235
|
-
const toolCallId = block.id || generateId();
|
|
236
|
-
chunks.push(formatDataStream({
|
|
237
|
-
type: "tool-input-start",
|
|
238
|
-
toolCallId,
|
|
239
|
-
toolName: block.name,
|
|
240
|
-
dynamic: true
|
|
241
|
-
}));
|
|
242
|
-
if (block.input) {
|
|
243
|
-
chunks.push(formatDataStream({
|
|
244
|
-
type: "tool-input-available",
|
|
245
|
-
toolCallId,
|
|
246
|
-
toolName: block.name,
|
|
247
|
-
dynamic: true,
|
|
248
|
-
input: block.input
|
|
249
|
-
}));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
426
|
+
async function* runWithJSONOutput(sdk, options, userInput, signal) {
|
|
427
|
+
const sdkOptions = createSDKOptions(options);
|
|
428
|
+
const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
|
|
429
|
+
const abortHandler = setupAbortHandler(queryIterator, signal);
|
|
430
|
+
try {
|
|
431
|
+
let resultMessage = null;
|
|
432
|
+
for await (const message of queryIterator) {
|
|
433
|
+
if (message.type === "result") {
|
|
434
|
+
resultMessage = message;
|
|
254
435
|
}
|
|
255
|
-
break;
|
|
256
436
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
chunks.push(formatDataStream({
|
|
260
|
-
type: "finish",
|
|
261
|
-
finishReason: resultMsg.is_error ? "error" : "stop",
|
|
262
|
-
messageMetadata: {
|
|
263
|
-
usage: resultMsg.usage,
|
|
264
|
-
duration_ms: resultMsg.duration_ms,
|
|
265
|
-
num_turns: resultMsg.num_turns,
|
|
266
|
-
total_cost_usd: resultMsg.total_cost_usd
|
|
267
|
-
}
|
|
268
|
-
}));
|
|
269
|
-
break;
|
|
437
|
+
if (resultMessage) {
|
|
438
|
+
yield JSON.stringify(resultMessage) + "\n";
|
|
270
439
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
const contentArray = usrMsg.message.content;
|
|
277
|
-
if (usrMsg.tool_use_result || Array.isArray(contentArray) && contentArray.some((c) => c.type === "tool_result")) {
|
|
278
|
-
for (const tool of contentArray) {
|
|
279
|
-
if (tool.is_error) {
|
|
280
|
-
chunks.push(formatDataStream({
|
|
281
|
-
type: "tool-output-error",
|
|
282
|
-
toolCallId: tool.tool_use_id,
|
|
283
|
-
errorText: tool.content,
|
|
284
|
-
dynamic: true
|
|
285
|
-
}));
|
|
286
|
-
} else {
|
|
287
|
-
chunks.push(formatDataStream({
|
|
288
|
-
type: "tool-output-available",
|
|
289
|
-
toolCallId: tool.tool_use_id,
|
|
290
|
-
output: usrMsg.tool_use_result || tool.content,
|
|
291
|
-
dynamic: true
|
|
292
|
-
}));
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
break;
|
|
440
|
+
} finally {
|
|
441
|
+
if (signal) {
|
|
442
|
+
signal.removeEventListener("abort", abortHandler);
|
|
297
443
|
}
|
|
298
|
-
default:
|
|
299
|
-
break;
|
|
300
444
|
}
|
|
301
|
-
return chunks;
|
|
302
445
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
446
|
+
async function* runWithStreamJSONOutput(sdk, options, userInput, signal) {
|
|
447
|
+
const sdkOptions = createSDKOptions(options);
|
|
448
|
+
const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
|
|
449
|
+
const abortHandler = setupAbortHandler(queryIterator, signal);
|
|
450
|
+
try {
|
|
451
|
+
for await (const message of queryIterator) {
|
|
452
|
+
yield JSON.stringify(message) + "\n";
|
|
453
|
+
}
|
|
454
|
+
} finally {
|
|
455
|
+
if (signal) {
|
|
456
|
+
signal.removeEventListener("abort", abortHandler);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
307
459
|
}
|
|
308
|
-
function
|
|
309
|
-
|
|
460
|
+
async function* runWithAISDKUIOutput(sdk, options, userInput) {
|
|
461
|
+
const sdkOptions = createSDKOptions({
|
|
462
|
+
...options,
|
|
463
|
+
includePartialMessages: true
|
|
464
|
+
});
|
|
465
|
+
const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
|
|
466
|
+
setupAbortHandler(queryIterator, options.abortController?.signal);
|
|
467
|
+
yield* streamSDKMessagesToAISDKUI(queryIterator);
|
|
310
468
|
}
|
|
311
469
|
async function* runMockAgent(options, userInput, signal) {
|
|
312
470
|
if (signal?.aborted) {
|
|
@@ -314,6 +472,11 @@ async function* runMockAgent(options, userInput, signal) {
|
|
|
314
472
|
return;
|
|
315
473
|
}
|
|
316
474
|
try {
|
|
475
|
+
const messageId = generateId();
|
|
476
|
+
yield formatDataStream({
|
|
477
|
+
type: "start",
|
|
478
|
+
messageId
|
|
479
|
+
});
|
|
317
480
|
const response = `I received your request: "${userInput}"
|
|
318
481
|
|
|
319
482
|
Model: ${options.model}
|
|
@@ -341,7 +504,13 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
|
|
|
341
504
|
yield formatDataStream({ type: "text-end", id: textId });
|
|
342
505
|
yield formatDataStream({
|
|
343
506
|
type: "finish",
|
|
344
|
-
finishReason: "
|
|
507
|
+
finishReason: mapFinishReason("success"),
|
|
508
|
+
usage: convertUsageToAISDK({
|
|
509
|
+
input_tokens: 0,
|
|
510
|
+
output_tokens: 0,
|
|
511
|
+
cache_creation_input_tokens: 0,
|
|
512
|
+
cache_read_input_tokens: 0
|
|
513
|
+
})
|
|
345
514
|
});
|
|
346
515
|
yield `data: [DONE]
|
|
347
516
|
|
|
@@ -352,7 +521,13 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
|
|
|
352
521
|
yield formatDataStream({ type: "error", errorText: errorMessage });
|
|
353
522
|
yield formatDataStream({
|
|
354
523
|
type: "finish",
|
|
355
|
-
finishReason: "
|
|
524
|
+
finishReason: mapFinishReason("error_during_execution", true),
|
|
525
|
+
usage: convertUsageToAISDK({
|
|
526
|
+
input_tokens: 0,
|
|
527
|
+
output_tokens: 0,
|
|
528
|
+
cache_creation_input_tokens: 0,
|
|
529
|
+
cache_read_input_tokens: 0
|
|
530
|
+
})
|
|
356
531
|
});
|
|
357
532
|
yield `data: [DONE]
|
|
358
533
|
|
|
@@ -363,28 +538,47 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
|
|
|
363
538
|
// src/runner.ts
|
|
364
539
|
async function runAgent(options) {
|
|
365
540
|
const abortController = new AbortController();
|
|
366
|
-
const signalHandler = () => {
|
|
541
|
+
const signalHandler = async () => {
|
|
367
542
|
console.error("[Runner] Received termination signal, stopping...");
|
|
368
543
|
abortController.abort();
|
|
369
|
-
console.error(
|
|
544
|
+
console.error(
|
|
545
|
+
"[Runner] AbortController.abort() completed (listeners triggered)"
|
|
546
|
+
);
|
|
370
547
|
};
|
|
371
548
|
process.on("SIGTERM", signalHandler);
|
|
372
549
|
process.on("SIGINT", signalHandler);
|
|
373
550
|
console.error("[Runner] Signal handlers registered");
|
|
374
551
|
try {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
552
|
+
let runner;
|
|
553
|
+
switch (options.runner) {
|
|
554
|
+
case "claude": {
|
|
555
|
+
const runnerOptions = {
|
|
556
|
+
model: options.model,
|
|
557
|
+
systemPrompt: options.systemPrompt,
|
|
558
|
+
maxTurns: options.maxTurns,
|
|
559
|
+
allowedTools: options.allowedTools,
|
|
560
|
+
resume: options.resume,
|
|
561
|
+
approvalDir: options.approvalDir,
|
|
562
|
+
outputFormat: options.outputFormat,
|
|
563
|
+
abortController
|
|
564
|
+
};
|
|
565
|
+
runner = createClaudeRunner(runnerOptions);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case "codex":
|
|
569
|
+
throw new Error(
|
|
570
|
+
"Codex runner not yet implemented. Use --runner=claude for now."
|
|
571
|
+
);
|
|
572
|
+
case "copilot":
|
|
573
|
+
throw new Error(
|
|
574
|
+
"Copilot runner not yet implemented. Use --runner=claude for now."
|
|
575
|
+
);
|
|
576
|
+
default:
|
|
577
|
+
throw new Error(
|
|
578
|
+
`Unknown runner: ${options.runner}. Supported runners: claude, codex, copilot`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
for await (const chunk of runner.run(options.userInput)) {
|
|
388
582
|
process.stdout.write(chunk);
|
|
389
583
|
}
|
|
390
584
|
} finally {
|
|
@@ -397,6 +591,11 @@ async function runAgent(options) {
|
|
|
397
591
|
function parseCliArgs() {
|
|
398
592
|
const { values, positionals } = parseArgs({
|
|
399
593
|
options: {
|
|
594
|
+
runner: {
|
|
595
|
+
type: "string",
|
|
596
|
+
short: "r",
|
|
597
|
+
default: "claude"
|
|
598
|
+
},
|
|
400
599
|
model: {
|
|
401
600
|
type: "string",
|
|
402
601
|
short: "m",
|
|
@@ -407,11 +606,6 @@ function parseCliArgs() {
|
|
|
407
606
|
short: "c",
|
|
408
607
|
default: process.env.SANDAGENT_WORKSPACE ?? process.cwd()
|
|
409
608
|
},
|
|
410
|
-
template: {
|
|
411
|
-
type: "string",
|
|
412
|
-
short: "T",
|
|
413
|
-
default: process.env.SANDAGENT_TEMPLATE ?? "default"
|
|
414
|
-
},
|
|
415
609
|
"system-prompt": {
|
|
416
610
|
type: "string",
|
|
417
611
|
short: "s"
|
|
@@ -431,6 +625,10 @@ function parseCliArgs() {
|
|
|
431
625
|
"approval-dir": {
|
|
432
626
|
type: "string"
|
|
433
627
|
},
|
|
628
|
+
"output-format": {
|
|
629
|
+
type: "string",
|
|
630
|
+
short: "o"
|
|
631
|
+
},
|
|
434
632
|
help: {
|
|
435
633
|
type: "boolean",
|
|
436
634
|
short: "h"
|
|
@@ -460,15 +658,30 @@ function parseCliArgs() {
|
|
|
460
658
|
console.error('Usage: sandagent run [options] -- "<user input>"');
|
|
461
659
|
process.exit(1);
|
|
462
660
|
}
|
|
661
|
+
const runner = values.runner;
|
|
662
|
+
if (!["claude", "codex", "copilot"].includes(runner)) {
|
|
663
|
+
console.error(
|
|
664
|
+
'Error: --runner must be one of: "claude", "codex", "copilot"'
|
|
665
|
+
);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
const outputFormat = values["output-format"];
|
|
669
|
+
if (outputFormat && !["text", "json", "stream-json", "stream"].includes(outputFormat)) {
|
|
670
|
+
console.error(
|
|
671
|
+
'Error: --output-format must be one of: "text", "json", "stream-json", "stream"'
|
|
672
|
+
);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
463
675
|
return {
|
|
676
|
+
runner,
|
|
464
677
|
model: values.model,
|
|
465
678
|
cwd: values.cwd,
|
|
466
|
-
template: values.template,
|
|
467
679
|
systemPrompt: values["system-prompt"],
|
|
468
680
|
maxTurns: values["max-turns"] ? Number.parseInt(values["max-turns"], 10) : void 0,
|
|
469
681
|
allowedTools: values["allowed-tools"]?.split(",").map((t) => t.trim()),
|
|
470
682
|
resume: values.resume,
|
|
471
683
|
approvalDir: values["approval-dir"],
|
|
684
|
+
outputFormat: outputFormat ?? "stream",
|
|
472
685
|
userInput
|
|
473
686
|
};
|
|
474
687
|
}
|
|
@@ -482,43 +695,29 @@ Streams AI SDK UI messages directly to stdout.
|
|
|
482
695
|
Usage:
|
|
483
696
|
sandagent run [options] -- "<user input>"
|
|
484
697
|
|
|
485
|
-
# Or run from a template directory:
|
|
486
|
-
cd templates/coder
|
|
487
|
-
sandagent run -- "Build a REST API"
|
|
488
|
-
|
|
489
698
|
Options:
|
|
699
|
+
-r, --runner <runner> Runner to use: claude, codex, copilot (default: claude)
|
|
490
700
|
-m, --model <model> Model to use (default: claude-sonnet-4-20250514)
|
|
491
701
|
-c, --cwd <path> Working directory (default: current directory)
|
|
492
|
-
-
|
|
493
|
-
Available: default, coder, analyst, researcher
|
|
494
|
-
-s, --system-prompt <prompt> Custom system prompt (overrides template)
|
|
702
|
+
-s, --system-prompt <prompt> Custom system prompt
|
|
495
703
|
-t, --max-turns <n> Maximum conversation turns
|
|
496
704
|
-a, --allowed-tools <tools> Comma-separated list of allowed tools
|
|
497
705
|
-r, --resume <session-id> Resume a previous session
|
|
706
|
+
-o, --output-format <format> Output format (default: stream)
|
|
707
|
+
Available: text, json(single result), stream-json(realtime streaming), stream(ai sdk ui sse format)
|
|
498
708
|
-h, --help Show this help message
|
|
499
709
|
|
|
500
710
|
Environment Variables:
|
|
501
711
|
ANTHROPIC_API_KEY Anthropic API key (required)
|
|
502
712
|
SANDAGENT_WORKSPACE Default workspace path
|
|
503
|
-
SANDAGENT_TEMPLATE Default template to use
|
|
504
713
|
SANDAGENT_LOG_LEVEL Logging level (debug, info, warn, error)
|
|
505
714
|
|
|
506
|
-
Templates:
|
|
507
|
-
default General-purpose assistant
|
|
508
|
-
coder Optimized for software development
|
|
509
|
-
analyst Optimized for data analysis
|
|
510
|
-
researcher Optimized for research tasks
|
|
511
|
-
|
|
512
715
|
Examples:
|
|
513
|
-
# Run with default
|
|
716
|
+
# Run with default settings
|
|
514
717
|
sandagent run -- "Create a hello world script"
|
|
515
718
|
|
|
516
|
-
# Run
|
|
517
|
-
|
|
518
|
-
sandagent run -- "Build a REST API with Express"
|
|
519
|
-
|
|
520
|
-
# Use a specific template
|
|
521
|
-
sandagent run --template analyst -- "Analyze sales.csv"
|
|
719
|
+
# Run with custom system prompt
|
|
720
|
+
sandagent run --system-prompt "You are a coding assistant" -- "Build a REST API with Express"
|
|
522
721
|
|
|
523
722
|
# Specify working directory
|
|
524
723
|
sandagent run --cwd ./my-project -- "Fix the bug in main.ts"
|
|
@@ -528,14 +727,15 @@ async function main() {
|
|
|
528
727
|
const args = parseCliArgs();
|
|
529
728
|
process.chdir(args.cwd);
|
|
530
729
|
await runAgent({
|
|
730
|
+
runner: args.runner,
|
|
531
731
|
model: args.model,
|
|
532
|
-
template: args.template,
|
|
533
732
|
userInput: args.userInput,
|
|
534
733
|
systemPrompt: args.systemPrompt,
|
|
535
734
|
maxTurns: args.maxTurns,
|
|
536
735
|
allowedTools: args.allowedTools,
|
|
537
736
|
resume: args.resume,
|
|
538
|
-
approvalDir: args.approvalDir
|
|
737
|
+
approvalDir: args.approvalDir,
|
|
738
|
+
outputFormat: args.outputFormat
|
|
539
739
|
});
|
|
540
740
|
}
|
|
541
741
|
main().catch((error) => {
|