@2kw/ai 6.2.0-dev.23 → 6.2.0-dev.26

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 (39) hide show
  1. package/README.md +2 -2
  2. package/dist/agent-config/export.d.ts +31 -0
  3. package/dist/agent-config/export.js +87 -0
  4. package/dist/agent-config/file.d.ts +20 -0
  5. package/dist/agent-config/file.js +137 -0
  6. package/dist/agent-config/plan.d.ts +33 -0
  7. package/dist/agent-config/plan.js +100 -0
  8. package/dist/agent-config/schema.d.ts +420 -0
  9. package/dist/agent-config/schema.js +222 -0
  10. package/dist/agent-config/template.d.ts +6 -0
  11. package/dist/agent-config/template.js +59 -0
  12. package/dist/agent-config/validate.d.ts +29 -0
  13. package/dist/agent-config/validate.js +123 -0
  14. package/dist/commands/agent-apply.d.ts +9 -0
  15. package/dist/commands/agent-apply.js +176 -0
  16. package/dist/commands/agent-export.d.ts +3 -0
  17. package/dist/commands/agent-export.js +100 -0
  18. package/dist/commands/agent-init.d.ts +4 -0
  19. package/dist/commands/agent-init.js +54 -0
  20. package/dist/commands/agent-run.d.ts +5 -0
  21. package/dist/commands/agent-run.js +190 -0
  22. package/dist/commands/agent-versions.js +12 -7
  23. package/dist/commands/agents.js +24 -9
  24. package/dist/commands/ai.d.ts +5 -37
  25. package/dist/commands/ai.js +29 -136
  26. package/dist/lib/agent-decide.d.ts +41 -0
  27. package/dist/lib/agent-decide.js +129 -0
  28. package/dist/lib/agent-lookup.d.ts +15 -0
  29. package/dist/lib/agent-lookup.js +71 -0
  30. package/dist/lib/agent-models.d.ts +28 -0
  31. package/dist/lib/agent-models.js +53 -0
  32. package/dist/lib/agent-run.d.ts +83 -0
  33. package/dist/lib/agent-run.js +170 -0
  34. package/dist/lib/approval-prompt.d.ts +18 -0
  35. package/dist/lib/approval-prompt.js +84 -0
  36. package/dist/lib/client.js +6 -2
  37. package/dist/lib/errors.d.ts +12 -0
  38. package/dist/lib/errors.js +46 -2
  39. package/package.json +5 -2
