@2kw/ai 6.3.0-dev.39 → 6.3.0-dev.42

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.
@@ -5,7 +5,7 @@ import { isJsonOutput } from "../lib/config.js";
5
5
  import { BackboneApiError, CliUsageError } from "../lib/errors.js";
6
6
  import { withSpinner } from "../lib/output.js";
7
7
  import { resolveAgent, splitAgentRef, suggestAgents } from "../lib/agent-lookup.js";
8
- import { buildRunEnvelope, EXIT_CODES, printRunText, stripControl } from "../lib/agent-run.js";
8
+ import { buildRunEnvelope, EXIT_CODES, MODE_OPTION_HELP, modeItem, parseModeOption, printRunText, stripControl, } from "../lib/agent-run.js";
9
9
  import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, planDecisions, } from "../lib/agent-decide.js";
10
10
  import { PromptAbortedError, promptApprovals, readlineAsk } from "../lib/approval-prompt.js";
11
11
  import { withModelOverride } from "../lib/agent-models.js";
@@ -90,7 +90,7 @@ async function finishRun(command, client, target, first, opts) {
90
90
  throw new CliUsageError(`No pending approvals for ${responseId} (already decided or superseded).`);
91
91
  }
92
92
  const decisions = decisionsFromAnswers(rows, answers);
93
- result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), responseId, decisions, target.label, target.model));
93
+ result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), responseId, decisions, target.label, target.model, target.mode));
94
94
  env = buildRunEnvelope(result, agentRef);
95
95
  }
96
96
  }
