@smithers-orchestrator/agents 0.16.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +65 -0
  3. package/src/AgentLike.ts +28 -0
  4. package/src/AmpAgent.js +232 -0
  5. package/src/AmpAgentOptions.ts +26 -0
  6. package/src/AnthropicAgent.js +54 -0
  7. package/src/AnthropicAgentOptions.ts +8 -0
  8. package/src/BaseCliAgent/AgentCliActionKind.ts +10 -0
  9. package/src/BaseCliAgent/AgentCliEvent.ts +44 -0
  10. package/src/BaseCliAgent/BaseCliAgent.js +874 -0
  11. package/src/BaseCliAgent/BaseCliAgentOptions.ts +13 -0
  12. package/src/BaseCliAgent/CliOutputInterpreter.ts +8 -0
  13. package/src/BaseCliAgent/CliUsageInfo.ts +7 -0
  14. package/src/BaseCliAgent/CodexConfigOverrides.ts +3 -0
  15. package/src/BaseCliAgent/PiExtensionUiRequest.ts +10 -0
  16. package/src/BaseCliAgent/PiExtensionUiResponse.ts +7 -0
  17. package/src/BaseCliAgent/RunCommandResult.ts +5 -0
  18. package/src/BaseCliAgent/buildGenerateResult.js +57 -0
  19. package/src/BaseCliAgent/combineNonEmpty.js +8 -0
  20. package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +198 -0
  21. package/src/BaseCliAgent/extractPrompt.js +88 -0
  22. package/src/BaseCliAgent/extractTextFromJsonValue.js +46 -0
  23. package/src/BaseCliAgent/index.js +32 -0
  24. package/src/BaseCliAgent/normalizeCodexConfig.js +22 -0
  25. package/src/BaseCliAgent/parseHelpers.js +111 -0
  26. package/src/BaseCliAgent/pushFlag.js +18 -0
  27. package/src/BaseCliAgent/pushList.js +10 -0
  28. package/src/BaseCliAgent/resolveTimeouts.js +24 -0
  29. package/src/BaseCliAgent/runCommandEffect.js +32 -0
  30. package/src/BaseCliAgent/runRpcCommandEffect.js +365 -0
  31. package/src/BaseCliAgent/truncateToBytes.js +13 -0
  32. package/src/BaseCliAgent/tryParseJson.js +18 -0
  33. package/src/ClaudeCodeAgent.js +455 -0
  34. package/src/ClaudeCodeAgentOptions.ts +52 -0
  35. package/src/CodexAgent.js +593 -0
  36. package/src/CodexAgentOptions.ts +23 -0
  37. package/src/ForgeAgent.js +128 -0
  38. package/src/ForgeAgentOptions.ts +14 -0
  39. package/src/GeminiAgent.js +273 -0
  40. package/src/GeminiAgentOptions.ts +20 -0
  41. package/src/KimiAgent.js +260 -0
  42. package/src/KimiAgentOptions.ts +21 -0
  43. package/src/OpenAIAgent.js +54 -0
  44. package/src/OpenAIAgentOptions.ts +8 -0
  45. package/src/PiAgent.js +468 -0
  46. package/src/PiAgentOptions.ts +40 -0
  47. package/src/SdkAgentOptions.ts +16 -0
  48. package/src/agent-contract/SmithersAgentContract.ts +10 -0
  49. package/src/agent-contract/SmithersAgentContractTool.ts +8 -0
  50. package/src/agent-contract/SmithersAgentToolCategory.ts +6 -0
  51. package/src/agent-contract/SmithersListedTool.ts +4 -0
  52. package/src/agent-contract/SmithersToolSurface.ts +1 -0
  53. package/src/agent-contract/createSmithersAgentContract.js +188 -0
  54. package/src/agent-contract/index.js +10 -0
  55. package/src/agent-contract/renderSmithersAgentPromptGuidance.js +81 -0
  56. package/src/capability-registry/AgentCapabilityRegistry.ts +22 -0
  57. package/src/capability-registry/AgentToolDescriptor.ts +4 -0
  58. package/src/capability-registry/hashCapabilityRegistry.js +43 -0
  59. package/src/capability-registry/index.js +8 -0
  60. package/src/capability-registry/normalizeCapabilityRegistry.js +52 -0
  61. package/src/capability-registry/normalizeCapabilityStringList.js +9 -0
  62. package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +6 -0
  63. package/src/cli-capabilities/CliAgentCapabilityDoctorReport.ts +18 -0
  64. package/src/cli-capabilities/CliAgentCapabilityReportEntry.ts +9 -0
  65. package/src/cli-capabilities/formatCliAgentCapabilityDoctorReport.js +24 -0
  66. package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +92 -0
  67. package/src/cli-capabilities/getCliAgentCapabilityReport.js +52 -0
  68. package/src/cli-capabilities/index.js +11 -0
  69. package/src/diagnostics/DiagnosticCheck.ts +11 -0
  70. package/src/diagnostics/DiagnosticCheckId.ts +4 -0
  71. package/src/diagnostics/DiagnosticContext.ts +4 -0
  72. package/src/diagnostics/DiagnosticReport.ts +9 -0
  73. package/src/diagnostics/enrichReportWithErrorAnalysis.js +34 -0
  74. package/src/diagnostics/formatDiagnosticSummary.js +17 -0
  75. package/src/diagnostics/getDiagnosticStrategy.js +503 -0
  76. package/src/diagnostics/index.js +13 -0
  77. package/src/diagnostics/launchDiagnostics.js +16 -0
  78. package/src/diagnostics/runDiagnostics.js +52 -0
  79. package/src/index.d.ts +872 -0
  80. package/src/index.js +39 -0
  81. package/src/resolveSdkModel.js +9 -0
  82. package/src/sanitizeForOpenAI.js +47 -0
  83. package/src/streamResultToGenerateResult.js +70 -0
  84. package/src/zodToOpenAISchema.js +16 -0