@@ -0,0 +1,190 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync } from "node:fs";
3
+ import { getClient, runAction } from "../lib/client.js";
4
+ import { isJsonOutput } from "../lib/config.js";
5
+ import { BackboneApiError, CliUsageError } from "../lib/errors.js";
6
+ import { withSpinner } from "../lib/output.js";
7
+ import { resolveAgent, splitAgentRef, suggestAgents } from "../lib/agent-lookup.js";
8
+ import { buildRunEnvelope, EXIT_CODES, printRunText, stripControl } from "../lib/agent-run.js";
9
+ import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, planDecisions, } from "../lib/agent-decide.js";
10
+ import { PromptAbortedError, promptApprovals, readlineAsk } from "../lib/approval-prompt.js";
11
+ import { withModelOverride } from "../lib/agent-models.js";
12
+ import { readStdinInput } from "./ai.js";
13
+ export function resolveRunInput(arg, inputFile, readStdin = readStdinInput) {
14
+ if (arg)
15
+ return arg;
16
+ if (inputFile) {
17
+ let text;
18
+ try {
19
+ text = readFileSync(inputFile, "utf-8");
20
+ }
21
+ catch (err) {
22
+ const code = err.code;
23
+ throw new CliUsageError(`${inputFile}: cannot read file (${code ?? err.message})`);
24
+ }
25
+ // A byte order mark (U+FEFF) from Windows editors is not part of the task.
26
+ if (text.charCodeAt(0) === 0xfeff)
27
+ text = text.slice(1);
28
+ if (!text.trim())
29
+ throw new CliUsageError(`Input file ${inputFile} is empty.`);
30
+ return text;
31
+ }
32
+ const piped = readStdin();
33
+ if (piped)
34
+ return piped;
35
+ throw new CliUsageError("Input is required: pass it as an argument, with --input-file, or via stdin.");
36
+ }
37
+ async function withAgentSuggestions(client, agentRef, err) {
38
+ if (!(err instanceof BackboneApiError) || err.status !== 404 || !err.message.startsWith("Agent not found")) {
39
+ return err;
40
+ }
41
+ const names = await suggestAgents(client, splitAgentRef(agentRef).name);
42
+ if (names.length === 0)
43
+ return err;
44
+ return new BackboneApiError({
45
+ status: err.status,
46
+ error: err.errorType,
47
+ message: `${err.message}. Did you mean: ${names.join(", ")}`,
48
+ timestamp: err.timestamp,
49
+ code: err.code,
50
+ });
51
+ }
52
+ /**
53
+ * Prints a run result and sets the exit code. In interactive mode an approval pause
54
+ * is decided at the terminal and continued until the run stops pausing for approval.
55
+ * Continuations stay on the run's label and model override, and the envelope's `next` names both.
56
+ */
57
+ async function finishRun(command, client, target, first, opts) {
58
+ const agentRef = `${target.name}${target.label ? `@${target.label}` : ""}${target.model ? `#${target.model}` : ""}`;
59
+ let result = first;
60
+ let env = buildRunEnvelope(result, agentRef);
61
+ if (opts.interactive && env.status === "requires_approval") {
62
+ try {
63
+ const agent = await resolveAgent(client, target.name);
64
+ while (env.status === "requires_approval" && env.responseId) {
65
+ const responseId = env.responseId;
66
+ // Show what the agent said this round before asking; only the final round's text reaches stdout.
67
+ if (env.text)
68
+ console.error(stripControl(env.text));
69
+ let answers;
70
+ // One readline per round: the continuation spinner (ora) pauses stdin when it stops,
71
+ // and only a fresh readline resumes it for the next round's questions.
72
+ const { ask, close } = readlineAsk();
73
+ try {
74
+ answers = await promptApprovals(env.pendingApprovals, ask);
75
+ }
76
+ catch (err) {
77
+ // Ctrl+C / Ctrl+D / closed stdin: nothing was decided, so the run is still paused (exit 3 below).
78
+ if (err instanceof PromptAbortedError) {
79
+ console.error("Approval prompt closed; nothing was decided.");
80
+ break;
81
+ }
82
+ throw err;
83
+ }
84
+ finally {
85
+ close();
86
+ }
87
+ const rows = await fetchPendingApprovals(client, String(agent.id), responseId);
88
+ // Same local check as `decide`: never send a continuation with no decisions.
89
+ if (rows.length === 0) {
90
+ throw new CliUsageError(`No pending approvals for ${responseId} (already decided or superseded).`);
91
+ }
92
+ const decisions = decisionsFromAnswers(rows, answers);
93
+ result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), responseId, decisions, target.label, target.model));
94
+ env = buildRunEnvelope(result, agentRef);
95
+ }
96
+ }
97
+ catch (err) {
98
+ // Whatever failed, the paused response id is what the user needs to resume by hand.
99
+ if (env.responseId) {
100
+ console.error(stripControl(`Run is still paused: response ${env.responseId}. Resume with: ${env.next}`));
101
+ }
102
+ throw err;
103
+ }
104
+ }
105
+ if (opts.raw) {
106
+ console.log(JSON.stringify(result, null, 2));
107
+ }
108
+ else if (isJsonOutput(command)) {
109
+ console.log(JSON.stringify(env, null, 2));
110
+ }
111
+ else {
112
+ printRunText(env);
113
+ }
114
+ process.exitCode = EXIT_CODES[env.status];
115
+ }
116
+ export function makeAgentRunCommand() {
117
+ return new Command("run")
118
+ .description("Run an agent on a task (exit 3 = paused for approval, 4 = needs client tool output, 5 = incomplete)")
119
+ .argument("<agent>", "Agent name or id, optionally with @label and #model")
120
+ .argument("[input]", "Task text (or use --input-file, or pipe it on stdin)")
121
+ .option("--input-file <path>", "Read the task text from a file")
122
+ .option("--conversation <id>", "Continue a conversation")
123
+ .option("--continue <responseId>", "Continue from a previous response")
124
+ .option("--model <model>", "Run this request on another model from the agent's list (sends <agent>#<model>)")
125
+ .option("--no-input", "Never prompt for approvals, even on a terminal")
126
+ .option("--raw", "Print the untouched API response")
127
+ .action(async (agentArg, input, opts, command) => {
128
+ await runAction(command, async () => {
129
+ const agentRef = withModelOverride(agentArg, opts.model);
130
+ const text = resolveRunInput(input, opts.inputFile);
131
+ const client = getClient(command);
132
+ const body = { model: `agent/${agentRef}`, input: text, stream: false };
133
+ if (opts.conversation)
134
+ body.conversation = opts.conversation;
135
+ if (opts.continue)
136
+ body.previous_response_id = opts.continue;
137
+ let data;
138
+ try {
139
+ // The generated body type does not model a bare-string input, hence the cast.
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ ({ data } = await withSpinner("Running agent...", () => client.POST("/v1/responses", { body })));
142
+ }
143
+ catch (err) {
144
+ throw await withAgentSuggestions(client, agentRef, err);
145
+ }
146
+ // The prompt reads stdin and writes to stderr, so those two decide; stdout may be redirected.
147
+ const interactive = !!process.stdin.isTTY && !!process.stderr.isTTY && !isJsonOutput(command) && opts.input !== false && !opts.raw;
148
+ const { name, label, model } = splitAgentRef(agentRef);
149
+ await finishRun(command, client, { name, label, model }, data, {
150
+ raw: !!opts.raw,
151
+ interactive,
152
+ });
153
+ });
154
+ });
155
+ }
156
+ export function makeAgentDecideCommand() {
157
+ return new Command("decide")
158
+ .description("Decide every pending approval of a paused run and continue it")
159
+ .argument("<agent>", "Agent name or id, optionally with @label and #model (use the same @label and #model the run used)")
160
+ .requiredOption("--response <responseId>", "The paused response id (responseId in the run envelope)")
161
+ .option("--approve <ids...>", "Approval ids to approve")
162
+ .option("--reject <ids...>", "Approval ids to reject")
163
+ .option("--approve-all", "Approve every pending approval of the response")
164
+ .option("--reject-all", "Reject every pending approval of the response")
165
+ .option("--reason <text>", "Reason recorded on each decision (the model sees it on rejects)")
166
+ .option("--remember", "Also approve later calls of the same tool in this conversation")
167
+ .option("--raw", "Print the untouched API response")
168
+ .action(async (agentRef, opts, command) => {
169
+ await runAction(command, async () => {
170
+ const client = getClient(command);
171
+ const { name, label, model } = splitAgentRef(agentRef);
172
+ // Dropped silently, an empty override would continue the run on the default model.
173
+ if (model === "")
174
+ throw new CliUsageError(`Empty #model in '${agentRef}': use the model the run used, or drop the '#'.`);
175
+ const agent = await resolveAgent(client, name);
176
+ const pending = await fetchPendingApprovals(client, String(agent.id), opts.response);
177
+ const decisions = planDecisions(pending, {
178
+ approve: opts.approve,
179
+ reject: opts.reject,
180
+ approveAll: opts.approveAll,
181
+ rejectAll: opts.rejectAll,
182
+ reason: opts.reason,
183
+ remember: opts.remember,
184
+ }, opts.response);
185
+ const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model));
186
+ await finishRun(command, client, { name, label, model }, result, { raw: !!opts.raw, interactive: false });
187
+ });
188
+ });
189
+ }
190
+ //# sourceMappingURL=agent-run.js.map
@@ -2,6 +2,8 @@ import { Command } from "commander";
2
2
  import { getClient, runAction } from "../lib/client.js";