@@ -122,14 +122,21 @@ export function makeAgentRunCommand() {
122
122
  .option("--conversation <id>", "Continue a conversation")
123
123
  .option("--continue <responseId>", "Continue from a previous response")
124
124
  .option("--model <model>", "Run this request on another model from the agent's list (sends <agent>#<model>)")
125
+ .option("--mode <mode>", MODE_OPTION_HELP)
125
126
  .option("--no-input", "Never prompt for approvals, even on a terminal")
126
127
  .option("--raw", "Print the untouched API response")
127
128
  .action(async (agentArg, input, opts, command) => {
128
129
  await runAction(command, async () => {
129
130
  const agentRef = withModelOverride(agentArg, opts.model);
131
+ const mode = parseModeOption(opts.mode);
130
132
  const text = resolveRunInput(input, opts.inputFile);
131
133
  const client = getClient(command);
132
- const body = { model: `agent/${agentRef}`, input: text, stream: false };
134
+ // Without --mode the body stays exactly as before #656 (D2): input is the bare string.
135
+ const body = {
136
+ model: `agent/${agentRef}`,
137
+ input: mode ? [{ type: "message", role: "user", content: text }, modeItem(mode)] : text,
138
+ stream: false,
139
+ };
133
140
  if (opts.conversation)
134
141
  body.conversation = opts.conversation;
135
142
  if (opts.continue)
@@ -146,7 +153,7 @@ export function makeAgentRunCommand() {
146
153
  // The prompt reads stdin and writes to stderr, so those two decide; stdout may be redirected.
147
154
  const interactive = !!process.stdin.isTTY && !!process.stderr.isTTY && !isJsonOutput(command) && opts.input !== false && !opts.raw;
148
155
  const { name, label, model } = splitAgentRef(agentRef);
149
- await finishRun(command, client, { name, label, model }, data, {
156
+ await finishRun(command, client, { name, label, model, mode }, data, {
150
157
  raw: !!opts.raw,
151
158
  interactive,
152
159
  });
@@ -164,9 +171,11 @@ export function makeAgentDecideCommand() {
164
171
  .option("--reject-all", "Reject every pending approval of the response")
165
172
  .option("--reason <text>", "Reason recorded on each decision (the model sees it on rejects)")
166
173
  .option("--remember", "Also approve later calls of the same tool in this conversation")
174
+ .option("--mode <mode>", MODE_OPTION_HELP)
167
175
  .option("--raw", "Print the untouched API response")
168
176
  .action(async (agentRef, opts, command) => {
169
177
  await runAction(command, async () => {
178
+ const mode = parseModeOption(opts.mode);
170
179
  const client = getClient(command);
171
180
  const { name, label, model } = splitAgentRef(agentRef);
172
181
  // Dropped silently, an empty override would continue the run on the default model.
@@ -182,8 +191,8 @@ export function makeAgentDecideCommand() {
182
191
  reason: opts.reason,
183
192
  remember: opts.remember,
184
193
  }, 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 });
194
+ const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model, mode));
195
+ await finishRun(command, client, { name, label, model, mode }, result, { raw: !!opts.raw, interactive: false });
187
196
  });
188
197
  });
189
198
  }
@@ -1,6 +1,6 @@
1
1
  import type { components } from "../generated/openapi.js";
2
2
  import type { ApiClient } from "./agent-lookup.js";
3
- import type { ResponsesResult } from "./agent-run.js";
3
+ import { type ConversationMode, type ResponsesResult } from "./agent-run.js";
4
4
  export type ApprovalRow = components["schemas"]["ToolApprovalDTO"];
5
5
  export interface DecideOptions {
6
6
  approve?: string[];
@@ -36,6 +36,7 @@ export declare function toApprovalItems(decisions: Decision[]): Record<string, s
36
36
  * server resolves the continuation's tools, instructions and model from this reference, so both must match
37
37
  * the run's or the continuation drops to another version or the agent's default model.
38
38
  * Never the echoed `name@<versionNumber>` (it would 404 as a label).
39
+ * With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
39
40
  */
40
- export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string): Promise<ResponsesResult>;
41
+ export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string, mode?: ConversationMode): Promise<ResponsesResult>;
41
42
  //# sourceMappingURL=agent-decide.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { modeItem } from "./agent-run.js";
1
2
  import { CliUsageError } from "./errors.js";
2
3
  import { paginationParams } from "./pagination.js";
3
4
  const MAX_PAGES = 50;
@@ -113,13 +114,15 @@ export function toApprovalItems(decisions) {
113
114
  * server resolves the continuation's tools, instructions and model from this reference, so both must match
114
115
  * the run's or the continuation drops to another version or the agent's default model.
115
116
  * Never the echoed `name@<versionNumber>` (it would 404 as a label).
117
+ * With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
116
118
  */
117
- export async function continueWithDecisions(client, agentId, responseId, decisions, label, model) {
119
+ export async function continueWithDecisions(client, agentId, responseId, decisions, label, model, mode) {
120
+ const approvals = toApprovalItems(decisions);
118
121
  const body = {
119
122
  model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
120
123
  previous_response_id: responseId,
121
124
  stream: false,
122
- input: toApprovalItems(decisions),
125
+ input: mode ? [...approvals, modeItem(mode)] : approvals,
123
126
  };
124
127
  // The generated body type does not model backbone: extension items, hence the cast.
125
128
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -25,10 +25,30 @@ export interface PendingToolCall {
25
25
  callId: string;
26
26
  arguments: unknown;
27
27
  }
28
+ /** The end user's conversation mode (epic &59); the server matches these three values exactly. */
29
+ export type ConversationMode = "plan" | "ask" | "auto";
30
+ export declare const CONVERSATION_MODES: readonly ConversationMode[];
31
+ /** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
32
+ export declare const MODE_OPTION_HELP: string;
33
+ /**
34
+ * The `--mode` value, checked before any request is sent: anything but the three exact values is a
35
+ * usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
36
+ */
37
+ export declare function parseModeOption(raw: string | undefined): ConversationMode | undefined;
38
+ /** The `backbone:mode` input item (S1 D7); always appended last. */
39
+ export declare function modeItem(mode: ConversationMode): {
40
+ type: "backbone:mode";
41
+ mode: ConversationMode;
42
+ };
43
+ /**
44
+ * The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
45
+ * and any value other than the three read as null, which also means "none set".
46
+ */
47
+ export declare function responseMode(result: ResponsesResult | undefined): ConversationMode | null;
28
48
  export interface RunEnvelope {
29
49
  status: RunStatus;
30
- /** Reserved for the server-side conversation mode (#656); always null until then. */
31
- mode: null;
50
+ /** The conversation mode the request ran under (`conversation_mode`); null when none is set. */
51
+ mode: ConversationMode | null;
32
52
  agent: string | null;
33
53
  version: number | null;
34
54
  responseId: string | null;
@@ -61,6 +81,7 @@ export interface ResponsesResult {
61
81
  incomplete_details?: {
62
82
  reason?: string;
63
83
  } | null;
84
+ conversation_mode?: string | null;
64
85
  }
65
86
  export declare const EXIT_CODES: Record<RunStatus, number>;
66
87
  /** The assistant's text: every `output_text` part of every `message` item, in order. */
@@ -1,4 +1,35 @@
1
1
  import chalk from "chalk";
2
+ import { CliUsageError } from "./errors.js";
3
+ export const CONVERSATION_MODES = ["plan", "ask", "auto"];
4
+ /** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
5
+ export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
6
+ "auto (the operator's policy as written). Without it, the conversation keeps its mode";
7
+ function isConversationMode(value) {
8
+ return typeof value === "string" && CONVERSATION_MODES.includes(value);
9
+ }
10
+ /**
11
+ * The `--mode` value, checked before any request is sent: anything but the three exact values is a
12
+ * usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
13
+ */
14
+ export function parseModeOption(raw) {
15
+ if (raw === undefined)
16
+ return undefined;
17
+ if (isConversationMode(raw))
18
+ return raw;
19
+ throw new CliUsageError(`Invalid --mode '${raw}': use plan, ask or auto.`);
20
+ }
21
+ /** The `backbone:mode` input item (S1 D7); always appended last. */
22
+ export function modeItem(mode) {
23
+ return { type: "backbone:mode", mode };
24
+ }
25
+ /**
26
+ * The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
27
+ * and any value other than the three read as null, which also means "none set".
28
+ */
29
+ export function responseMode(result) {
30
+ const raw = result?.conversation_mode;
31
+ return isConversationMode(raw) ? raw : null;
32
+ }
2
33
  export const EXIT_CODES = {
3
34
  completed: 0,
4
35
  requires_approval: 3,
@@ -118,7 +149,7 @@ export function buildRunEnvelope(result, agentRef) {
118
149
  : null;
119
150
  return {
120
151
  status,
121
- mode: null,
152
+ mode: responseMode(result),
122
153
  agent,
123
154
  version,
124
155
  responseId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.3.0-dev.39",
3
+ "version": "6.3.0-dev.42",
4
4
  "description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
5
  "keywords": [
6
6
  "cli",