@@ -0,0 +1,24 @@
1
+
2
+ /**
3
+ * @typedef {number | { totalMs?: number; idleMs?: number; } | undefined} TimeoutInput
4
+ */
5
+ /**
6
+ * @param {TimeoutInput} timeout
7
+ * @param {{ totalMs?: number; idleMs?: number }} [fallback]
8
+ * @returns {{ totalMs?: number; idleMs?: number }}
9
+ */
10
+ export function resolveTimeouts(timeout, fallback) {
11
+ if (typeof timeout === "number") {
12
+ return { totalMs: timeout };
13
+ }
14
+ if (timeout && typeof timeout === "object") {
15
+ return {
16
+ totalMs: typeof timeout.totalMs === "number" ? timeout.totalMs : fallback?.totalMs,
17
+ idleMs: typeof timeout.idleMs === "number" ? timeout.idleMs : fallback?.idleMs,
18
+ };
19
+ }
20
+ return {
21
+ totalMs: fallback?.totalMs,
22
+ idleMs: fallback?.idleMs,
23
+ };
24
+ }
@@ -0,0 +1,32 @@
1
+ import { Effect } from "effect";
2
+ import { spawnCaptureEffect } from "@smithers-orchestrator/driver/child-process";
3
+ /**
4
+ * @typedef {{ cwd: string; env: Record<string, string>; input?: string; timeoutMs?: number; idleTimeoutMs?: number; signal?: AbortSignal; maxOutputBytes?: number; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; }} RunCommandOptions
5
+ */
6
+ /** @typedef {import("./RunCommandResult.ts").RunCommandResult} RunCommandResult */
7
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
8
+
9
+ /**
10
+ * @param {string} command
11
+ * @param {string[]} args
12
+ * @param {RunCommandOptions} options
13
+ * @returns {Effect.Effect<RunCommandResult, SmithersError>}
14
+ */
15
+ export function runCommandEffect(command, args, options) {
16
+ const { cwd, env, input, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, onStdout, onStderr, } = options;
17
+ return spawnCaptureEffect(command, args, {
18
+ cwd,
19
+ env,
20
+ input,
21
+ signal,
22
+ timeoutMs,
23
+ idleTimeoutMs,
24
+ maxOutputBytes,
25
+ onStdout,
26
+ onStderr,
27
+ }).pipe(Effect.annotateLogs({
28
+ agentCommand: command,
29
+ agentArgs: args.join(" "),
30
+ cwd,
31
+ }), Effect.withLogSpan(`agent:${command}`));
32
+ }
@@ -0,0 +1,365 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createInterface } from "node:readline";
4
+ import { Effect } from "effect";
5
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
6
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
7
+ import { logDebug, logWarning } from "@smithers-orchestrator/observability/logging";
8
+ import { toolOutputTruncatedTotal } from "@smithers-orchestrator/observability/metrics";
9
+ import { Metric } from "effect";
10
+ import { extractTextFromJsonValue } from "./extractTextFromJsonValue.js";
11
+ import { truncateToBytes } from "./truncateToBytes.js";
12
+ /** @typedef {import("./PiExtensionUiResponse.ts").PiExtensionUiResponse} PiExtensionUiResponse */
13
+
14
+ /** @typedef {import("./PiExtensionUiRequest.ts").PiExtensionUiRequest} PiExtensionUiRequest */
15
+ /**
16
+ * @typedef {{ cwd: string; env: Record<string, string>; prompt: string; timeoutMs?: number; idleTimeoutMs?: number; signal?: AbortSignal; maxOutputBytes?: number; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; onJsonEvent?: (event: Record<string, unknown>) => Promise<void> | void; onExtensionUiRequest?: (request: PiExtensionUiRequest) => Promise<PiExtensionUiResponse | null> | PiExtensionUiResponse | null; }} RunRpcCommandOptions
17
+ */
18
+
19
+ /**
20
+ * @param {number | undefined} timeoutMs
21
+ * @param {() => void} onTimeout
22
+ */
23
+ function createOneShotTimer(timeoutMs, onTimeout) {
24
+ if (!timeoutMs || !Number.isFinite(timeoutMs)) {
25
+ return { clear: () => { } };
26
+ }
27
+ const timer = setTimeout(onTimeout, timeoutMs);
28
+ return {
29
+ clear: () => clearTimeout(timer),
30
+ };
31
+ }
32
+ /**
33
+ * @param {number | undefined} timeoutMs
34
+ * @param {() => void} onTimeout
35
+ */
36
+ function createInactivityTimer(timeoutMs, onTimeout) {
37
+ let timer;
38
+ if (!timeoutMs || !Number.isFinite(timeoutMs)) {
39
+ return {
40
+ reset: () => { },
41
+ clear: () => { },
42
+ };
43
+ }
44
+ const reset = () => {
45
+ if (timer)
46
+ clearTimeout(timer);
47
+ timer = setTimeout(onTimeout, timeoutMs);
48
+ };
49
+ const clear = () => {
50
+ if (timer)
51
+ clearTimeout(timer);
52
+ timer = undefined;
53
+ };
54
+ reset();
55
+ return { reset, clear };
56
+ }
57
+ /**
58
+ * @param {string} command
59
+ * @param {string[]} args
60
+ * @param {RunRpcCommandOptions} options
61
+ * @returns {Effect.Effect<{ text: string; output: unknown; stderr: string; exitCode: number | null; usage?: any; }, SmithersError>}
62
+ */
63
+ export function runRpcCommandEffect(command, args, options) {
64
+ const { cwd, env, prompt, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, onStdout, onStderr, onJsonEvent, onExtensionUiRequest, } = options;
65
+ const span = `agent:${command}:rpc`;
66
+ const logAnnotations = {
67
+ agentCommand: command,
68
+ agentArgs: args.join(" "),
69
+ cwd,
70
+ rpc: true,
71
+ timeoutMs: timeoutMs ?? null,
72
+ idleTimeoutMs: idleTimeoutMs ?? null,
73
+ };
74
+ return Effect.async((resume) => {
75
+ let stderr = "";
76
+ let settled = false;
77
+ let exitCode = null;
78
+ let textDeltas = "";
79
+ let streamedAnyText = false;
80
+ let finalMessage = null;
81
+ let promptResponseError = null;
82
+ let extractedUsage = undefined;
83
+ let stderrTruncated = false;
84
+ logDebug("starting agent RPC command", logAnnotations, span);
85
+ const child = spawn(command, args, {
86
+ cwd,
87
+ env,
88
+ detached: true,
89
+ stdio: ["pipe", "pipe", "pipe"],
90
+ });
91
+ child.unref();
92
+ const rl = createInterface({ input: child.stdout });
93
+ /**
94
+ * @param {string} message
95
+ * @param {Record<string, unknown>} [details]
96
+ * @param {unknown} [cause]
97
+ */
98
+ const makeAgentCliError = (message, details, cause) => new SmithersError("AGENT_CLI_ERROR", message, {
99
+ agentArgs: args,
100
+ agentCommand: command,
101
+ cwd,
102
+ ...details,
103
+ }, { cause });
104
+ /**
105
+ * @param {SmithersError} err
106
+ */
107
+ const handleError = (err, message = "agent RPC command failed") => {
108
+ if (settled)
109
+ return;
110
+ settled = true;
111
+ if (signal) {
112
+ signal.removeEventListener("abort", onAbort);
113
+ }
114
+ logWarning(message, {
115
+ ...logAnnotations,
116
+ error: err.message,
117
+ }, span);
118
+ try {
119
+ rl.close();
120
+ }
121
+ catch {
122
+ // ignore
123
+ }
124
+ resume(Effect.fail(err));
125
+ };
126
+ /**
127
+ * @param {string} text
128
+ * @param {unknown} output
129
+ */
130
+ const finalize = (text, output) => {
131
+ if (settled)
132
+ return;
133
+ settled = true;
134
+ if (signal) {
135
+ signal.removeEventListener("abort", onAbort);
136
+ }
137
+ logDebug("agent RPC command completed", {
138
+ ...logAnnotations,
139
+ exitCode: child.exitCode ?? exitCode,
140
+ stderrBytes: Buffer.byteLength(stderr, "utf8"),
141
+ textBytes: Buffer.byteLength(text, "utf8"),
142
+ }, span);
143
+ try {
144
+ rl.close();
145
+ }
146
+ catch {
147
+ // ignore
148
+ }
149
+ resume(Effect.succeed({ text, output, stderr, exitCode: child.exitCode, usage: extractedUsage }));
150
+ };
151
+ /**
152
+ * @param {NodeJS.Signals} signal
153
+ */
154
+ const killProcessGroup = (signal) => {
155
+ if (!child.pid)
156
+ return;
157
+ try {
158
+ process.kill(-child.pid, signal);
159
+ }
160
+ catch {
161
+ // process group already exited
162
+ }
163
+ };
164
+ const terminateChild = () => {
165
+ if (!child.pid)
166
+ return;
167
+ killProcessGroup("SIGTERM");
168
+ const killTimer = setTimeout(() => {
169
+ killProcessGroup("SIGKILL");
170
+ }, 250);
171
+ child.once("close", () => clearTimeout(killTimer));
172
+ };
173
+ /**
174
+ * @param {string} reason
175
+ */
176
+ const kill = (reason) => {
177
+ terminateChild();
178
+ handleError(makeAgentCliError(reason), "agent RPC command interrupted");
179
+ };
180
+ const totalTimeout = createOneShotTimer(timeoutMs, () => kill(`CLI timed out after ${timeoutMs}ms`));
181
+ const inactivity = createInactivityTimer(idleTimeoutMs, () => kill(`CLI idle timed out after ${idleTimeoutMs}ms`));
182
+ function onAbort() {
183
+ kill("CLI aborted");
184
+ }
185
+ if (signal?.aborted) {
186
+ onAbort();
187
+ }
188
+ else if (signal) {
189
+ signal.addEventListener("abort", onAbort, { once: true });
190
+ if (signal.aborted) {
191
+ signal.removeEventListener("abort", onAbort);
192
+ onAbort();
193
+ }
194
+ }
195
+ /**
196
+ * @param {PiExtensionUiRequest} request
197
+ */
198
+ const maybeWriteExtensionResponse = async (request) => {
199
+ const needsResponse = ["select", "confirm", "input", "editor"].includes(request.method);
200
+ if (!needsResponse && !onExtensionUiRequest)
201
+ return;
202
+ let response = onExtensionUiRequest ? await onExtensionUiRequest(request) : null;
203
+ if (!response && needsResponse) {
204
+ response = { type: "extension_ui_response", id: request.id, cancelled: true };
205
+ }
206
+ if (!response)
207
+ return;
208
+ const normalized = { ...response, id: request.id, type: "extension_ui_response" };
209
+ if (!child.stdin) {
210
+ handleError(makeAgentCliError("Failed to send extension UI response: child stdin is not available"));
211
+ terminateChild();
212
+ return;
213
+ }
214
+ child.stdin.write(`${JSON.stringify(normalized)}\n`);
215
+ };
216
+ /**
217
+ * @param {string} line
218
+ */
219
+ const handleLine = async (line) => {
220
+ inactivity.reset();
221
+ let parsed;
222
+ try {
223
+ parsed = JSON.parse(line);
224
+ }
225
+ catch {
226
+ return;
227
+ }
228
+ if (!parsed || typeof parsed !== "object")
229
+ return;
230
+ const event = parsed;
231
+ void Promise.resolve(onJsonEvent?.(event)).catch(() => undefined);
232
+ const type = event.type;
233
+ if (type === "response" && event.command === "prompt" && event.success === false) {
234
+ const errorMessage = typeof event.error === "string" ? event.error : "PI RPC prompt failed";
235
+ promptResponseError = errorMessage;
236
+ kill(errorMessage);
237
+ return;
238
+ }
239
+ if (type === "message_update") {
240
+ const assistantEvent = event.assistantMessageEvent;
241
+ if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
242
+ textDeltas += assistantEvent.delta;
243
+ streamedAnyText = true;
244
+ onStdout?.(assistantEvent.delta);
245
+ }
246
+ }
247
+ if (type === "message_end") {
248
+ const message = event.message;
249
+ if (message?.role === "assistant") {
250
+ finalMessage = event.message;
251
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
252
+ promptResponseError = message.errorMessage || `Request ${message.stopReason}`;
253
+ }
254
+ }
255
+ }
256
+ if (event.usage) {
257
+ extractedUsage = event.usage;
258
+ }
259
+ if (type === "turn_end") {
260
+ const message = event.message;
261
+ if (message?.role === "assistant") {
262
+ finalMessage = event.message ?? finalMessage;
263
+ if (message.usage)
264
+ extractedUsage = message.usage;
265
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
266
+ promptResponseError = message.errorMessage || `Request ${message.stopReason}`;
267
+ }
268
+ const extracted = finalMessage ? extractTextFromJsonValue(finalMessage) : undefined;
269
+ const text = extracted ?? textDeltas;
270
+ if (!streamedAnyText && text) {
271
+ onStdout?.(text);
272
+ }
273
+ inactivity.clear();
274
+ totalTimeout.clear();
275
+ if (promptResponseError) {
276
+ handleError(makeAgentCliError(promptResponseError));
277
+ return;
278
+ }
279
+ finalize(text, finalMessage ?? text);
280
+ child.stdin?.end();
281
+ terminateChild();
282
+ }
283
+ }
284
+ if (type === "extension_ui_request") {
285
+ await maybeWriteExtensionResponse(event);
286
+ }
287
+ };
288
+ let lineQueue = Promise.resolve();
289
+ rl.on("line", (line) => {
290
+ lineQueue = lineQueue.then(() => handleLine(line)).catch((err) => {
291
+ handleError(err instanceof SmithersError
292
+ ? err
293
+ : toSmithersError(err, undefined, { code: "AGENT_CLI_ERROR" }));
294
+ });
295
+ });
296
+ child.stdout?.on("data", () => {
297
+ inactivity.reset();
298
+ });
299
+ child.stderr?.on("data", (chunk) => {
300
+ inactivity.reset();
301
+ const text = chunk.toString("utf8");
302
+ const nextStderr = stderr + text;
303
+ if (!stderrTruncated && maxOutputBytes && Buffer.byteLength(nextStderr, "utf8") > maxOutputBytes) {
304
+ stderrTruncated = true;
305
+ void Effect.runPromise(Metric.increment(toolOutputTruncatedTotal));
306
+ logWarning("agent RPC stderr truncated", {
307
+ ...logAnnotations,
308
+ maxOutputBytes,
309
+ }, span);
310
+ }
311
+ stderr = truncateToBytes(nextStderr, maxOutputBytes);
312
+ onStderr?.(text);
313
+ });
314
+ child.on("error", (err) => {
315
+ inactivity.clear();
316
+ totalTimeout.clear();
317
+ handleError(toSmithersError(err, undefined, {
318
+ code: "AGENT_CLI_ERROR",
319
+ details: {
320
+ agentArgs: args,
321
+ agentCommand: command,
322
+ cwd,
323
+ },
324
+ }));
325
+ });
326
+ child.on("close", (code) => {
327
+ exitCode = code ?? null;
328
+ inactivity.clear();
329
+ totalTimeout.clear();
330
+ if (settled)
331
+ return;
332
+ if (promptResponseError) {
333
+ handleError(makeAgentCliError(promptResponseError));
334
+ return;
335
+ }
336
+ if (code && code !== 0) {
337
+ handleError(makeAgentCliError(stderr.trim() || `CLI exited with code ${code}`));
338
+ return;
339
+ }
340
+ const text = finalMessage ? extractTextFromJsonValue(finalMessage) ?? textDeltas : textDeltas;
341
+ if (!streamedAnyText && text) {
342
+ onStdout?.(text);
343
+ }
344
+ finalize(text ?? "", finalMessage ?? text ?? "");
345
+ });
346
+ const promptPayload = { id: randomUUID(), type: "prompt", message: prompt };
347
+ if (!child.stdin) {
348
+ handleError(makeAgentCliError("Child process stdin is not available; cannot send prompt payload."));
349
+ return;
350
+ }
351
+ child.stdin.write(`${JSON.stringify(promptPayload)}\n`);
352
+ return Effect.sync(() => {
353
+ try {
354
+ rl.close();
355
+ }
356
+ catch {
357
+ // ignore
358
+ }
359
+ if (signal) {
360
+ signal.removeEventListener("abort", onAbort);
361
+ }
362
+ killProcessGroup("SIGKILL");
363
+ });
364
+ }).pipe(Effect.annotateLogs(logAnnotations), Effect.withLogSpan(span));
365
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @param {string} text
3
+ * @param {number} [maxBytes]
4
+ * @returns {string}
5
+ */
6
+ export function truncateToBytes(text, maxBytes) {
7
+ if (!maxBytes || maxBytes <= 0)
8
+ return text;
9
+ const buf = Buffer.from(text, "utf8");
10
+ if (buf.length <= maxBytes)
11
+ return text;
12
+ return buf.subarray(0, maxBytes).toString("utf8");
13
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @param {string} text
3
+ * @returns {unknown | undefined}
4
+ */
5
+ export function tryParseJson(text) {
6
+ const trimmed = text.trim();
7
+ if (!trimmed)
8
+ return undefined;
9
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
10
+ try {
11
+ return JSON.parse(trimmed);
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ return undefined;
18
+ }