3
3
  import { formatPage, formatDetail, formatSuccess } from "../lib/output.js";
4
4
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
5
+ import { maskSecrets } from "../agent-config/file.js";
6
+ import { collectModel, MODEL_HELP, MODELS_HELP, modelFieldsFromOptions } from "../lib/agent-models.js";
5
7
  export function makeAgentVersionsCommand() {
6
8
  const cmd = new Command("versions").description("Manage agent versions");
7
9
  const list = new Command("list").description("List versions of an agent");
@@ -16,7 +18,8 @@ export function makeAgentVersionsCommand() {
16
18
  query: paginationParams(opts),
17
19
  },
18
20
  });
19
- formatPage(data, command, ["id", "versionNumber", "model", "changeDescription", "createdAt"]);
21
+ // Version reads return webhook secrets in plaintext (#669); only `agents export --include-secrets` prints them.
22
+ formatPage(maskSecrets(data, []), command, ["id", "versionNumber", "models", "changeDescription", "createdAt"]);
20
23
  });
21
24
  });
22
25
  cmd.addCommand(list);
@@ -29,7 +32,7 @@ export function makeAgentVersionsCommand() {
29
32
  await runAction(command, async () => {
30
33
  const client = getClient(command);
31
34
  const { data } = await client.GET("/v1/agents/{agentId}/versions/{versionId}", { params: { path: { agentId: opts.agent, versionId } } });
32
- formatDetail(data, command);
35
+ formatDetail(maskSecrets(data, []), command);
33
36
  });
34
37
  });
