@smithers-orchestrator/agents 0.24.0 → 0.25.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.
- package/package.json +15 -5
- package/src/AgentLike.ts +5 -0
- package/src/AmpAgent.js +15 -5
- package/src/AmpAgentOptions.ts +6 -0
- package/src/BaseCliAgent/BaseCliAgent.js +205 -11
- package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +21 -3
- package/src/BaseCliAgent/index.d.ts +467 -0
- package/src/ClaudeCodeAgent.js +6 -2
- package/src/CodexAgent.js +17 -2
- package/src/CodexAgentOptions.ts +11 -0
- package/src/GeminiAgent.js +34 -224
- package/src/GeminiAgentOptions.ts +4 -9
- package/src/OpenCodeAgent.js +2 -12
- package/src/OpenCodeAgentOptions.ts +19 -0
- package/src/PiAgent.js +63 -5
- package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +0 -1
- package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +3 -2
- package/src/cli-capabilities/getCliAgentCapabilityReport.js +0 -6
- package/src/cli-surface/cliAgentSurfaceManifest.js +1 -40
- package/src/createElevenLabsTextToSpeechTool.js +128 -0
- package/src/createElevenLabsTextToSpeechTool.ts +33 -0
- package/src/diagnostics/getDiagnosticStrategy.js +94 -23
- package/src/diagnostics/launchDiagnostics.js +7 -4
- package/src/document-parsing/DocumentParsingProvider.ts +13 -0
- package/src/document-parsing/DocumentParsingResult.ts +13 -0
- package/src/document-parsing/DocumentParsingToolset.ts +4 -0
- package/src/document-parsing/DocumentParsingToolsetOptions.ts +9 -0
- package/src/document-parsing/createDocumentParsingToolset.d.ts +9 -0
- package/src/document-parsing/createDocumentParsingToolset.js +416 -0
- package/src/http/CreateHttpToolOptions.ts +4 -0
- package/src/http/HttpToolAuth.ts +15 -0
- package/src/http/HttpToolInput.ts +11 -0
- package/src/http/HttpToolOutput.ts +7 -0
- package/src/http/createHttpTool.js +136 -0
- package/src/image-generation/ImageGenerationProvider.ts +7 -0
- package/src/image-generation/ImageGenerationRequest.ts +8 -0
- package/src/image-generation/ImageGenerationResult.ts +10 -0
- package/src/image-generation/ImageGenerationToolOptions.ts +10 -0
- package/src/image-generation/createImageGenerationTool.d.ts +18 -0
- package/src/image-generation/createImageGenerationTool.js +92 -0
- package/src/index.d.ts +490 -147
- package/src/index.js +23 -5
- package/src/streamResultToGenerateResult.js +55 -26
- package/src/transcription/createTranscriptionTool.js +182 -0
- package/src/transcription/createTranscriptionTool.ts +29 -0
- package/src/transcription/index.js +1 -0
- package/src/transcription/index.ts +6 -0
- package/src/web-search/GroundedWebSearchProvider.ts +21 -0
- package/src/web-search/GroundedWebSearchToolset.ts +6 -0
- package/src/web-search/createBraveSearchProvider.js +53 -0
- package/src/web-search/createExaSearchProvider.js +72 -0
- package/src/web-search/createGroundedWebSearchToolset.js +110 -0
- package/src/web-search/createSerperSearchProvider.js +63 -0
- package/src/web-search/createTavilySearchProvider.js +59 -0
- package/src/web-search/index.js +5 -0
- package/src/zodToOpenAISchema.js +4 -0
- package/src/OpenCodeAgent.ts +0 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "AI SDK and CLI agent adapters for Smithers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"default": "./src/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./BaseCliAgent": {
|
|
14
|
-
"types": "./src/index.d.ts",
|
|
14
|
+
"types": "./src/BaseCliAgent/index.d.ts",
|
|
15
15
|
"import": "./src/BaseCliAgent/index.js",
|
|
16
16
|
"default": "./src/BaseCliAgent/index.js"
|
|
17
17
|
},
|
|
@@ -40,6 +40,16 @@
|
|
|
40
40
|
"import": "./src/mcp/createMcpToolset.js",
|
|
41
41
|
"default": "./src/mcp/createMcpToolset.js"
|
|
42
42
|
},
|
|
43
|
+
"./document-parsing/createDocumentParsingToolset": {
|
|
44
|
+
"types": "./src/document-parsing/createDocumentParsingToolset.d.ts",
|
|
45
|
+
"import": "./src/document-parsing/createDocumentParsingToolset.js",
|
|
46
|
+
"default": "./src/document-parsing/createDocumentParsingToolset.js"
|
|
47
|
+
},
|
|
48
|
+
"./image-generation/createImageGenerationTool": {
|
|
49
|
+
"types": "./src/image-generation/createImageGenerationTool.d.ts",
|
|
50
|
+
"import": "./src/image-generation/createImageGenerationTool.js",
|
|
51
|
+
"default": "./src/image-generation/createImageGenerationTool.js"
|
|
52
|
+
},
|
|
43
53
|
"./*": {
|
|
44
54
|
"types": "./src/index.d.ts",
|
|
45
55
|
"import": "./src/*.js",
|
|
@@ -56,9 +66,9 @@
|
|
|
56
66
|
"ai": "^6.0.168",
|
|
57
67
|
"effect": "^3.21.1",
|
|
58
68
|
"zod": "^4.3.6",
|
|
59
|
-
"@smithers-orchestrator/driver": "0.
|
|
60
|
-
"@smithers-orchestrator/errors": "0.
|
|
61
|
-
"@smithers-orchestrator/observability": "0.
|
|
69
|
+
"@smithers-orchestrator/driver": "0.25.0",
|
|
70
|
+
"@smithers-orchestrator/errors": "0.25.0",
|
|
71
|
+
"@smithers-orchestrator/observability": "0.25.0"
|
|
62
72
|
},
|
|
63
73
|
"devDependencies": {
|
|
64
74
|
"@types/bun": "latest",
|
package/src/AgentLike.ts
CHANGED
|
@@ -14,6 +14,11 @@ export type AgentLike = {
|
|
|
14
14
|
capabilities?: AgentCapabilityRegistry;
|
|
15
15
|
/** True when the agent consumes outputSchema through a native structured-output API. */
|
|
16
16
|
supportsNativeStructuredOutput?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Performs deterministic startup checks before the first generation call in a
|
|
19
|
+
* workflow run. A rejected promise fails the task without retrying.
|
|
20
|
+
*/
|
|
21
|
+
preflight?: (args?: AgentGenerateOptions) => Promise<void>;
|
|
17
22
|
/**
|
|
18
23
|
* Generates a response or action based on the provided arguments.
|
|
19
24
|
*
|
package/src/AmpAgent.js
CHANGED
|
@@ -194,7 +194,15 @@ export class AmpAgent extends BaseCliAgent {
|
|
|
194
194
|
* @param {{ prompt: string; systemPrompt?: string; cwd: string; options: any; }} params
|
|
195
195
|
*/
|
|
196
196
|
async buildCommand(params) {
|
|
197
|
-
|
|
197
|
+
// Resume an existing thread when a session id is provided. Amp continues a
|
|
198
|
+
// thread with `amp threads continue <id>` (vs starting a fresh thread), so
|
|
199
|
+
// new-thread-only flags (--visibility, --archive) are skipped on resume.
|
|
200
|
+
// NB: the `threads continue` invocation follows the Amp manifest; confirm
|
|
201
|
+
// against the live `amp` CLI if its thread syntax changes.
|
|
202
|
+
const resumeSession = typeof params.options?.resumeSession === "string"
|
|
203
|
+
? params.options.resumeSession
|
|
204
|
+
: this.opts.resume;
|
|
205
|
+
const args = resumeSession ? ["threads", "continue", resumeSession] : [];
|
|
198
206
|
const yoloEnabled = this.opts.yolo ?? this.yolo;
|
|
199
207
|
// Dangerous allow all (yolo mode) — must come before --execute
|
|
200
208
|
if (this.opts.dangerouslyAllowAll || yoloEnabled) {
|
|
@@ -202,8 +210,9 @@ export class AmpAgent extends BaseCliAgent {
|
|
|
202
210
|
}
|
|
203
211
|
// Model / mode
|
|
204
212
|
pushFlag(args, "--model", this.opts.model ?? this.model);
|
|
205
|
-
// Visibility for new threads
|
|
206
|
-
|
|
213
|
+
// Visibility for new threads (not applicable when continuing one)
|
|
214
|
+
if (!resumeSession)
|
|
215
|
+
pushFlag(args, "--visibility", this.opts.visibility);
|
|
207
216
|
// MCP config
|
|
208
217
|
pushFlag(args, "--mcp-config", this.opts.mcpConfig);
|
|
209
218
|
// Settings file
|
|
@@ -217,8 +226,9 @@ export class AmpAgent extends BaseCliAgent {
|
|
|
217
226
|
args.push("--no-jetbrains");
|
|
218
227
|
// Color handling
|
|
219
228
|
args.push("--no-color");
|
|
220
|
-
// Archive thread after execution to keep things clean
|
|
221
|
-
|
|
229
|
+
// Archive thread after execution to keep things clean (new threads only)
|
|
230
|
+
if (!resumeSession)
|
|
231
|
+
args.push("--archive");
|
|
222
232
|
if (this.extraArgs?.length)
|
|
223
233
|
args.push(...this.extraArgs);
|
|
224
234
|
// Build prompt with system prompt prepended
|
package/src/AmpAgentOptions.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type { BaseCliAgentOptions } from "./BaseCliAgent/BaseCliAgentOptions";
|
|
|
4
4
|
* Configuration options for the AmpAgent.
|
|
5
5
|
*/
|
|
6
6
|
export type AmpAgentOptions = BaseCliAgentOptions & {
|
|
7
|
+
/**
|
|
8
|
+
* Thread id to continue. When set (or when a task passes
|
|
9
|
+
* `options.resumeSession`), buildCommand emits `amp threads continue <id>`
|
|
10
|
+
* instead of starting a fresh thread.
|
|
11
|
+
*/
|
|
12
|
+
resume?: string;
|
|
7
13
|
/** Visibility setting for the new thread (e.g., private, public) */
|
|
8
14
|
visibility?: "private" | "public" | "workspace" | "group";
|
|
9
15
|
/** Path to a specific MCP configuration file */
|
|
@@ -5,6 +5,68 @@ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
|
5
5
|
import { logDebug, logInfo, logWarning } from "@smithers-orchestrator/observability/logging";
|
|
6
6
|
import { agentDurationMs, agentErrorsTotal, agentInvocationsTotal, agentRetriesTotal, agentTokensTotal, } from "@smithers-orchestrator/observability/metrics";
|
|
7
7
|
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
8
|
+
|
|
9
|
+
const QUOTA_PATTERNS = [
|
|
10
|
+
/\bhit\s+your\s+usage\s+limit\b/i,
|
|
11
|
+
/\busage\s+limit\s+exceeded\b/i,
|
|
12
|
+
/\bquota\s+exceeded\b/i,
|
|
13
|
+
/\brate\s+limit\s+exceeded\b/i,
|
|
14
|
+
/\byou('ve| have)\s+reached\s+(your\s+)?(usage|rate|quota)\b/i,
|
|
15
|
+
/\b(usage|quota|rate)\s+(cap|ceiling|limit)\s+(reached|exceeded|hit)\b/i,
|
|
16
|
+
/\btoo\s+many\s+requests\b/i,
|
|
17
|
+
/\b(429|rate.limit)\b[\s\S]{0,100}?try\s+again\b/i,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detects provider quota/rate-limit errors and returns a SmithersError with
|
|
22
|
+
* AGENT_QUOTA_EXCEEDED code. The reset time is parsed when present.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} message
|
|
25
|
+
* @param {string} command
|
|
26
|
+
* @param {{ agentId?: string; agentModel?: string; agentEngine?: string; nowMs?: () => number }} [context]
|
|
27
|
+
* @returns {SmithersError | null}
|
|
28
|
+
*/
|
|
29
|
+
export function classifyQuotaError(message, command, context = {}) {
|
|
30
|
+
if (!message) return null;
|
|
31
|
+
const isQuota = QUOTA_PATTERNS.some((re) => re.test(message));
|
|
32
|
+
if (!isQuota) return null;
|
|
33
|
+
const { agentId, agentModel, agentEngine, nowMs = () => Date.now() } = context;
|
|
34
|
+
const now = nowMs();
|
|
35
|
+
let quotaResetAtMs;
|
|
36
|
+
let resetHint;
|
|
37
|
+
// Format: "try again at Jun 18th, 2026 9:54 AM" — strip ordinal suffix before parsing
|
|
38
|
+
const dateMatch = /try again at\s+([A-Z][a-z]+ \d+(?:st|nd|rd|th)?,?\s+\d{4}\s+\d+:\d+\s+(?:AM|PM))/i.exec(message);
|
|
39
|
+
if (dateMatch) {
|
|
40
|
+
const normalized = dateMatch[1].replace(/(\d+)(st|nd|rd|th)\b/gi, "$1");
|
|
41
|
+
const parsed = Date.parse(normalized);
|
|
42
|
+
if (Number.isFinite(parsed) && parsed > now) {
|
|
43
|
+
quotaResetAtMs = parsed;
|
|
44
|
+
}
|
|
45
|
+
resetHint = dateMatch[0];
|
|
46
|
+
} else {
|
|
47
|
+
// Format: "retry after N seconds"
|
|
48
|
+
const secondsMatch = /retry after\s+(\d+)\s+second/i.exec(message);
|
|
49
|
+
if (secondsMatch) {
|
|
50
|
+
const deltaMs = Number(secondsMatch[1]) * 1000;
|
|
51
|
+
if (deltaMs > 0) {
|
|
52
|
+
quotaResetAtMs = now + deltaMs;
|
|
53
|
+
}
|
|
54
|
+
resetHint = secondsMatch[0];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const modelLabel = agentModel ?? "<unset>";
|
|
58
|
+
const idLabel = agentId ?? "<anonymous>";
|
|
59
|
+
const summary = `Agent "${idLabel}" (${command}, model=${modelLabel}) hit a provider usage/quota limit: ${message.slice(0, 300)}.${resetHint ? ` ${resetHint}.` : ""} Retries are preserved; the run will pause until the quota resets.`;
|
|
60
|
+
return new SmithersError("AGENT_QUOTA_EXCEEDED", summary, {
|
|
61
|
+
failureQuota: true,
|
|
62
|
+
agentId: idLabel,
|
|
63
|
+
agentEngine: agentEngine ?? "unknown",
|
|
64
|
+
agentModel: modelLabel,
|
|
65
|
+
command,
|
|
66
|
+
underlying: message.slice(0, 500),
|
|
67
|
+
...(quotaResetAtMs != null ? { quotaResetAtMs } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
8
70
|
import { launchDiagnostics, enrichReportWithErrorAnalysis, formatDiagnosticSummary } from "../diagnostics/index.js";
|
|
9
71
|
import { extractPrompt } from "./extractPrompt.js";
|
|
10
72
|
import { resolveTimeouts } from "./resolveTimeouts.js";
|
|
@@ -299,6 +361,8 @@ function extractTextFromJsonPayload(raw) {
|
|
|
299
361
|
}
|
|
300
362
|
const chunks = [];
|
|
301
363
|
for (const parsed of parsedLines) {
|
|
364
|
+
if (typeof parsed?.role === "string" && parsed.role !== "assistant")
|
|
365
|
+
continue;
|
|
302
366
|
let text;
|
|
303
367
|
if (parsed?.type === "text" && typeof parsed?.part?.text === "string") {
|
|
304
368
|
text = parsed.part.text;
|
|
@@ -692,15 +756,8 @@ export class BaseCliAgent {
|
|
|
692
756
|
const agentId = this.id;
|
|
693
757
|
const agentModel = this.model;
|
|
694
758
|
const agentEngine = resolveAgentEngineTag(this);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
* engine surfaces them with a clear, actionable message and stops retrying
|
|
698
|
-
* (these errors are deterministic and will never recover by re-running).
|
|
699
|
-
*
|
|
700
|
-
* @param {string} message
|
|
701
|
-
* @param {string} command
|
|
702
|
-
* @returns {SmithersError | null}
|
|
703
|
-
*/
|
|
759
|
+
const agentCtx = { agentId, agentModel, agentEngine };
|
|
760
|
+
const classifyQuota = (message, command) => classifyQuotaError(message, command, agentCtx);
|
|
704
761
|
function classifyNonRetryableAgentError(message, command) {
|
|
705
762
|
if (!message)
|
|
706
763
|
return null;
|
|
@@ -862,7 +919,7 @@ export class BaseCliAgent {
|
|
|
862
919
|
}
|
|
863
920
|
flushBufferedLines(stream, false);
|
|
864
921
|
};
|
|
865
|
-
diagnosticsPromise = launchDiagnostics(commandSpec.command, commandEnv, cwd);
|
|
922
|
+
diagnosticsPromise = launchDiagnostics(commandSpec.command, commandEnv, cwd, this.diagnosticHints?.());
|
|
866
923
|
return Effect.gen(this, function* () {
|
|
867
924
|
const result = yield* runCommandEffect(commandSpec.command, commandSpec.args, {
|
|
868
925
|
cwd,
|
|
@@ -922,6 +979,10 @@ export class BaseCliAgent {
|
|
|
922
979
|
filteredStderr ||
|
|
923
980
|
result.stdout.trim() ||
|
|
924
981
|
`CLI exited with code ${result.exitCode}`;
|
|
982
|
+
const quota = classifyQuota(errorText, commandSpec.command);
|
|
983
|
+
if (quota) {
|
|
984
|
+
return yield* Effect.fail(quota);
|
|
985
|
+
}
|
|
925
986
|
const nonRetryable = classifyNonRetryableAgentError(errorText, commandSpec.command);
|
|
926
987
|
if (nonRetryable) {
|
|
927
988
|
return yield* Effect.fail(nonRetryable);
|
|
@@ -949,7 +1010,12 @@ export class BaseCliAgent {
|
|
|
949
1010
|
}
|
|
950
1011
|
}
|
|
951
1012
|
if (completedEvent?.ok === false) {
|
|
952
|
-
|
|
1013
|
+
const completedError = completedEvent.error || "CLI agent reported an error";
|
|
1014
|
+
const completedQuota = classifyQuota(completedError, commandSpec.command);
|
|
1015
|
+
if (completedQuota) {
|
|
1016
|
+
return yield* Effect.fail(completedQuota);
|
|
1017
|
+
}
|
|
1018
|
+
return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", completedError));
|
|
953
1019
|
}
|
|
954
1020
|
// Some CLIs may print extra banners to stdout. Allow individual agents
|
|
955
1021
|
// to provide patterns so this logic stays opt-in and agent-specific.
|
|
@@ -1069,6 +1135,128 @@ export class BaseCliAgent {
|
|
|
1069
1135
|
}
|
|
1070
1136
|
/**
|
|
1071
1137
|
* @param {AgentGenerateOptions} [options]
|
|
1138
|
+
* @returns {Promise<void>}
|
|
1139
|
+
*/
|
|
1140
|
+
async preflight(options) {
|
|
1141
|
+
const cwd = this.cwd ?? options?.rootDir ?? process.cwd();
|
|
1142
|
+
const env = {
|
|
1143
|
+
...process.env,
|
|
1144
|
+
...this.env,
|
|
1145
|
+
...taskContextEnv(options?.taskContext),
|
|
1146
|
+
};
|
|
1147
|
+
const { systemFromMessages } = extractPrompt(options);
|
|
1148
|
+
const combinedSystem = combineNonEmpty([
|
|
1149
|
+
this.systemPrompt,
|
|
1150
|
+
systemFromMessages,
|
|
1151
|
+
]);
|
|
1152
|
+
const agentId = this.id ?? "<anonymous>";
|
|
1153
|
+
const agentModel = this.model ?? "<unset>";
|
|
1154
|
+
let cleanup;
|
|
1155
|
+
let command = resolveAgentEngineTag(this);
|
|
1156
|
+
try {
|
|
1157
|
+
const commandSpec = await this.buildCommand({
|
|
1158
|
+
prompt: "",
|
|
1159
|
+
systemPrompt: combinedSystem,
|
|
1160
|
+
cwd,
|
|
1161
|
+
options,
|
|
1162
|
+
});
|
|
1163
|
+
cleanup = commandSpec.cleanup;
|
|
1164
|
+
command = commandSpec.command;
|
|
1165
|
+
const commandEnv = commandSpec.env
|
|
1166
|
+
? { ...env, ...commandSpec.env }
|
|
1167
|
+
: env;
|
|
1168
|
+
const report = await launchDiagnostics(commandSpec.command, commandEnv, cwd, this.diagnosticHints?.());
|
|
1169
|
+
if (!report) {
|
|
1170
|
+
logDebug("agent preflight skipped; no diagnostics strategy", {
|
|
1171
|
+
agentId,
|
|
1172
|
+
agentEngine: commandSpec.command,
|
|
1173
|
+
agentModel,
|
|
1174
|
+
cwd,
|
|
1175
|
+
}, "agent.preflight");
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const failed = report.checks.filter((check) => check.status === "fail");
|
|
1179
|
+
const errored = report.checks.filter((check) => check.status === "error");
|
|
1180
|
+
if (failed.length > 0) {
|
|
1181
|
+
const summary = formatDiagnosticSummary(report);
|
|
1182
|
+
logWarning(summary, {
|
|
1183
|
+
agentId,
|
|
1184
|
+
agentEngine: commandSpec.command,
|
|
1185
|
+
agentModel,
|
|
1186
|
+
cwd,
|
|
1187
|
+
}, "agent.preflight");
|
|
1188
|
+
throw new SmithersError("AGENT_CONFIG_INVALID", `Agent "${agentId}" (${commandSpec.command}, model=${agentModel}) failed preflight: ${summary}`, {
|
|
1189
|
+
failureRetryable: false,
|
|
1190
|
+
preflight: true,
|
|
1191
|
+
agentId,
|
|
1192
|
+
agentEngine: commandSpec.command,
|
|
1193
|
+
agentModel,
|
|
1194
|
+
command: commandSpec.command,
|
|
1195
|
+
diagnostics: report,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
if (errored.length > 0) {
|
|
1199
|
+
logWarning(`Agent preflight diagnostics had non-blocking errors: ${formatDiagnosticSummary(report)}`, {
|
|
1200
|
+
agentId,
|
|
1201
|
+
agentEngine: commandSpec.command,
|
|
1202
|
+
agentModel,
|
|
1203
|
+
cwd,
|
|
1204
|
+
}, "agent.preflight");
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
logDebug("agent preflight passed", {
|
|
1208
|
+
agentId,
|
|
1209
|
+
agentEngine: commandSpec.command,
|
|
1210
|
+
agentModel,
|
|
1211
|
+
cwd,
|
|
1212
|
+
durationMs: Math.round(report.durationMs),
|
|
1213
|
+
}, "agent.preflight");
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
catch (cause) {
|
|
1217
|
+
if (cause instanceof SmithersError && cause.details?.preflight === true) {
|
|
1218
|
+
throw cause;
|
|
1219
|
+
}
|
|
1220
|
+
const normalized = cause instanceof SmithersError
|
|
1221
|
+
? cause
|
|
1222
|
+
: toSmithersError(cause, "build agent preflight command", {
|
|
1223
|
+
code: "AGENT_CONFIG_INVALID",
|
|
1224
|
+
details: {
|
|
1225
|
+
failureRetryable: false,
|
|
1226
|
+
preflight: true,
|
|
1227
|
+
agentId,
|
|
1228
|
+
agentEngine: command,
|
|
1229
|
+
agentModel,
|
|
1230
|
+
command,
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
throw new SmithersError(normalized.code ?? "AGENT_CONFIG_INVALID", `Agent "${agentId}" (${command}, model=${agentModel}) failed preflight: ${normalized.summary ?? normalized.message}`, {
|
|
1234
|
+
...normalized.details,
|
|
1235
|
+
failureRetryable: false,
|
|
1236
|
+
preflight: true,
|
|
1237
|
+
agentId,
|
|
1238
|
+
agentEngine: normalized.details?.agentEngine ?? command,
|
|
1239
|
+
agentModel,
|
|
1240
|
+
command: normalized.details?.command ?? command,
|
|
1241
|
+
}, { cause: normalized });
|
|
1242
|
+
}
|
|
1243
|
+
finally {
|
|
1244
|
+
if (cleanup) {
|
|
1245
|
+
try {
|
|
1246
|
+
await cleanup();
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
logWarning("agent preflight cleanup failed", {
|
|
1250
|
+
agentId,
|
|
1251
|
+
agentEngine: command,
|
|
1252
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1253
|
+
}, "agent.preflight");
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* @param {AgentGenerateOptions} [options]
|
|
1072
1260
|
* @returns {Promise<GenerateTextResult<Record<string, never>, unknown>>}
|
|
1073
1261
|
*/
|
|
1074
1262
|
async generate(options) {
|
|
@@ -1088,4 +1276,10 @@ export class BaseCliAgent {
|
|
|
1088
1276
|
createOutputInterpreter() {
|
|
1089
1277
|
return undefined;
|
|
1090
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* @returns {{ provider?: string; model?: string } | undefined}
|
|
1281
|
+
*/
|
|
1282
|
+
diagnosticHints() {
|
|
1283
|
+
return undefined;
|
|
1284
|
+
}
|
|
1091
1285
|
}
|
|
@@ -24,7 +24,7 @@ function extractLastAssistantMessage(messages) {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* @param {unknown} parsed
|
|
27
|
-
* @param {{ sawDeltaSinceBoundary: boolean }} state
|
|
27
|
+
* @param {{ sawDeltaSinceBoundary: boolean; lastFinalText?: string }} state
|
|
28
28
|
* @returns {string[]}
|
|
29
29
|
*/
|
|
30
30
|
function extractCliStreamTextChunks(parsed, state) {
|
|
@@ -45,6 +45,24 @@ function extractCliStreamTextChunks(parsed, state) {
|
|
|
45
45
|
const emitFinal = (text) => {
|
|
46
46
|
if (text && !state.sawDeltaSinceBoundary) {
|
|
47
47
|
chunks.push(text);
|
|
48
|
+
state.lastFinalText = text;
|
|
49
|
+
}
|
|
50
|
+
state.sawDeltaSinceBoundary = false;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The end-of-run `result` payload echoes the final assistant message — e.g.
|
|
54
|
+
* Claude Code emits a complete `assistant` line AND a `result` line that both
|
|
55
|
+
* carry the same answer, with no deltas in between. Treat `result` as a
|
|
56
|
+
* fallback: surface it only when nothing already covered this turn and it is
|
|
57
|
+
* not a verbatim repeat of the text we just emitted. Without this the answer
|
|
58
|
+
* is streamed (and persisted as a NodeOutput) twice, so every consumer — the
|
|
59
|
+
* TUI, `smithers chat`, the gateway UI — shows it doubled.
|
|
60
|
+
* @param {string | undefined} text
|
|
61
|
+
*/
|
|
62
|
+
const emitResult = (text) => {
|
|
63
|
+
if (text && !state.sawDeltaSinceBoundary && text !== state.lastFinalText) {
|
|
64
|
+
chunks.push(text);
|
|
65
|
+
state.lastFinalText = text;
|
|
48
66
|
}
|
|
49
67
|
state.sawDeltaSinceBoundary = false;
|
|
50
68
|
};
|
|
@@ -103,7 +121,7 @@ function extractCliStreamTextChunks(parsed, state) {
|
|
|
103
121
|
emitFinal(extractTextFromJsonValue(message));
|
|
104
122
|
}
|
|
105
123
|
if (type === "result") {
|
|
106
|
-
|
|
124
|
+
emitResult(extractTextFromJsonValue(record.result ?? record.response ?? record.output ?? record));
|
|
107
125
|
}
|
|
108
126
|
if (type === "turn_end" && message?.role === "assistant") {
|
|
109
127
|
emitFinal(extractTextFromJsonValue(message));
|
|
@@ -132,7 +150,7 @@ export function createAgentStdoutTextEmitter(options) {
|
|
|
132
150
|
const { outputFormat, onText } = options;
|
|
133
151
|
let buffer = "";
|
|
134
152
|
let emittedAnyText = false;
|
|
135
|
-
const state = { sawDeltaSinceBoundary: false };
|
|
153
|
+
const state = { sawDeltaSinceBoundary: false, lastFinalText: undefined };
|
|
136
154
|
/**
|
|
137
155
|
* @param {string | undefined} text
|
|
138
156
|
*/
|