@2kw/ai 8.0.6 → 8.1.0-dev.12
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/dist/commands/agent-run.js +13 -4
- package/dist/commands/ai.js +14 -1
- package/dist/commands/datasets.js +1 -1
- package/dist/commands/responses.js +2 -1
- package/dist/lib/agent-decide.js +4 -5
- package/dist/lib/agent-run.js +3 -0
- package/dist/lib/output.d.ts +4 -1
- package/dist/lib/output.js +12 -1
- package/dist/lib/response-stream.d.ts +14 -0
- package/dist/lib/response-stream.js +122 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { buildRunEnvelope, EXIT_CODES, MODE_OPTION_HELP, modeItem, parseModeOpti
|
|
|
9
9
|
import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, parseToolAnswers, planContinuation, } 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";
|
|
12
|
+
import { postResponse } from "../lib/response-stream.js";
|
|
12
13
|
import { readStdinInput } from "./ai.js";
|
|
13
14
|
export function resolveRunInput(arg, inputFile, readStdin = readStdinInput) {
|
|
14
15
|
const text = readRunInput(arg, inputFile, readStdin);
|
|
@@ -146,7 +147,7 @@ export function makeAgentRunCommand() {
|
|
|
146
147
|
const body = {
|
|
147
148
|
model: `agent/${agentRef}`,
|
|
148
149
|
input: mode ? [...turn, modeItem(mode)] : text ?? [],
|
|
149
|
-
stream:
|
|
150
|
+
stream: true,
|
|
150
151
|
};
|
|
151
152
|
if (opts.conversation)
|
|
152
153
|
body.conversation = opts.conversation;
|
|
@@ -154,9 +155,17 @@ export function makeAgentRunCommand() {
|
|
|
154
155
|
body.previous_response_id = opts.continue;
|
|
155
156
|
let data;
|
|
156
157
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
data = await withSpinner("Running agent...", (spinner) => postResponse(client, body, (started) => {
|
|
159
|
+
// Printed at once (#1729), so a run cut off later can still be found.
|
|
160
|
+
if (!started.conversation?.id)
|
|
161
|
+
return;
|
|
162
|
+
// ora's render() writes a frame even off a TTY, where it never spins.
|
|
163
|
+
if (spinner.isSpinning)
|
|
164
|
+
spinner.clear();
|
|
165
|
+
console.error(`Conversation: ${started.conversation.id}`);
|
|
166
|
+
if (spinner.isSpinning)
|
|
167
|
+
spinner.render();
|
|
168
|
+
}));
|
|
160
169
|
}
|
|
161
170
|
catch (err) {
|
|
162
171
|
throw await withAgentSuggestions(client, agentRef, err);
|
package/dist/commands/ai.js
CHANGED
|
@@ -5,6 +5,7 @@ import { chatUrlFor, isJsonOutput, resolveConfig } from "../lib/config.js";
|
|
|
5
5
|
import { CliUsageError } from "../lib/errors.js";
|
|
6
6
|
import { withModelOverride } from "../lib/agent-models.js";
|
|
7
7
|
import { formatList, withSpinner } from "../lib/output.js";
|
|
8
|
+
import { postResponse } from "../lib/response-stream.js";
|
|
8
9
|
import { buildRunEnvelope, extractResponseText, printRunText, } from "../lib/agent-run.js";
|
|
9
10
|
import chalk from "chalk";
|
|
10
11
|
export { extractResponseText };
|
|
@@ -125,7 +126,19 @@ export function makeAiCommand() {
|
|
|
125
126
|
if (opts.conversation)
|
|
126
127
|
body.conversation = opts.conversation;
|
|
127
128
|
const client = getClient(command);
|
|
128
|
-
|
|
129
|
+
// An agent turn is streamed (#1732): a proxy cuts a request that answers nothing for 100 s.
|
|
130
|
+
const data = await withSpinner("Generating response...", async (spinner) => opts.agent
|
|
131
|
+
? postResponse(client, body, (started) => {
|
|
132
|
+
// Printed at once, as `agents run` does, so a turn cut off later can still be found.
|
|
133
|
+
if (!started.conversation?.id)
|
|
134
|
+
return;
|
|
135
|
+
if (spinner.isSpinning)
|
|
136
|
+
spinner.clear();
|
|
137
|
+
console.error(`Conversation: ${started.conversation.id}`);
|
|
138
|
+
if (spinner.isSpinning)
|
|
139
|
+
spinner.render();
|
|
140
|
+
})
|
|
141
|
+
: (await client.POST("/v1/responses", { body })).data);
|
|
129
142
|
printResponseResult(data, command, opts.agent);
|
|
130
143
|
});
|
|
131
144
|
});
|
|
@@ -41,7 +41,7 @@ export function makeDatasetsCommand() {
|
|
|
41
41
|
.description("Create a new dataset")
|
|
42
42
|
.requiredOption("-n, --name <name>", "Dataset name")
|
|
43
43
|
.option("-d, --description <text>", "Dataset description")
|
|
44
|
-
.option("-t, --type <type>", "Dataset type")
|
|
44
|
+
.option("-t, --type <type>", "Dataset type (default: text)")
|
|
45
45
|
.option("--input-schema <json>", "Input JSON Schema")
|
|
46
46
|
.option("--expected-output-schema <json>", "Expected output JSON Schema")
|
|
47
47
|
.option("--metadata <json>", "Metadata (JSON)")
|
|
@@ -7,7 +7,8 @@ export function makeResponsesCommand() {
|
|
|
7
7
|
cmd
|
|
8
8
|
.command("get")
|
|
9
9
|
.description("Read a background response as it stands now: queued, in_progress, requires_action, completed, " +
|
|
10
|
-
"incomplete, failed or cancelled. Only background responses can be read by id
|
|
10
|
+
"incomplete, failed or cancelled. Only background responses can be read by id: the result of " +
|
|
11
|
+
"`agents run` is read with `conversations items list <conversationId>`.")
|
|
11
12
|
.argument("<responseId>", "Response ID")
|
|
12
13
|
.action(async (responseId, _opts, command) => {
|
|
13
14
|
await runAction(command, async () => {
|
package/dist/lib/agent-decide.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { modeItem } from "./agent-run.js";
|
|
3
3
|
import { CliUsageError } from "./errors.js";
|
|
4
4
|
import { paginationParams } from "./pagination.js";
|
|
5
|
+
import { postResponse } from "./response-stream.js";
|
|
5
6
|
const defaultIo = {
|
|
6
7
|
readFile: (path) => readFileSync(path, "utf-8"),
|
|
7
8
|
// Not readStdinInput: that one trims, and a tool result is sent as read (P5).
|
|
@@ -215,12 +216,10 @@ export async function continueWithDecisions(client, agentId, responseId, decisio
|
|
|
215
216
|
const body = {
|
|
216
217
|
model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
|
|
217
218
|
previous_response_id: responseId,
|
|
218
|
-
stream:
|
|
219
|
+
stream: true,
|
|
219
220
|
input: mode ? [...items, modeItem(mode)] : items,
|
|
220
221
|
};
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
const { data } = await client.POST("/v1/responses", { body });
|
|
224
|
-
return data;
|
|
222
|
+
// Streamed for the reason a run is (#1729): the rest of the turn can outlast a proxy's timeout.
|
|
223
|
+
return postResponse(client, body);
|
|
225
224
|
}
|
|
226
225
|
//# sourceMappingURL=agent-decide.js.map
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -189,6 +189,9 @@ const DECODED_ITEM_TYPES = new Set([
|
|
|
189
189
|
"backbone:tool_image",
|
|
190
190
|
// A table or chart a finished ShowTable or ShowChart call showed (#1419): it never holds a run either.
|
|
191
191
|
"backbone:ui",
|
|
192
|
+
// A canvas a finished CanvasWrite or CanvasEdit call saved, and a member's selection (#1698): neither holds a run.
|
|
193
|
+
"backbone:canvas",
|
|
194
|
+
"backbone:canvas_selection",
|
|
192
195
|
"mcp_approval_request",
|
|
193
196
|
"mcp_call",
|
|
194
197
|
"mcp_list_tools",
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Ora } from "ora";
|
|
1
2
|
import type { Command } from "commander";
|
|
2
3
|
type AnyRecord = Record<string, any>;
|
|
3
4
|
/**
|
|
@@ -21,9 +22,11 @@ export declare function formatList(items: AnyRecord[] | undefined, command: Comm
|
|
|
21
22
|
* Display a success message.
|
|
22
23
|
*/
|
|
23
24
|
export declare function formatSuccess(message: string, command: Command): void;
|
|
25
|
+
/** Stops the running spinner, if any, which shows the cursor again. Call it before `process.exit`. */
|
|
26
|
+
export declare function stopSpinner(): void;
|
|
24
27
|
/**
|
|
25
28
|
* Run an async action with a spinner.
|
|
26
29
|
*/
|
|
27
|
-
export declare function withSpinner<T>(text: string, action: () => Promise<T>): Promise<T>;
|
|
30
|
+
export declare function withSpinner<T>(text: string, action: (spinner: Ora) => Promise<T>): Promise<T>;
|
|
28
31
|
export {};
|
|
29
32
|
//# sourceMappingURL=output.d.ts.map
|
package/dist/lib/output.js
CHANGED
|
@@ -82,13 +82,20 @@ export function formatSuccess(message, command) {
|
|
|
82
82
|
console.log(chalk.green(message));
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
/** The spinner `withSpinner` runs now, so an exit on Ctrl+C can stop it: ora hides the cursor while it spins (#1733). */
|
|
86
|
+
let running;
|
|
87
|
+
/** Stops the running spinner, if any, which shows the cursor again. Call it before `process.exit`. */
|
|
88
|
+
export function stopSpinner() {
|
|
89
|
+
running?.stop();
|
|
90
|
+
}
|
|
85
91
|
/**
|
|
86
92
|
* Run an async action with a spinner.
|
|
87
93
|
*/
|
|
88
94
|
export async function withSpinner(text, action) {
|
|
89
95
|
const spinner = ora(text).start();
|
|
96
|
+
running = spinner;
|
|
90
97
|
try {
|
|
91
|
-
const result = await action();
|
|
98
|
+
const result = await action(spinner);
|
|
92
99
|
spinner.succeed();
|
|
93
100
|
return result;
|
|
94
101
|
}
|
|
@@ -96,6 +103,10 @@ export async function withSpinner(text, action) {
|
|
|
96
103
|
spinner.fail();
|
|
97
104
|
throw err;
|
|
98
105
|
}
|
|
106
|
+
finally {
|
|
107
|
+
if (running === spinner)
|
|
108
|
+
running = undefined;
|
|
109
|
+
}
|
|
99
110
|
}
|
|
100
111
|
function formatValue(value) {
|
|
101
112
|
if (value === null || value === undefined)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ApiClient } from "./agent-lookup.js";
|
|
2
|
+
import type { ResponsesResult } from "./agent-run.js";
|
|
3
|
+
/**
|
|
4
|
+
* POSTs an agent turn to `/v1/responses` and returns its terminal response, as `stream: false` would (#1729).
|
|
5
|
+
* The turn is streamed: a proxy in front of the API cuts a request that answers nothing for 100 s (HTTP 524),
|
|
6
|
+
* and a stream's events and keepalives keep it open however long the run takes. A model that cannot stream
|
|
7
|
+
* is refused before any work (`stream_not_supported`), so that turn is sent again without a stream.
|
|
8
|
+
*
|
|
9
|
+
* `onStart` gets the response as `response.created` names it, before the run works. Once the conversation is
|
|
10
|
+
* known, a cut stream or Ctrl+C names it and the command that reads the run's result: a synchronous
|
|
11
|
+
* response cannot be read by id afterwards (`GET /v1/responses/{id}` reads background responses only).
|
|
12
|
+
*/
|
|
13
|
+
export declare function postResponse(client: ApiClient, body: Record<string, unknown>, onStart?: (started: ResponsesResult) => void): Promise<ResponsesResult>;
|
|
14
|
+
//# sourceMappingURL=response-stream.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { BackboneApiError } from "./errors.js";
|
|
2
|
+
import { stopSpinner } from "./output.js";
|
|
3
|
+
/**
|
|
4
|
+
* POSTs an agent turn to `/v1/responses` and returns its terminal response, as `stream: false` would (#1729).
|
|
5
|
+
* The turn is streamed: a proxy in front of the API cuts a request that answers nothing for 100 s (HTTP 524),
|
|
6
|
+
* and a stream's events and keepalives keep it open however long the run takes. A model that cannot stream
|
|
7
|
+
* is refused before any work (`stream_not_supported`), so that turn is sent again without a stream.
|
|
8
|
+
*
|
|
9
|
+
* `onStart` gets the response as `response.created` names it, before the run works. Once the conversation is
|
|
10
|
+
* known, a cut stream or Ctrl+C names it and the command that reads the run's result: a synchronous
|
|
11
|
+
* response cannot be read by id afterwards (`GET /v1/responses/{id}` reads background responses only).
|
|
12
|
+
*/
|
|
13
|
+
export async function postResponse(client, body, onStart) {
|
|
14
|
+
let response;
|
|
15
|
+
try {
|
|
16
|
+
// The generated body type does not model a bare-string input or backbone: items, hence the cast.
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
({ response } = await client.POST("/v1/responses", { body: { ...body, stream: true }, parseAs: "stream" }));
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (!(err instanceof BackboneApiError && err.code === "stream_not_supported"))
|
|
22
|
+
throw err;
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const { data } = await client.POST("/v1/responses", { body: { ...body, stream: false } });
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
28
|
+
return (await response.json());
|
|
29
|
+
}
|
|
30
|
+
return readResponseStream(response.body, onStart);
|
|
31
|
+
}
|
|
32
|
+
/** How to read a run whose stream was lost: the conversation keeps every item the run wrote. */
|
|
33
|
+
function readHint(started) {
|
|
34
|
+
const conversation = started?.conversation?.id;
|
|
35
|
+
return conversation
|
|
36
|
+
? ` The run goes on on the server; read its result with: 2kw conversations items list ${conversation}`
|
|
37
|
+
: "";
|
|
38
|
+
}
|
|
39
|
+
/** Reads Open Responses server-sent events up to the terminal one; comments (keepalives) are skipped. */
|
|
40
|
+
async function readResponseStream(body, onStart) {
|
|
41
|
+
let started;
|
|
42
|
+
let error;
|
|
43
|
+
const failed = () => new BackboneApiError({
|
|
44
|
+
status: 500,
|
|
45
|
+
error: error?.code ?? "run_failed",
|
|
46
|
+
code: error?.code ?? undefined,
|
|
47
|
+
message: error?.message ?? "The response failed.",
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
});
|
|
50
|
+
const onInterrupt = () => {
|
|
51
|
+
stopSpinner(); // a spinning ora hides the cursor, and process.exit would leave it hidden (#1733)
|
|
52
|
+
console.error(`Interrupted.${readHint(started)}`);
|
|
53
|
+
process.exit(130);
|
|
54
|
+
};
|
|
55
|
+
const handle = (frame) => {
|
|
56
|
+
const data = frame
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.filter((line) => line.startsWith("data:"))
|
|
59
|
+
.map((line) => line.slice(5).trimStart())
|
|
60
|
+
.join("\n");
|
|
61
|
+
if (!data || data === "[DONE]")
|
|
62
|
+
return undefined;
|
|
63
|
+
let event;
|
|
64
|
+
try {
|
|
65
|
+
event = JSON.parse(data);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined; // a garbled frame is skipped: the run goes on, and its terminal event still ends it
|
|
69
|
+
}
|
|
70
|
+
switch (event.type) {
|
|
71
|
+
case "response.created":
|
|
72
|
+
started = event.response;
|
|
73
|
+
if (started?.conversation?.id)
|
|
74
|
+
process.once("SIGINT", onInterrupt);
|
|
75
|
+
onStart?.(event.response);
|
|
76
|
+
return undefined;
|
|
77
|
+
case "error":
|
|
78
|
+
error = event;
|
|
79
|
+
return undefined;
|
|
80
|
+
case "response.completed":
|
|
81
|
+
case "response.incomplete":
|
|
82
|
+
return event.response;
|
|
83
|
+
case "response.failed":
|
|
84
|
+
throw failed();
|
|
85
|
+
default:
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const decoder = new TextDecoder();
|
|
90
|
+
let buffer = "";
|
|
91
|
+
const reader = body.getReader();
|
|
92
|
+
try {
|
|
93
|
+
for (;;) {
|
|
94
|
+
let chunk;
|
|
95
|
+
try {
|
|
96
|
+
chunk = await reader.read();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
throw new Error(`The connection closed before the run finished (${err.message}).${readHint(started)}`);
|
|
100
|
+
}
|
|
101
|
+
if (chunk.done)
|
|
102
|
+
break;
|
|
103
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
104
|
+
let end;
|
|
105
|
+
while ((end = /\r?\n\r?\n/.exec(buffer))) {
|
|
106
|
+
const frame = buffer.slice(0, end.index);
|
|
107
|
+
buffer = buffer.slice(end.index + end[0].length);
|
|
108
|
+
const terminal = handle(frame);
|
|
109
|
+
if (terminal)
|
|
110
|
+
return terminal;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
116
|
+
await reader.cancel().catch(() => { });
|
|
117
|
+
}
|
|
118
|
+
if (error)
|
|
119
|
+
throw failed(); // an error event whose response.failed never arrived
|
|
120
|
+
throw new Error(`The response stream ended before the run finished.${readHint(started)}`);
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=response-stream.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2kw/ai",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.1.0-dev.12",
|
|
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",
|