35
38
  cmd
@@ -40,14 +43,15 @@ export function makeAgentVersionsCommand() {
40
43
  await runAction(command, async () => {
41
44
  const client = getClient(command);
42
45
  const { data } = await client.GET("/v1/agents/{agentId}/versions/latest", { params: { path: { agentId: opts.agent } } });
43
- formatDetail(data, command);
46
+ formatDetail(maskSecrets(data, []), command);
44
47
  });
45
48
  });
46
49
  cmd
47
50
  .command("create")
48
51
  .description("Create a new agent version")
49
52
  .requiredOption("--agent <id>", "Agent ID")
50
- .requiredOption("-m, --model <model>", "Model (provider/model)")
53
+ .option("-m, --model <model>", MODEL_HELP, collectModel)
54
+ .option("--models <list>", MODELS_HELP)
51
55
  .option("-i, --instructions <text>", "System instructions")
52
56
  .option("--options <json>", "Model options as JSON")
53
57
  .option("--tools <json>", "Tool definitions as JSON")
@@ -56,11 +60,12 @@ export function makeAgentVersionsCommand() {
56
60
  .option("-c, --change-description <text>", "Change description")
57
61
  .action(async (opts, command) => {
58
62
  await runAction(command, async () => {
63
+ const models = modelFieldsFromOptions(opts, true);
59
64
  const client = getClient(command);
60
65
  const { data } = await client.POST("/v1/agents/{agentId}/versions", {
61
66
  params: { path: { agentId: opts.agent } },
62
67
  body: {
63
- model: opts.model,
68
+ ...models,
64
69
  instructions: opts.instructions,
65
70
  changeDescription: opts.changeDescription,
66
71
  options: opts.options ? JSON.parse(opts.options) : undefined,
@@ -69,7 +74,7 @@ export function makeAgentVersionsCommand() {
69
74
  hitlPolicy: opts.hitlPolicy ? JSON.parse(opts.hitlPolicy) : undefined,
70
75
  },
71
76
  });
72
- formatDetail(data, command);
77
+ formatDetail(maskSecrets(data, []), command);
73
78
  });
74
79
  });
75
80
  cmd
@@ -81,7 +86,7 @@ export function makeAgentVersionsCommand() {
81
86
  await runAction(command, async () => {
82
87
  const client = getClient(command);
83
88
  const { data } = await client.PUT("/v1/agents/{agentId}/versions/{versionId}/activate", { params: { path: { agentId: opts.agent, versionId } } });
84
- formatDetail(data, command);
89
+ formatDetail(maskSecrets(data, []), command);
85
90
  });
86
91
  });
87
92
  cmd
@@ -4,6 +4,12 @@ import { formatPage, formatDetail, formatSuccess } from "../lib/output.js";
4
4
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
5
5
  import { makeAgentVersionsCommand } from "./agent-versions.js";
6
6
  import { makeAgentLabelsCommand } from "./agent-labels.js";
7
+ import { makeAgentDecideCommand, makeAgentRunCommand } from "./agent-run.js";
8
+ import { makeAgentApplyCommand } from "./agent-apply.js";
9
+ import { makeAgentExportCommand } from "./agent-export.js";
10
+ import { makeAgentInitCommand, makeAgentSchemaCommand } from "./agent-init.js";
11
+ import { maskSecrets } from "../agent-config/file.js";
12
+ import { collectModel, MODEL_HELP, MODELS_HELP, modelFieldsFromOptions } from "../lib/agent-models.js";
7
13
  export function makeAgentsCommand() {
8
14
  const cmd = new Command("agents").description("Manage agents");
9
15
  const list = new Command("list").description("List agents");
@@ -15,7 +21,8 @@ export function makeAgentsCommand() {
15
21
  const { data } = await client.GET("/v1/agents", {
16
22
  params: { query: { search: opts.search, ...paginationParams(opts) } },
17
23
  });
18
- formatPage(data, command, ["id", "name", "model", "latestVersionId", "createdAt"]);
24
+ // Agent reads return webhook secrets in plaintext (#669); only `export --include-secrets` prints them.
25
+ formatPage(maskSecrets(data, []), command, ["id", "name", "models", "latestVersionId", "createdAt"]);
19
26
  });
20
27
  });
