@smithers-orchestrator/agents 0.23.0 → 0.24.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/package.json +9 -4
- package/src/BaseCliAgent/BaseCliAgent.js +79 -8
- package/src/BaseCliAgent/RunCommandResult.ts +4 -0
- package/src/BaseCliAgent/runCommandEffect.js +3 -2
- package/src/CodexAgent.js +13 -2
- package/src/CodexAgentOptions.ts +11 -0
- package/src/PiAgent.js +63 -5
- package/src/diagnostics/getDiagnosticStrategy.js +82 -12
- package/src/diagnostics/launchDiagnostics.js +7 -4
- package/src/mcp/McpToolsetOptions.ts +16 -0
- package/src/mcp/createMcpToolset.d.ts +12 -0
- package/src/mcp/createMcpToolset.js +1 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"description": "AI SDK and CLI agent adapters for Smithers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"import": "./src/cli-capabilities/index.js",
|
|
36
36
|
"default": "./src/cli-capabilities/index.js"
|
|
37
37
|
},
|
|
38
|
+
"./mcp/createMcpToolset": {
|
|
39
|
+
"types": "./src/mcp/createMcpToolset.d.ts",
|
|
40
|
+
"import": "./src/mcp/createMcpToolset.js",
|
|
41
|
+
"default": "./src/mcp/createMcpToolset.js"
|
|
42
|
+
},
|
|
38
43
|
"./*": {
|
|
39
44
|
"types": "./src/index.d.ts",
|
|
40
45
|
"import": "./src/*.js",
|
|
@@ -51,9 +56,9 @@
|
|
|
51
56
|
"ai": "^6.0.168",
|
|
52
57
|
"effect": "^3.21.1",
|
|
53
58
|
"zod": "^4.3.6",
|
|
54
|
-
"@smithers-orchestrator/
|
|
55
|
-
"@smithers-orchestrator/
|
|
56
|
-
"@smithers-orchestrator/observability": "0.
|
|
59
|
+
"@smithers-orchestrator/driver": "0.24.2",
|
|
60
|
+
"@smithers-orchestrator/errors": "0.24.2",
|
|
61
|
+
"@smithers-orchestrator/observability": "0.24.2"
|
|
57
62
|
},
|
|
58
63
|
"devDependencies": {
|
|
59
64
|
"@types/bun": "latest",
|
|
@@ -451,6 +451,27 @@ function buildStreamResult(result) {
|
|
|
451
451
|
fullStream: fullStream,
|
|
452
452
|
};
|
|
453
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Fallback when truncated stdout lost the per-message usage events: the
|
|
456
|
+
* interpreter's completed event carries the harness usage summary (#277).
|
|
457
|
+
* @param {{ usage?: unknown } | null} completedEvent
|
|
458
|
+
* @returns {CliUsageInfo | undefined}
|
|
459
|
+
*/
|
|
460
|
+
function usageFromCompletedEvent(completedEvent) {
|
|
461
|
+
const u = completedEvent?.usage;
|
|
462
|
+
if (!u || typeof u !== "object" || Array.isArray(u))
|
|
463
|
+
return undefined;
|
|
464
|
+
const num = (value) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
|
|
465
|
+
const usage = {
|
|
466
|
+
inputTokens: num(u.input_tokens) ?? num(u.inputTokens),
|
|
467
|
+
outputTokens: num(u.output_tokens) ?? num(u.outputTokens),
|
|
468
|
+
cacheReadTokens: num(u.cache_read_input_tokens) ?? num(u.cacheReadTokens),
|
|
469
|
+
cacheWriteTokens: num(u.cache_creation_input_tokens) ?? num(u.cacheWriteTokens),
|
|
470
|
+
reasoningTokens: num(u.reasoning_tokens) ?? num(u.reasoningTokens),
|
|
471
|
+
totalTokens: num(u.total_tokens) ?? num(u.totalTokens),
|
|
472
|
+
};
|
|
473
|
+
return Object.values(usage).some((value) => value !== undefined) ? usage : undefined;
|
|
474
|
+
}
|
|
454
475
|
/**
|
|
455
476
|
* @param {string} raw
|
|
456
477
|
* @returns {CliUsageInfo | undefined}
|
|
@@ -841,7 +862,7 @@ export class BaseCliAgent {
|
|
|
841
862
|
}
|
|
842
863
|
flushBufferedLines(stream, false);
|
|
843
864
|
};
|
|
844
|
-
diagnosticsPromise = launchDiagnostics(commandSpec.command, commandEnv, cwd);
|
|
865
|
+
diagnosticsPromise = launchDiagnostics(commandSpec.command, commandEnv, cwd, this.diagnosticHints?.());
|
|
845
866
|
return Effect.gen(this, function* () {
|
|
846
867
|
const result = yield* runCommandEffect(commandSpec.command, commandSpec.args, {
|
|
847
868
|
cwd,
|
|
@@ -851,6 +872,10 @@ export class BaseCliAgent {
|
|
|
851
872
|
idleTimeoutMs: callTimeouts.idleMs,
|
|
852
873
|
signal: options?.abortSignal,
|
|
853
874
|
maxOutputBytes: this.maxOutputBytes ?? options?.maxOutputBytes,
|
|
875
|
+
// CLI harnesses emit their final result event at the END of
|
|
876
|
+
// the stream; if the capture cap trips, the tail is the part
|
|
877
|
+
// that must survive (#277).
|
|
878
|
+
truncateKeep: "tail",
|
|
854
879
|
onStdout: (chunk) => {
|
|
855
880
|
stdoutEmitter?.push(chunk);
|
|
856
881
|
handleInterpreterChunk("stdout", chunk);
|
|
@@ -863,12 +888,30 @@ export class BaseCliAgent {
|
|
|
863
888
|
flushBufferedLines("stdout", true);
|
|
864
889
|
flushBufferedLines("stderr", true);
|
|
865
890
|
emitEvents(interpreter?.onExit?.(result));
|
|
866
|
-
|
|
891
|
+
if (result.stdoutTruncated) {
|
|
892
|
+
emitEvents({
|
|
893
|
+
type: "action",
|
|
894
|
+
engine: commandSpec.command,
|
|
895
|
+
phase: "completed",
|
|
896
|
+
entryType: "thought",
|
|
897
|
+
action: {
|
|
898
|
+
id: `stdout-truncated-${randomUUID()}`,
|
|
899
|
+
kind: "warning",
|
|
900
|
+
title: "captured stdout truncated",
|
|
901
|
+
detail: {},
|
|
902
|
+
},
|
|
903
|
+
message: "Captured stdout exceeded maxOutputBytes; kept the stream tail. The streamed interpreter answer is used as the result text.",
|
|
904
|
+
ok: true,
|
|
905
|
+
level: "warning",
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
const outputFileText = commandSpec.outputFile
|
|
867
909
|
? yield* Effect.tryPromise({
|
|
868
910
|
try: () => fs.readFile(commandSpec.outputFile, "utf8"),
|
|
869
911
|
catch: (cause) => toSmithersError(cause, "read output file"),
|
|
870
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(
|
|
871
|
-
:
|
|
912
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(null)))
|
|
913
|
+
: null;
|
|
914
|
+
const stdout = typeof outputFileText === "string" ? outputFileText : result.stdout;
|
|
872
915
|
if (result.exitCode && result.exitCode !== 0) {
|
|
873
916
|
const filteredStderr = filterBenignStderr(result.stderr, commandSpec.benignStderrPatterns);
|
|
874
917
|
if (!(commandSpec.command === "codex" && filteredStderr.length === 0)) {
|
|
@@ -934,13 +977,35 @@ export class BaseCliAgent {
|
|
|
934
977
|
}
|
|
935
978
|
}
|
|
936
979
|
}
|
|
937
|
-
const
|
|
938
|
-
?
|
|
980
|
+
const extractedFromStdout = outputFormat === "json" || outputFormat === "stream-json"
|
|
981
|
+
? extractTextFromJsonPayload(rawText)
|
|
939
982
|
: rawText;
|
|
940
|
-
|
|
983
|
+
// The interpreter parses the live stream line-by-line BEFORE the
|
|
984
|
+
// capture cap applies, so its completed answer survives stdout
|
|
985
|
+
// truncation. Prefer it whenever the captured stdout was
|
|
986
|
+
// truncated or yields no final message; otherwise keep the
|
|
987
|
+
// historical extraction so intact runs are unchanged (#277).
|
|
988
|
+
const streamedAnswer = typeof completedEvent?.answer === "string" && completedEvent.answer.trim().length > 0
|
|
989
|
+
? completedEvent.answer
|
|
990
|
+
: undefined;
|
|
991
|
+
// A dedicated final-message file (e.g. codex --output-last-message)
|
|
992
|
+
// is the CLI's authoritative output channel: it holds the complete
|
|
993
|
+
// final message and is immune to the stdout byte cap and to
|
|
994
|
+
// line-by-line stream interpretation. When it parsed as JSON, trust
|
|
995
|
+
// it over the truncation/stream fallbacks, which otherwise surface a
|
|
996
|
+
// short `message` field instead of the full structured object.
|
|
997
|
+
const outputFileJson = typeof outputFileText === "string" && outputFileText.trim() !== ""
|
|
998
|
+
? tryParseJson(outputFileText)
|
|
999
|
+
: null;
|
|
1000
|
+
const extractedText = outputFileJson != null
|
|
1001
|
+
? outputFileText
|
|
1002
|
+
: result.stdoutTruncated || extractedFromStdout == null || extractedFromStdout.trim() === ""
|
|
1003
|
+
? (streamedAnswer ?? extractedFromStdout ?? rawText)
|
|
1004
|
+
: extractedFromStdout;
|
|
1005
|
+
const output = outputFileJson ?? tryParseJson(extractedText);
|
|
941
1006
|
// Extract token usage from raw stdout before text extraction strips it.
|
|
942
1007
|
// Each CLI harness embeds usage differently (NDJSON events, JSON stats, etc.)
|
|
943
|
-
const cliUsage = extractUsageFromOutput(stdout);
|
|
1008
|
+
const cliUsage = extractUsageFromOutput(stdout) ?? usageFromCompletedEvent(completedEvent);
|
|
944
1009
|
const usage = cliUsage ? {
|
|
945
1010
|
inputTokens: cliUsage.inputTokens,
|
|
946
1011
|
inputTokenDetails: {
|
|
@@ -1023,4 +1088,10 @@ export class BaseCliAgent {
|
|
|
1023
1088
|
createOutputInterpreter() {
|
|
1024
1089
|
return undefined;
|
|
1025
1090
|
}
|
|
1091
|
+
/**
|
|
1092
|
+
* @returns {{ provider?: string; model?: string } | undefined}
|
|
1093
|
+
*/
|
|
1094
|
+
diagnosticHints() {
|
|
1095
|
+
return undefined;
|
|
1096
|
+
}
|
|
1026
1097
|
}
|
|
@@ -2,4 +2,8 @@ export type RunCommandResult = {
|
|
|
2
2
|
stdout: string;
|
|
3
3
|
stderr: string;
|
|
4
4
|
exitCode: number | null;
|
|
5
|
+
/** True when captured stdout exceeded maxOutputBytes and was truncated. */
|
|
6
|
+
stdoutTruncated?: boolean;
|
|
7
|
+
/** True when captured stderr exceeded maxOutputBytes and was truncated. */
|
|
8
|
+
stderrTruncated?: boolean;
|
|
5
9
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { spawnCaptureEffect } from "@smithers-orchestrator/driver/child-process";
|
|
3
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
|
|
4
|
+
* @typedef {{ cwd: string; env: Record<string, string>; input?: string; timeoutMs?: number; idleTimeoutMs?: number; signal?: AbortSignal; maxOutputBytes?: number; truncateKeep?: "head" | "tail"; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; }} RunCommandOptions
|
|
5
5
|
*/
|
|
6
6
|
/** @typedef {import("./RunCommandResult.ts").RunCommandResult} RunCommandResult */
|
|
7
7
|
/** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
|
|
@@ -13,7 +13,7 @@ import { spawnCaptureEffect } from "@smithers-orchestrator/driver/child-process"
|
|
|
13
13
|
* @returns {Effect.Effect<RunCommandResult, SmithersError>}
|
|
14
14
|
*/
|
|
15
15
|
export function runCommandEffect(command, args, options) {
|
|
16
|
-
const { cwd, env, input, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, onStdout, onStderr, } = options;
|
|
16
|
+
const { cwd, env, input, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, truncateKeep, onStdout, onStderr, } = options;
|
|
17
17
|
return spawnCaptureEffect(command, args, {
|
|
18
18
|
cwd,
|
|
19
19
|
env,
|
|
@@ -22,6 +22,7 @@ export function runCommandEffect(command, args, options) {
|
|
|
22
22
|
timeoutMs,
|
|
23
23
|
idleTimeoutMs,
|
|
24
24
|
maxOutputBytes,
|
|
25
|
+
truncateKeep,
|
|
25
26
|
onStdout,
|
|
26
27
|
onStderr,
|
|
27
28
|
}).pipe(Effect.annotateLogs({
|
package/src/CodexAgent.js
CHANGED
|
@@ -59,6 +59,15 @@ export class CodexAgent extends BaseCliAgent {
|
|
|
59
59
|
super(opts);
|
|
60
60
|
this.opts = opts;
|
|
61
61
|
this.capabilities = createCodexCapabilityRegistry(opts);
|
|
62
|
+
// Native structured output (`codex exec --output-schema`) constrains the
|
|
63
|
+
// model to emit only final JSON and makes it refuse tool calls ("tool calls
|
|
64
|
+
// are constrained by a JSON response schema"), which breaks any agentic task
|
|
65
|
+
// (read/edit/run). It is therefore OPT-IN: by default Codex is treated like
|
|
66
|
+
// the other CLI engines (supportsNativeStructuredOutput=false), so the engine
|
|
67
|
+
// prompt-injects the schema and extracts JSON from the agent's final text,
|
|
68
|
+
// leaving tool use intact. Set nativeStructuredOutput:true for pure, tool-free
|
|
69
|
+
// extraction tasks that want strict schema enforcement.
|
|
70
|
+
this.supportsNativeStructuredOutput = opts.nativeStructuredOutput === true;
|
|
62
71
|
}
|
|
63
72
|
/**
|
|
64
73
|
* @returns {CliOutputInterpreter}
|
|
@@ -548,10 +557,12 @@ export class CodexAgent extends BaseCliAgent {
|
|
|
548
557
|
// turn.completed with token usage for metrics. extractUsageFromOutput
|
|
549
558
|
// in BaseCliAgent will parse these automatically.
|
|
550
559
|
args.push("--json");
|
|
551
|
-
// Auto-wire output schema from task context if not explicitly set
|
|
560
|
+
// Auto-wire output schema from task context if not explicitly set — only when
|
|
561
|
+
// native structured output is opted in. Otherwise the engine handles the schema
|
|
562
|
+
// via prompt-injection and Codex keeps full tool access (see constructor note).
|
|
552
563
|
// Skip when resuming — `codex exec resume` does not accept --output-schema.
|
|
553
564
|
let schemaCleanupFile = null;
|
|
554
|
-
if (!resumeSession && !this.opts.outputSchema && params.options?.outputSchema) {
|
|
565
|
+
if (!resumeSession && this.opts.nativeStructuredOutput === true && !this.opts.outputSchema && params.options?.outputSchema) {
|
|
555
566
|
const schema = params.options.outputSchema;
|
|
556
567
|
const { z } = await import("zod");
|
|
557
568
|
let jsonSchema = z.toJSONSchema(schema);
|
package/src/CodexAgentOptions.ts
CHANGED
|
@@ -17,6 +17,17 @@ export type CodexAgentOptions = BaseCliAgentOptions & {
|
|
|
17
17
|
skipGitRepoCheck?: boolean;
|
|
18
18
|
addDir?: string[];
|
|
19
19
|
outputSchema?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Opt in to Codex's native structured output (`codex exec --output-schema`).
|
|
22
|
+
*
|
|
23
|
+
* Defaults to `false`. Native structured output makes the model emit only the
|
|
24
|
+
* final JSON and refuse tool calls, so it BREAKS agentic tasks (read/edit/run) —
|
|
25
|
+
* Codex returns `blocked` with no changes. Left off, Smithers treats Codex like
|
|
26
|
+
* the other CLI engines: it prompt-injects the schema and extracts JSON from the
|
|
27
|
+
* agent's final message, so tool use stays intact. Enable only for pure, tool-free
|
|
28
|
+
* extraction tasks that need strict schema enforcement.
|
|
29
|
+
*/
|
|
30
|
+
nativeStructuredOutput?: boolean;
|
|
20
31
|
color?: "always" | "never" | "auto";
|
|
21
32
|
json?: boolean;
|
|
22
33
|
outputLastMessage?: string;
|
package/src/PiAgent.js
CHANGED
|
@@ -104,11 +104,14 @@ export class PiAgent extends BaseCliAgent {
|
|
|
104
104
|
: undefined;
|
|
105
105
|
const effectiveSession = resumeSession ?? this.opts.session;
|
|
106
106
|
this.issuedSessionRef = effectiveSession;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
// pi's --print (non-interactive: process prompt and exit) and --mode
|
|
108
|
+
// are independent. Apply --print to every non-RPC mode so json task
|
|
109
|
+
// executions also process one prompt and exit instead of lingering as
|
|
110
|
+
// an interactive session (#284).
|
|
111
|
+
if (params.mode !== "rpc" && this.opts.print !== false) {
|
|
112
|
+
args.push("--print");
|
|
110
113
|
}
|
|
111
|
-
|
|
114
|
+
if (params.mode !== "text") {
|
|
112
115
|
args.push("--mode", params.mode);
|
|
113
116
|
}
|
|
114
117
|
pushFlag(args, "--provider", this.opts.provider);
|
|
@@ -197,7 +200,9 @@ export class PiAgent extends BaseCliAgent {
|
|
|
197
200
|
createOutputInterpreter() {
|
|
198
201
|
let sessionId = this.issuedSessionRef;
|
|
199
202
|
let emittedStarted = false;
|
|
203
|
+
let emittedCompleted = false;
|
|
200
204
|
let finalAnswer = "";
|
|
205
|
+
let finalUsage;
|
|
201
206
|
/**
|
|
202
207
|
* @param {unknown} value
|
|
203
208
|
*/
|
|
@@ -231,6 +236,17 @@ export class PiAgent extends BaseCliAgent {
|
|
|
231
236
|
}];
|
|
232
237
|
};
|
|
233
238
|
/**
|
|
239
|
+
* @param {Record<string, unknown>} payload
|
|
240
|
+
*/
|
|
241
|
+
const captureUsage = (payload) => {
|
|
242
|
+
const candidate = (payload && typeof payload.usage === "object" && payload.usage) ||
|
|
243
|
+
(payload && typeof payload.message === "object" && payload.message && typeof payload.message.usage === "object" && payload.message.usage) ||
|
|
244
|
+
undefined;
|
|
245
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
246
|
+
finalUsage = candidate;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
234
250
|
* @param {string} line
|
|
235
251
|
* @returns {AgentCliEvent[]}
|
|
236
252
|
*/
|
|
@@ -267,6 +283,7 @@ export class PiAgent extends BaseCliAgent {
|
|
|
267
283
|
return startedEvents();
|
|
268
284
|
}
|
|
269
285
|
if (type === "message_end" || type === "turn_end") {
|
|
286
|
+
captureUsage(payload);
|
|
270
287
|
const message = payload.message;
|
|
271
288
|
if (message?.role === "assistant") {
|
|
272
289
|
const extracted = extractTextFromJsonValue(message);
|
|
@@ -276,6 +293,33 @@ export class PiAgent extends BaseCliAgent {
|
|
|
276
293
|
}
|
|
277
294
|
return startedEvents();
|
|
278
295
|
}
|
|
296
|
+
if (type === "agent_end") {
|
|
297
|
+
captureUsage(payload);
|
|
298
|
+
if (Array.isArray(payload.messages)) {
|
|
299
|
+
for (let i = payload.messages.length - 1; i >= 0; i--) {
|
|
300
|
+
const message = payload.messages[i];
|
|
301
|
+
if (message?.role === "assistant") {
|
|
302
|
+
const extracted = extractTextFromJsonValue(message);
|
|
303
|
+
if (extracted) {
|
|
304
|
+
finalAnswer = extracted;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
emittedCompleted = true;
|
|
311
|
+
return [
|
|
312
|
+
...startedEvents(),
|
|
313
|
+
{
|
|
314
|
+
type: "completed",
|
|
315
|
+
engine: this.cliEngine,
|
|
316
|
+
ok: true,
|
|
317
|
+
answer: finalAnswer || undefined,
|
|
318
|
+
usage: finalUsage,
|
|
319
|
+
resume: sessionId,
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
}
|
|
279
323
|
if (type === "tool_execution_start") {
|
|
280
324
|
const toolName = asString(payload.toolName) ?? "tool";
|
|
281
325
|
const toolId = asString(payload.toolCallId) ?? toolName;
|
|
@@ -355,6 +399,9 @@ export class PiAgent extends BaseCliAgent {
|
|
|
355
399
|
const started = !emittedStarted && sessionId
|
|
356
400
|
? startedEvents()
|
|
357
401
|
: [];
|
|
402
|
+
if (emittedCompleted) {
|
|
403
|
+
return started;
|
|
404
|
+
}
|
|
358
405
|
return [
|
|
359
406
|
...started,
|
|
360
407
|
{
|
|
@@ -362,6 +409,7 @@ export class PiAgent extends BaseCliAgent {
|
|
|
362
409
|
engine: this.cliEngine,
|
|
363
410
|
ok: !result.exitCode || result.exitCode === 0,
|
|
364
411
|
answer: finalAnswer || undefined,
|
|
412
|
+
usage: finalUsage,
|
|
365
413
|
error: result.exitCode && result.exitCode !== 0
|
|
366
414
|
? result.stderr.trim() || `PI exited with code ${result.exitCode}`
|
|
367
415
|
: undefined,
|
|
@@ -394,7 +442,7 @@ export class PiAgent extends BaseCliAgent {
|
|
|
394
442
|
const cwd = this.cwd ?? options?.rootDir ?? process.cwd();
|
|
395
443
|
const env = { ...process.env, ...this.env };
|
|
396
444
|
const args = this.buildArgs({ prompt, cwd, options, mode });
|
|
397
|
-
const diagnosticsPromise = launchDiagnostics("pi", env, cwd);
|
|
445
|
+
const diagnosticsPromise = launchDiagnostics("pi", env, cwd, this.diagnosticHints());
|
|
398
446
|
const interpreter = this.createOutputInterpreter();
|
|
399
447
|
/**
|
|
400
448
|
* @param {AgentCliEvent[] | AgentCliEvent | null | undefined} payload
|
|
@@ -465,4 +513,14 @@ export class PiAgent extends BaseCliAgent {
|
|
|
465
513
|
outputFormat: mode,
|
|
466
514
|
};
|
|
467
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* @returns {{ provider?: string; model?: string; apiKey?: string }}
|
|
518
|
+
*/
|
|
519
|
+
diagnosticHints() {
|
|
520
|
+
return {
|
|
521
|
+
provider: this.opts.provider,
|
|
522
|
+
model: this.opts.model ?? this.model,
|
|
523
|
+
apiKey: this.opts.apiKey,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
468
526
|
}
|
|
@@ -9,6 +9,9 @@ import { spawnSync } from "node:child_process";
|
|
|
9
9
|
/**
|
|
10
10
|
* @typedef {{ id: DiagnosticCheckId; run: (ctx: DiagnosticContext) => Promise<DiagnosticCheck>; }} DiagnosticCheckDef
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{ provider?: string; model?: string; apiKey?: string }} DiagnosticHints
|
|
14
|
+
*/
|
|
12
15
|
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
14
17
|
// Shared check helpers
|
|
@@ -113,7 +116,7 @@ const claudeRateLimitCheck = {
|
|
|
113
116
|
"content-type": "application/json",
|
|
114
117
|
},
|
|
115
118
|
body: JSON.stringify({
|
|
116
|
-
model: "claude-
|
|
119
|
+
model: "claude-fable-5",
|
|
117
120
|
messages: [{ role: "user", content: "hi" }],
|
|
118
121
|
}),
|
|
119
122
|
signal: AbortSignal.timeout(4_000),
|
|
@@ -472,15 +475,75 @@ const antigravityStrategy = {
|
|
|
472
475
|
// ---------------------------------------------------------------------------
|
|
473
476
|
// Pi strategy
|
|
474
477
|
// ---------------------------------------------------------------------------
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Resolve the effective pi provider family from an explicit `--provider`, a
|
|
480
|
+
* `provider/model` prefix, or a bare model id's well-known prefix. Returns ""
|
|
481
|
+
* when undeterminable so callers fall back to pi's default (google) (#284).
|
|
482
|
+
* @param {DiagnosticHints | undefined} hints
|
|
483
|
+
* @returns {string}
|
|
484
|
+
*/
|
|
485
|
+
function resolvePiProvider(hints) {
|
|
486
|
+
const explicit = (hints?.provider || "").trim().toLowerCase();
|
|
487
|
+
if (explicit) {
|
|
488
|
+
return explicit;
|
|
489
|
+
}
|
|
490
|
+
const model = typeof hints?.model === "string" ? hints.model.trim().toLowerCase() : "";
|
|
491
|
+
if (!model) {
|
|
492
|
+
return "";
|
|
493
|
+
}
|
|
494
|
+
if (model.includes("/")) {
|
|
495
|
+
return model.split("/")[0];
|
|
496
|
+
}
|
|
497
|
+
// Bare model id (no provider prefix) — infer the provider family from
|
|
498
|
+
// common id prefixes so diagnostics probe the right backend.
|
|
499
|
+
if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o3-") || model.startsWith("o4-") || model.startsWith("chatgpt")) {
|
|
500
|
+
return "openai";
|
|
501
|
+
}
|
|
502
|
+
if (model.startsWith("claude")) {
|
|
503
|
+
return "anthropic";
|
|
504
|
+
}
|
|
505
|
+
if (model.startsWith("gemini")) {
|
|
506
|
+
return "google";
|
|
507
|
+
}
|
|
508
|
+
return "";
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* @param {DiagnosticHints | undefined} hints
|
|
512
|
+
* @returns {DiagnosticCheckDef[]}
|
|
513
|
+
*/
|
|
514
|
+
function piProviderChecks(hints) {
|
|
515
|
+
const raw = resolvePiProvider(hints);
|
|
516
|
+
if (raw === "openai" || raw === "openai-codex" || raw === "azure" || raw === "azure-openai") {
|
|
517
|
+
return [...codexApiKeyAndRateLimitCheck];
|
|
518
|
+
}
|
|
519
|
+
if (raw === "anthropic" || raw === "claude") {
|
|
520
|
+
return [claudeApiKeyCheck, claudeRateLimitCheck];
|
|
521
|
+
}
|
|
522
|
+
return [googleAuthCheck, googleRateLimitCheck];
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* pi accepts credentials via the `--api-key` option instead of an environment
|
|
526
|
+
* variable. Diagnostics only see the process env, so map an explicit apiKey to
|
|
527
|
+
* the env var the selected provider's checks read — otherwise an apiKey-only pi
|
|
528
|
+
* run is misreported as "key missing" (#284). Returns undefined when there is
|
|
529
|
+
* nothing to inject.
|
|
530
|
+
* @param {string} command
|
|
531
|
+
* @param {DiagnosticHints | undefined} hints
|
|
532
|
+
* @returns {Record<string, string> | undefined}
|
|
533
|
+
*/
|
|
534
|
+
export function diagnosticApiKeyEnv(command, hints) {
|
|
535
|
+
if (command !== "pi" || !hints?.apiKey) {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
const raw = resolvePiProvider(hints);
|
|
539
|
+
if (raw === "openai" || raw === "openai-codex" || raw === "azure" || raw === "azure-openai") {
|
|
540
|
+
return { OPENAI_API_KEY: hints.apiKey };
|
|
541
|
+
}
|
|
542
|
+
if (raw === "anthropic" || raw === "claude") {
|
|
543
|
+
return { ANTHROPIC_API_KEY: hints.apiKey };
|
|
544
|
+
}
|
|
545
|
+
return { GOOGLE_API_KEY: hints.apiKey };
|
|
546
|
+
}
|
|
484
547
|
// ---------------------------------------------------------------------------
|
|
485
548
|
// Amp strategy
|
|
486
549
|
// ---------------------------------------------------------------------------
|
|
@@ -524,13 +587,20 @@ const strategies = {
|
|
|
524
587
|
antigravity: antigravityStrategy,
|
|
525
588
|
agy: antigravityStrategy,
|
|
526
589
|
gemini: geminiStrategy,
|
|
527
|
-
pi: piStrategy,
|
|
528
590
|
amp: ampStrategy,
|
|
529
591
|
};
|
|
530
592
|
/**
|
|
531
593
|
* @param {string} command
|
|
594
|
+
* @param {DiagnosticHints} [hints]
|
|
532
595
|
* @returns {AgentDiagnosticStrategy | null}
|
|
533
596
|
*/
|
|
534
|
-
export function getDiagnosticStrategy(command) {
|
|
597
|
+
export function getDiagnosticStrategy(command, hints) {
|
|
598
|
+
if (command === "pi") {
|
|
599
|
+
return {
|
|
600
|
+
agentId: "pi",
|
|
601
|
+
command: "pi",
|
|
602
|
+
checks: [checkCliInstalled("pi", "Pi"), ...piProviderChecks(hints)],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
535
605
|
return strategies[command] ?? null;
|
|
536
606
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDiagnosticStrategy } from "./getDiagnosticStrategy.js";
|
|
1
|
+
import { diagnosticApiKeyEnv, getDiagnosticStrategy } from "./getDiagnosticStrategy.js";
|
|
2
2
|
import { runDiagnostics } from "./runDiagnostics.js";
|
|
3
3
|
/** @typedef {import("./DiagnosticReport.ts").DiagnosticReport} DiagnosticReport */
|
|
4
4
|
|
|
@@ -6,11 +6,14 @@ import { runDiagnostics } from "./runDiagnostics.js";
|
|
|
6
6
|
* @param {string} command
|
|
7
7
|
* @param {Record<string, string>} env
|
|
8
8
|
* @param {string} cwd
|
|
9
|
+
* @param {{ provider?: string; model?: string; apiKey?: string }} [hints]
|
|
9
10
|
* @returns {Promise<DiagnosticReport> | null}
|
|
10
11
|
*/
|
|
11
|
-
export function launchDiagnostics(command, env, cwd) {
|
|
12
|
-
const strategy = getDiagnosticStrategy(command);
|
|
12
|
+
export function launchDiagnostics(command, env, cwd, hints) {
|
|
13
|
+
const strategy = getDiagnosticStrategy(command, hints);
|
|
13
14
|
if (!strategy)
|
|
14
15
|
return null;
|
|
15
|
-
|
|
16
|
+
const apiKeyEnv = diagnosticApiKeyEnv(command, hints);
|
|
17
|
+
const effectiveEnv = apiKeyEnv ? { ...env, ...apiKeyEnv } : env;
|
|
18
|
+
return runDiagnostics(strategy, { env: effectiveEnv, cwd }).catch(() => null);
|
|
16
19
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for shaping the generated toolset. Mirrors the curation knobs on
|
|
3
|
+
* `createOpenApiTools` so MCP and OpenAPI integrations feel the same.
|
|
4
|
+
*/
|
|
5
|
+
export type McpToolsetOptions = {
|
|
6
|
+
/** Only expose these MCP tool names. */
|
|
7
|
+
include?: string[];
|
|
8
|
+
/** Drop these MCP tool names. */
|
|
9
|
+
exclude?: string[];
|
|
10
|
+
/** Prefix applied to every tool name, e.g. `"github_"`. */
|
|
11
|
+
namePrefix?: string;
|
|
12
|
+
/** Identifies this client to the server. */
|
|
13
|
+
clientName?: string;
|
|
14
|
+
/** Client version reported to the server. */
|
|
15
|
+
clientVersion?: string;
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { McpServerConfig } from "./McpServerConfig.js";
|
|
2
|
+
import type { McpToolset } from "./McpToolset.js";
|
|
3
|
+
import type { McpToolsetOptions } from "./McpToolsetOptions.js";
|
|
4
|
+
|
|
5
|
+
export type { McpServerConfig } from "./McpServerConfig.js";
|
|
6
|
+
export type { McpToolset } from "./McpToolset.js";
|
|
7
|
+
export type { McpToolsetOptions } from "./McpToolsetOptions.js";
|
|
8
|
+
|
|
9
|
+
export declare function createMcpToolset(
|
|
10
|
+
config: McpServerConfig,
|
|
11
|
+
options?: McpToolsetOptions,
|
|
12
|
+
): Promise<McpToolset>;
|
|
@@ -4,18 +4,7 @@ import { dynamicTool, jsonSchema } from "ai";
|
|
|
4
4
|
|
|
5
5
|
/** @typedef {import("./McpServerConfig.ts").McpServerConfig} McpServerConfig */
|
|
6
6
|
/** @typedef {import("./McpToolset.ts").McpToolset} McpToolset */
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Options for shaping the generated toolset. Mirrors the curation knobs on
|
|
10
|
-
* `createOpenApiTools` so MCP and OpenAPI integrations feel the same.
|
|
11
|
-
*
|
|
12
|
-
* @typedef {object} McpToolsetOptions
|
|
13
|
-
* @property {string[]} [include] Only expose these MCP tool names.
|
|
14
|
-
* @property {string[]} [exclude] Drop these MCP tool names.
|
|
15
|
-
* @property {string} [namePrefix] Prefix applied to every tool name (e.g. `"github_"`).
|
|
16
|
-
* @property {string} [clientName] Identifies this client to the server.
|
|
17
|
-
* @property {string} [clientVersion] Client version reported to the server.
|
|
18
|
-
*/
|
|
7
|
+
/** @typedef {import("./McpToolsetOptions.ts").McpToolsetOptions} McpToolsetOptions */
|
|
19
8
|
|
|
20
9
|
/**
|
|
21
10
|
* Connect to an MCP server and expose its tools as AI SDK tools an agent can call.
|