21
28
  cmd.addCommand(list);
@@ -29,14 +36,15 @@ export function makeAgentsCommand() {
29
36
  const { data } = await client.GET("/v1/agents/{id}", {
30
37
  params: { path: { id } },
31
38
  });
32
- formatDetail(data, command);
39
+ formatDetail(maskSecrets(data, []), command);
33
40
  });
34
41
  });
35
42
  cmd
36
43
  .command("create")
37
44
  .description("Create a new agent")
38
45
  .requiredOption("-n, --name <name>", "Agent name")
39
- .requiredOption("-m, --model <model>", "Model (provider/model)")
46
+ .option("-m, --model <model>", MODEL_HELP, collectModel)
47
+ .option("--models <list>", MODELS_HELP)
40
48
  .option("-d, --description <text>", "Agent description")
41
49
  .option("-i, --instructions <text>", "System instructions")
42
50
  .option("--options <json>", "Model options as JSON")
@@ -45,11 +53,12 @@ export function makeAgentsCommand() {
45
53
  .option("--hitl-policy <json>", "Human-in-the-loop policy as JSON")
46
54
  .action(async (opts, command) => {
47
55
  await runAction(command, async () => {
56
+ const models = modelFieldsFromOptions(opts, true);
48
57
  const client = getClient(command);
49
58
  const { data } = await client.POST("/v1/agents", {
50
59
  body: {
51
60
  name: opts.name,
52
- model: opts.model,
61
+ ...models,
53
62
  description: opts.description,
54
63
  instructions: opts.instructions,
55
64
  options: opts.options ? JSON.parse(opts.options) : undefined,
@@ -58,7 +67,7 @@ export function makeAgentsCommand() {
58
67
  hitlPolicy: opts.hitlPolicy ? JSON.parse(opts.hitlPolicy) : undefined,
59
68
  },
60
69
  });
61
- formatDetail(data, command);
70
+ formatDetail(maskSecrets(data, []), command);
62
71
  });
63
72
  });
64
73
  cmd
@@ -66,7 +75,8 @@ export function makeAgentsCommand() {
66
75
  .description("Update an agent's metadata and configuration")
67
76
  .argument("<id>", "Agent ID")
68
77
  .option("-n, --name <name>", "New name")
69
- .option("-m, --model <model>", "New model (provider/model)")
78
+ .option("-m, --model <model>", `${MODEL_HELP}; replaces the stored list`, collectModel)
79
+ .option("--models <list>", `${MODELS_HELP}; replaces the stored list`)
70
80
  .option("-d, --description <text>", "New description")
71
81
  .option("-i, --instructions <text>", "New system instructions")
72
82
  .option("--options <json>", "New model options as JSON")
@@ -79,8 +89,7 @@ export function makeAgentsCommand() {
79
89
  const body = {};
80
90
  if (opts.name)
81
91
  body.name = opts.name;
82
- if (opts.model)
83
- body.model = opts.model;
92
+ Object.assign(body, modelFieldsFromOptions(opts, false));
84
93
  if (opts.description !== undefined)
85
94
  body.description = opts.description;
86
95
  if (opts.instructions !== undefined)
@@ -97,7 +106,7 @@ export function makeAgentsCommand() {
97
106
  params: { path: { id } },
98
107
  body: body,
99
108
  });
100
- formatDetail(data, command);
109
+ formatDetail(maskSecrets(data, []), command);
101
110
  });
102
111
  });
103
112
  cmd
@@ -147,6 +156,12 @@ export function makeAgentsCommand() {
147
156
  });
148
157
  });
149
158
  cmd.addCommand(toolCatalogs);
159
+ cmd.addCommand(makeAgentRunCommand());
160
+ cmd.addCommand(makeAgentDecideCommand());
161
+ cmd.addCommand(makeAgentApplyCommand());
162
+ cmd.addCommand(makeAgentExportCommand());
163
+ cmd.addCommand(makeAgentInitCommand());
164
+ cmd.addCommand(makeAgentSchemaCommand());
150
165
  cmd.addCommand(makeAgentVersionsCommand());
151
166
  cmd.addCommand(makeAgentLabelsCommand());
152
167
  return cmd;
@@ -1,4 +1,6 @@
1
1
  import { Command } from "commander";
2
+ import { extractResponseText } from "../lib/agent-run.js";
3
+ export { extractResponseText };
2
4
  /**
3
5
  * Reads a single text input from stdin, or `undefined` when stdin is a TTY
4
6
  * (nothing was piped in) or empty. Mirrors the `-` stdin convention used by
@@ -11,47 +13,13 @@ export declare function readStdinInput(): string | undefined;
11
13
  * `--agent` / `--model` options, per {@code ModelReferenceResolver}'s
12
14
  * grammar: a stored agent is addressed as `agent/{ref}` (an `--agent`
13
15
  * value of `ref@label` naturally composes into `agent/ref@label`), a
14
- * direct gateway call is a plain `provider/model` string.
16
+ * direct gateway call is a plain `provider/model` string. With both, `--model`
17
+ * switches the agent to another entry of its `models` list for this request:
18
+ * `agent/{ref}#{model}` (#591, #626).
15
19
  */
16
20
  export declare function resolveModel(opts: {
17
21
  agent?: string;
18
22
  model?: string;
19
23
  }): string;
20
- interface ResponseContentPart {
21
- type?: string;
22
- text?: string;
23
- }
24
- interface ResponseOutputItem {
25
- type?: string;
26
- role?: string;
27
- content?: ResponseContentPart[];
28
- }
29
- interface ResponseUsage {
30
- input_tokens?: number;
31
- output_tokens?: number;
32
- total_tokens?: number;
33
- }
34
- interface ResponseResult {
35
- id?: string;
36
- status?: string;
37
- model?: string;
38
- output?: ResponseOutputItem[];
39
- usage?: ResponseUsage;
40
- conversation?: {
41
- id?: string;
42
- };
43
- }
44
- /**
45
- * Extracts the assistant's text from a Responses API result: the
46
- * `output_text` content parts of every `message` output item, in order.
47
- */
48
- export declare function extractResponseText(result: ResponseResult | undefined): string;
49
- /**
50
- * Extracts the text delta from one line of an SSE stream, or `undefined`
51
- * when the line carries nothing to print (not a `data:` line, `[DONE]`, an
52
- * event with no textual delta, or unparsable JSON).
53
- */
54
- export declare function extractSseDelta(line: string): string | undefined;
55
24
  export declare function makeAiCommand(): Command;
56
- export {};
57
25
  //# sourceMappingURL=ai.d.ts.map
@@ -1,10 +1,13 @@
1
1
  import { Command } from "commander";
2
2
  import { readFileSync } from "node:fs";
3
- import { getClient, resolveAuthHeader, runAction } from "../lib/client.js";
4
- import { resolveConfig, isJsonOutput } from "../lib/config.js";
5
- import { BackboneApiError } from "../lib/errors.js";
3
+ import { getClient, runAction } from "../lib/client.js";
4
+ import { isJsonOutput } from "../lib/config.js";
5
+ import { CliUsageError } from "../lib/errors.js";
6
+ import { withModelOverride } from "../lib/agent-models.js";
6
7
  import { formatList, withSpinner } from "../lib/output.js";
8
+ import { buildRunEnvelope, extractResponseText, printRunText, } from "../lib/agent-run.js";
7
9
  import chalk from "chalk";
10
+ export { extractResponseText };
8
11
  /**
9
12
  * Reads a single text input from stdin, or `undefined` when stdin is a TTY
10
13
  * (nothing was piped in) or empty. Mirrors the `-` stdin convention used by
@@ -27,134 +30,26 @@ export function readStdinInput() {
27
30
  * `--agent` / `--model` options, per {@code ModelReferenceResolver}'s
28
31
  * grammar: a stored agent is addressed as `agent/{ref}` (an `--agent`
29
32
  * value of `ref@label` naturally composes into `agent/ref@label`), a
30
- * direct gateway call is a plain `provider/model` string.
33
+ * direct gateway call is a plain `provider/model` string. With both, `--model`
34
+ * switches the agent to another entry of its `models` list for this request:
35
+ * `agent/{ref}#{model}` (#591, #626).
31
36
  */
32
37
  export function resolveModel(opts) {
33
- if (opts.agent && opts.model) {
34
- throw new Error("Use either --agent or --model, not both.");
35
- }
36
38
  if (opts.agent)
37
- return `agent/${opts.agent}`;
39
+ return `agent/${withModelOverride(opts.agent, opts.model)}`;
38
40
  if (opts.model)
39
41
  return opts.model;
40
42
  throw new Error("Either --agent or --model is required.");
41
43
  }
42
- /**
43
- * Extracts the assistant's text from a Responses API result: the
44
- * `output_text` content parts of every `message` output item, in order.
45
- */
46
- export function extractResponseText(result) {
47
- const parts = [];
48
- for (const item of result?.output ?? []) {
49
- if (item.type !== "message")
50
- continue;
51
- for (const part of item.content ?? []) {
52
- if (part.type === "output_text" && part.text) {
53
- parts.push(part.text);
54
- }
55
- }
56
- }
57
- return parts.join("\n");
58
- }
59
- function printResponseResult(result, command) {
44
+ function printResponseResult(result, command, agentRef) {
60
45
  if (isJsonOutput(command)) {
61
46
  console.log(JSON.stringify(result, null, 2));
62
47
  return;
63
48
  }
64
- const text = extractResponseText(result);
65
- if (text) {
66
- console.log(text);
67
- }
68
- if (result?.status && result.status !== "completed") {
69
- console.log(chalk.yellow(`\n[status: ${result.status}]`));
70
- }
49
+ printRunText(buildRunEnvelope(result, agentRef));
71
50
  if (result?.usage) {
72
51
  const usage = result.usage;
73
- console.log(chalk.dim(`\n[${result.model} | ${usage.input_tokens} in / ${usage.output_tokens} out / ${usage.total_tokens} total tokens]`));
74
- }
75
- if (result?.conversation?.id) {
76
- console.log(chalk.dim(`[conversation: ${result.conversation.id}]`));
77
- }
78
- }
79
- /**
80
- * Extracts the text delta from one line of an SSE stream, or `undefined`
81
- * when the line carries nothing to print (not a `data:` line, `[DONE]`, an
82
- * event with no textual delta, or unparsable JSON).
83
- */
84
- export function extractSseDelta(line) {
85
- const trimmed = line.trim();
86
- if (!trimmed.startsWith("data:"))
87
- return undefined;
88
- const payload = trimmed.slice(5).trim();
89
- if (!payload || payload === "[DONE]")
90
- return undefined;
91
- try {
92
- const event = JSON.parse(payload);
93
- const delta = event.delta;
94
- return typeof delta === "string" ? delta : undefined;
95
- }
96
- catch {
97
- return undefined;
98
- }
99
- }
100
- async function parseErrorResponse(res) {
101
- let body = {};
102
- try {
103
- body = await res.json();
104
- }
105
- catch {
106
- // Fall through to the defaults below.
107
- }
108
- return new BackboneApiError({
109
- status: body.status ?? res.status,
110
- error: body.error ?? res.statusText,
111
- message: body.message ?? `HTTP ${res.status}: ${res.statusText}`,
112
- timestamp: body.timestamp ?? new Date().toISOString(),
113
- });
114
- }
115
- /**
116
- * Streams `POST /v1/responses` over SSE, printing text deltas as they
117
- * arrive. Bypasses the typed openapi-fetch client (like the multipart raw
118
- * `fetch` calls in convert.ts/transcribe.ts) because the response body must
119
- * be read incrementally rather than parsed whole.
120
- */
121
- async function streamResponse(command, body) {
122
- const config = resolveConfig(command);
123
- const authHeader = await resolveAuthHeader(config);
124
- const baseUrl = config.baseUrl.replace(/\/+$/, "");
125
- const res = await withSpinner("Connecting...", () => fetch(`${baseUrl}/v1/responses`, {
126
- method: "POST",
127
- headers: {
128
- Authorization: authHeader,
129
- "Content-Type": "application/json",
130
- Accept: "text/event-stream",
131
- },
132
- body: JSON.stringify(body),
133
- }));
134
- if (!res.ok || !res.body) {
135
- throw await parseErrorResponse(res);
136
- }
137
- const reader = res.body.getReader();
138
- const decoder = new TextDecoder();
139
- let buffer = "";
140
- let wroteAny = false;
141
- for (;;) {
142
- const { done, value } = await reader.read();
143
- if (done)
144
- break;
145
- buffer += decoder.decode(value, { stream: true });
146
- const lines = buffer.split("\n");
147
- buffer = lines.pop() ?? "";
148
- for (const line of lines) {
149
- const delta = extractSseDelta(line);
150
- if (delta) {
151
- process.stdout.write(delta);
152
- wroteAny = true;
153
- }
154
- }
155
- }
156
- if (wroteAny) {
157
- process.stdout.write("\n");
52
+ console.log(chalk.dim(`[${result.model} | ${usage.input_tokens} in / ${usage.output_tokens} out / ${usage.total_tokens} total tokens]`));
158
53
  }
159
54
  }
160
55
  export function makeAiCommand() {
@@ -208,12 +103,15 @@ export function makeAiCommand() {
208
103
  .command("respond")
209
104
  .description("Send a request to the OpenAI-compatible Responses endpoint")
210
105
  .argument("[input]", "Input text (reads stdin if omitted)")
211
- .option("--agent <id>", "Invoke a stored agent by id or name (mutually exclusive with --model)")
212
- .option("--model <id>", "Model in provider/model format (mutually exclusive with --agent)")
106
+ .option("--agent <id>", "Invoke a stored agent by id or name, optionally with @label")
107
+ .option("--model <id>", "Model in provider/model format; with --agent, one of the agent's models to run this request on")
213
108
  .option("--conversation <id>", "Conversation ID this response belongs to")
214
- .option("--stream", "Stream the response as it is generated")
109
+ .option("--stream", "Stream the response (not supported by the server yet, #148)")
215
110
  .action(async (input, opts, command) => {
216
111
  await runAction(command, async () => {
112
+ if (opts.stream) {
113
+ throw new CliUsageError("Streaming is not supported by the server yet (#148). Run without --stream.");
114
+ }
217
115
  const model = resolveModel(opts);
218
116
  const inputText = input ?? readStdinInput();
219
117
  if (!inputText) {
@@ -222,24 +120,19 @@ export function makeAiCommand() {
222
120
  const body = {
223
121
  model,
224
122
  input: inputText,
225
- stream: !!opts.stream,
123
+ stream: false,
226
124
  };
227
125
  if (opts.conversation)
228
126
  body.conversation = opts.conversation;
229
- if (opts.stream) {
230
- await streamResponse(command, body);
231
- }
232
- else {
233
- const client = getClient(command);
234
- const { data } = await withSpinner("Generating response...", () =>
235
- // The wire `input` field also accepts a bare string (shorthand for
236
- // a single user message; see ResponseItemInputDeserializer), which
237
- // the generated type does not model — hence the cast, same as the
238
- // untyped convert.ts source-conversion calls.
239
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
- client.POST("/v1/responses", { body }));
241
- printResponseResult(data, command);
242
- }
127
+ const client = getClient(command);
128
+ const { data } = await withSpinner("Generating response...", () =>
129
+ // The wire `input` field also accepts a bare string (shorthand for
130
+ // a single user message; see ResponseItemInputDeserializer), which
131
+ // the generated type does not model — hence the cast, same as the
132
+ // untyped convert.ts source-conversion calls.
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ client.POST("/v1/responses", { body }));
135
+ printResponseResult(data, command, opts.agent);
243
136
  });
244
137
  });
245
138
  cmd