@tiny-fish/cli 0.50.2-next.407 → 0.51.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/README.md +8 -0
- package/dist/commands/research.d.ts +2 -0
- package/dist/commands/research.js +181 -0
- package/dist/index.js +7 -0
- package/dist/lib/client.d.ts +74 -0
- package/dist/lib/client.js +84 -11
- package/dist/lib/list-options.js +2 -1
- package/dist/lib/research.d.ts +158 -0
- package/dist/lib/research.js +78 -0
- package/dist/lib/signals.d.ts +3 -0
- package/dist/lib/signals.js +19 -0
- package/dist/program.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -454,3 +454,11 @@ TinyFish-owned origins (`*.tinyfish.ai`, `*.tinyfish.io`) or a local development
|
|
|
454
454
|
--sync \
|
|
455
455
|
--pretty
|
|
456
456
|
```
|
|
457
|
+
|
|
458
|
+
### Research
|
|
459
|
+
|
|
460
|
+
`tinyfish research run "question"` streams JSON events; `--async` returns a durable `research_run_id`. Use `--mode max` for deeper research with a larger budget; slower and costs more (default: deep), `--language ja`, or `--pretty` for readable output.
|
|
461
|
+
|
|
462
|
+
Manage runs with `tinyfish research run get <id>`, `list`, `watch <id>`, and `cancel <id>`. `get` includes partial/final reports; `list` supports `--status`, `--query`, `--limit`, `--cursor`, and `--sort-direction`. `watch --timeout <ms>` replays saved events and waits up to 45 minutes by default.
|
|
463
|
+
|
|
464
|
+
Ctrl+C during a streamed start requests cancellation with a five-second timeout; during watch it only detaches. JSON output stays on stdout; recovery hints go to stderr. Failed, cancelled, timed-out, and incomplete streams exit unsuccessfully. Starts are not retried automatically: if no ID arrives, check `list` before submitting again.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { withResolvedKey } from '../lib/auth.js';
|
|
3
|
+
import * as api from '../lib/client.js';
|
|
4
|
+
import { integerInRange } from '../lib/list-options.js';
|
|
5
|
+
import { ApiError, errLine, handleApiError, out, outLine } from '../lib/output.js';
|
|
6
|
+
import * as research from '../lib/research.js';
|
|
7
|
+
import { installSignalGuard, onBrokenPipe } from '../lib/signals.js';
|
|
8
|
+
const runLine = (run) => `${run.research_run_id} ${run.status} ${run.query}`;
|
|
9
|
+
function printReport(report) {
|
|
10
|
+
outLine(report.result);
|
|
11
|
+
report.citations.forEach((url, index) => outLine(`[${index + 1}] ${url}`));
|
|
12
|
+
}
|
|
13
|
+
function printRun(run, pretty) {
|
|
14
|
+
if (!pretty)
|
|
15
|
+
return out(run);
|
|
16
|
+
outLine(runLine(run));
|
|
17
|
+
if (run.deep_result)
|
|
18
|
+
printReport(run.deep_result);
|
|
19
|
+
}
|
|
20
|
+
async function observe(input, apiKey, pretty, timeout = research.RESEARCH_TIMEOUT_MS) {
|
|
21
|
+
const started = typeof input !== 'string';
|
|
22
|
+
let runId = started ? undefined : input;
|
|
23
|
+
// Guard exits 130 once the handler settles.
|
|
24
|
+
const uninstall = installSignalGuard(() => interrupted(started, runId, apiKey));
|
|
25
|
+
const removePipeHandler = onBrokenPipe(() => interrupted(started, runId, apiKey));
|
|
26
|
+
try {
|
|
27
|
+
let done = false;
|
|
28
|
+
for await (const event of api.researchStream(input, apiKey, AbortSignal.timeout(timeout))) {
|
|
29
|
+
if (event.event === 'created') {
|
|
30
|
+
runId = event.research_run_id;
|
|
31
|
+
errLine(`Research ID: ${runId}`);
|
|
32
|
+
}
|
|
33
|
+
renderEvent(event, pretty);
|
|
34
|
+
if (event.event === 'done') {
|
|
35
|
+
done = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!done || !runId)
|
|
40
|
+
throw new Error('Research stream ended before completion');
|
|
41
|
+
const result = await api.getResearch(runId, apiKey);
|
|
42
|
+
if (result.status !== 'COMPLETED') {
|
|
43
|
+
printRun(result, pretty);
|
|
44
|
+
throw new Error(`Research ${result.status}: ${result.error?.message ?? runId}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
recoveryHint(error, started, runId);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
uninstall();
|
|
53
|
+
removePipeHandler();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Fields per event: internal-docs/sse-research-events.md.
|
|
57
|
+
const PROGRESS = {
|
|
58
|
+
run_status: ['status'],
|
|
59
|
+
session: ['session_id'],
|
|
60
|
+
plan_updated: ['iteration', 'subquestions', 'search_queries'],
|
|
61
|
+
sources_searched: ['candidates'],
|
|
62
|
+
sources_fetching: ['urls'],
|
|
63
|
+
source_fetched: ['status', 'url'],
|
|
64
|
+
partial_summary: ['citations'],
|
|
65
|
+
evidence_snapshot: ['evidence'],
|
|
66
|
+
run_stats: ['stats'],
|
|
67
|
+
};
|
|
68
|
+
function field(key, value) {
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return [`${value.length} ${key}`];
|
|
71
|
+
if (typeof value === 'number')
|
|
72
|
+
return [`${key} ${value}`];
|
|
73
|
+
if (typeof value === 'string')
|
|
74
|
+
return [value];
|
|
75
|
+
if (typeof value !== 'object' || !value)
|
|
76
|
+
return [];
|
|
77
|
+
return Object.entries(value).flatMap(([k, v]) => (typeof v === 'number' ? field(k, v) : []));
|
|
78
|
+
}
|
|
79
|
+
function renderEvent(event, pretty) {
|
|
80
|
+
if (!pretty)
|
|
81
|
+
return out(event);
|
|
82
|
+
if (event.event === 'final_result')
|
|
83
|
+
return printReport(research.reportSchema.parse(event));
|
|
84
|
+
if (event.event === 'heartbeat')
|
|
85
|
+
return;
|
|
86
|
+
const detail = (PROGRESS[event.event] ?? ['message']).flatMap((key) => field(key, event[key]));
|
|
87
|
+
errLine(detail.length ? `${event.event}: ${detail.join(', ')}` : event.event);
|
|
88
|
+
}
|
|
89
|
+
function recoveryHint(error, started, runId) {
|
|
90
|
+
// A 4xx before creation means no run exists.
|
|
91
|
+
if (error instanceof ApiError && error.status < 500 && !(started && runId))
|
|
92
|
+
return;
|
|
93
|
+
errLine(`Check tinyfish research run ${runId ? `get ${runId}` : 'list before starting again'}.`);
|
|
94
|
+
}
|
|
95
|
+
async function interrupted(started, id, apiKey) {
|
|
96
|
+
if (!id)
|
|
97
|
+
return errLine('No research ID received. Check tinyfish research run list.');
|
|
98
|
+
if (!started)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
errLine(`Research ${(await api.cancelResearch(id, apiKey)).status}.`);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
errLine(`Cancellation unconfirmed. Use tinyfish research run cancel ${id}.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
108
|
+
// The cursor is only valid with the filters that produced it.
|
|
109
|
+
function nextPageFlags(opts, cursor) {
|
|
110
|
+
const flags = [
|
|
111
|
+
['--status', opts.status],
|
|
112
|
+
['--query', opts.query === undefined ? undefined : shellQuote(opts.query)],
|
|
113
|
+
['--limit', opts.limit],
|
|
114
|
+
['--sort-direction', opts.sortDirection],
|
|
115
|
+
['--cursor', cursor],
|
|
116
|
+
];
|
|
117
|
+
return flags
|
|
118
|
+
.flatMap(([flag, value]) => (value === undefined ? [] : [`${flag} ${value}`]))
|
|
119
|
+
.join(' ');
|
|
120
|
+
}
|
|
121
|
+
function start(query, opts) {
|
|
122
|
+
if (!query?.trim())
|
|
123
|
+
handleApiError(new Error('query required'));
|
|
124
|
+
return withResolvedKey(async (apiKey) => {
|
|
125
|
+
const request = api.parseWithSchema(research.requestSchema, { query, mode: opts.mode, output_language: opts.language }, 'Invalid research request');
|
|
126
|
+
if (!opts.async)
|
|
127
|
+
return observe(request, apiKey, opts.pretty);
|
|
128
|
+
const result = await api.startResearchAsync(request, apiKey);
|
|
129
|
+
if (opts.pretty)
|
|
130
|
+
outLine(result.research_run_id);
|
|
131
|
+
else
|
|
132
|
+
out(result);
|
|
133
|
+
if (result.error)
|
|
134
|
+
throw new Error(result.error.message);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export function registerResearch(program) {
|
|
138
|
+
const group = program
|
|
139
|
+
.command('research')
|
|
140
|
+
.description('Research reports')
|
|
141
|
+
.enablePositionalOptions();
|
|
142
|
+
const run = group
|
|
143
|
+
.command('run')
|
|
144
|
+
.description('Start research or manage runs')
|
|
145
|
+
.argument('[query]', 'Research question; a lone subcommand name runs that subcommand')
|
|
146
|
+
.option('--async', 'Submit without waiting for the report')
|
|
147
|
+
.addOption(new Option('--mode <mode>', 'deep (default) | max: deeper research with a larger budget; slower and costs more').choices(research.requestSchema.shape.mode.unwrap().options))
|
|
148
|
+
.option('--language <tag>', 'Report language (BCP 47)')
|
|
149
|
+
.option('--pretty', 'Human-readable output')
|
|
150
|
+
.addHelpText('after', '\nCtrl+C cancels a started run; watch only detaches.')
|
|
151
|
+
.action(start);
|
|
152
|
+
const command = (name, description) => run.command(name).description(description).option('--pretty', 'Human-readable output');
|
|
153
|
+
command('get <id>', 'Get status and partial or final results').action((id, opts) => withResolvedKey(async (apiKey) => printRun(await api.getResearch(id, apiKey), opts.pretty)));
|
|
154
|
+
command('cancel <id>', 'Cancel research; safe to repeat').action((id, opts) => withResolvedKey(async (apiKey) => {
|
|
155
|
+
const result = await api.cancelResearch(id, apiKey);
|
|
156
|
+
if (!opts.pretty)
|
|
157
|
+
return out(result);
|
|
158
|
+
outLine(`${result.research_run_id} ${result.status}${result.cancelled ? '' : ' (unchanged)'}`);
|
|
159
|
+
}));
|
|
160
|
+
command('list', 'List research runs')
|
|
161
|
+
.addOption(new Option('--status <status>', 'Run status').choices(research.statusSchema.options))
|
|
162
|
+
.option('--query <text>', 'Filter by query text')
|
|
163
|
+
.option('--limit <n>', 'Maximum runs (1-100; default: 20)', integerInRange(1, 100))
|
|
164
|
+
.option('--cursor <cursor>', 'Pagination cursor from a previous response')
|
|
165
|
+
.addOption(new Option('--sort-direction <direction>', 'Creation order (default: desc)').choices(research.listQuerySchema.shape.sort_direction.unwrap().options))
|
|
166
|
+
.action((opts) => withResolvedKey(async (apiKey) => {
|
|
167
|
+
const { pretty, sortDirection, ...filters } = opts;
|
|
168
|
+
const query = api.parseWithSchema(research.listQuerySchema, { ...filters, sort_direction: sortDirection }, 'Invalid list filters');
|
|
169
|
+
const result = await api.listResearch(query, apiKey);
|
|
170
|
+
if (!pretty)
|
|
171
|
+
return out(result);
|
|
172
|
+
if (result.data.length === 0)
|
|
173
|
+
outLine('No research runs.');
|
|
174
|
+
result.data.forEach((item) => outLine(runLine(item)));
|
|
175
|
+
if (result.pagination.next_cursor)
|
|
176
|
+
outLine(`Next: ${nextPageFlags(opts, result.pagination.next_cursor)}`);
|
|
177
|
+
}));
|
|
178
|
+
command('watch <id>', 'Replay and stream an existing run; Ctrl+C detaches')
|
|
179
|
+
.option('--timeout <ms>', 'Maximum watch duration in milliseconds', integerInRange(1, 2147483647), research.RESEARCH_TIMEOUT_MS)
|
|
180
|
+
.action((id, opts) => withResolvedKey((apiKey) => observe(api.parseResearchId(id), apiKey, opts.pretty, opts.timeout)));
|
|
181
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { buildProgram } from './program.js';
|
|
3
|
+
import { handleBrokenPipe } from './lib/signals.js';
|
|
4
|
+
// A reader closing early (`| head`) is not an error.
|
|
5
|
+
process.stdout.on('error', (error) => {
|
|
6
|
+
if (error.code !== 'EPIPE')
|
|
7
|
+
throw error;
|
|
8
|
+
handleBrokenPipe();
|
|
9
|
+
});
|
|
3
10
|
// Await parseAsync so async command handlers complete before the process exits
|
|
4
11
|
buildProgram()
|
|
5
12
|
.then(({ program }) => program.parseAsync(process.argv))
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStr
|
|
|
2
2
|
import type { PageParams } from './list-options.js';
|
|
3
3
|
import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileSummary, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from './types.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import * as research from './research.js';
|
|
5
6
|
type ConnectMap = Record<string, {
|
|
6
7
|
attempt_id: string;
|
|
7
8
|
}> | undefined;
|
|
8
9
|
export declare function buildConnectHeaders(connect: ConnectMap, caller: string | null): Record<string, string>;
|
|
9
10
|
export declare function setupPageConnectHeaders(attemptId: string | undefined): Record<string, string>;
|
|
10
11
|
export declare function resolveClientName(env?: Record<string, string | undefined>): string;
|
|
12
|
+
export declare function parseWithSchema<T>(schema: z.ZodType<T>, value: unknown, message: string): T;
|
|
11
13
|
export declare function runSync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunResponse>;
|
|
12
14
|
export declare function runAsync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunAsyncResponse>;
|
|
13
15
|
export declare function runStream(req: CliAgentRunParams, apiKey: string, signal?: AbortSignal, onDropped?: (count: number) => void): AsyncGenerator<AgentRunWithStreamingResponse>;
|
|
@@ -137,4 +139,76 @@ export declare function vaultConnect(req: VaultConnectRequest, apiKey: string):
|
|
|
137
139
|
export declare function vaultDisconnect(connectionId: string, apiKey: string): Promise<VaultDisconnectResponse>;
|
|
138
140
|
export declare function vaultItems(apiKey: string): Promise<VaultItemsListResponse>;
|
|
139
141
|
export declare function vaultSync(apiKey: string): Promise<VaultItemsSyncResponse>;
|
|
142
|
+
export declare const parseResearchId: (id: string) => string;
|
|
143
|
+
export declare function startResearchAsync(req: research.Request, apiKey: string): Promise<{
|
|
144
|
+
[x: string]: unknown;
|
|
145
|
+
research_run_id: string;
|
|
146
|
+
error: {
|
|
147
|
+
code: string;
|
|
148
|
+
message: string;
|
|
149
|
+
} | null;
|
|
150
|
+
}>;
|
|
151
|
+
export declare function getResearch(id: string, apiKey: string): Promise<{
|
|
152
|
+
[x: string]: unknown;
|
|
153
|
+
research_run_id: string;
|
|
154
|
+
status: "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED" | "QUEUED" | "TIMED_OUT";
|
|
155
|
+
query: string;
|
|
156
|
+
quick_result: {
|
|
157
|
+
[x: string]: unknown;
|
|
158
|
+
answer: string;
|
|
159
|
+
citations: string[];
|
|
160
|
+
} | null;
|
|
161
|
+
quick_result_summary: string | null;
|
|
162
|
+
deep_result: {
|
|
163
|
+
[x: string]: unknown;
|
|
164
|
+
result: string;
|
|
165
|
+
citations: string[];
|
|
166
|
+
} | null;
|
|
167
|
+
run_ids: string[];
|
|
168
|
+
created_at: string | null;
|
|
169
|
+
completed_at: string | null;
|
|
170
|
+
error: {
|
|
171
|
+
code: string;
|
|
172
|
+
message: string;
|
|
173
|
+
} | null;
|
|
174
|
+
research_plan: z.core.util.JSONType;
|
|
175
|
+
evidence: z.core.util.JSONType[] | null;
|
|
176
|
+
iterations: number | null;
|
|
177
|
+
}>;
|
|
178
|
+
export declare function listResearch(opts: research.ListQuery, apiKey: string): Promise<{
|
|
179
|
+
[x: string]: unknown;
|
|
180
|
+
data: {
|
|
181
|
+
[x: string]: unknown;
|
|
182
|
+
research_run_id: string;
|
|
183
|
+
status: "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED" | "QUEUED" | "TIMED_OUT";
|
|
184
|
+
query: string;
|
|
185
|
+
quick_result: {
|
|
186
|
+
[x: string]: unknown;
|
|
187
|
+
answer: string;
|
|
188
|
+
citations: string[];
|
|
189
|
+
} | null;
|
|
190
|
+
quick_result_summary: string | null;
|
|
191
|
+
deep_result: {
|
|
192
|
+
[x: string]: unknown;
|
|
193
|
+
result: string;
|
|
194
|
+
citations: string[];
|
|
195
|
+
} | null;
|
|
196
|
+
run_ids: string[];
|
|
197
|
+
created_at: string | null;
|
|
198
|
+
completed_at: string | null;
|
|
199
|
+
}[];
|
|
200
|
+
pagination: {
|
|
201
|
+
[x: string]: unknown;
|
|
202
|
+
total: number;
|
|
203
|
+
next_cursor: string | null;
|
|
204
|
+
has_more: boolean;
|
|
205
|
+
};
|
|
206
|
+
}>;
|
|
207
|
+
export declare function cancelResearch(id: string, apiKey: string): Promise<{
|
|
208
|
+
[x: string]: unknown;
|
|
209
|
+
research_run_id: string;
|
|
210
|
+
status: "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED" | "QUEUED" | "TIMED_OUT";
|
|
211
|
+
cancelled: boolean;
|
|
212
|
+
}>;
|
|
213
|
+
export declare function researchStream(input: research.Request | string, apiKey: string, signal: AbortSignal): AsyncGenerator<research.Event>;
|
|
140
214
|
export {};
|
package/dist/lib/client.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from '@tiny-fish/sdk';
|
|
1
|
+
import { APIConnectionError, APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from '@tiny-fish/sdk';
|
|
2
2
|
import { CONNECT_SOURCE, RUNTIME_ATTEMPT_ENV, loadConfig } from './auth.js';
|
|
3
3
|
import { API_URL_OVERRIDE, CLI_AGENT_IDENTITY, CLI_VERSION } from './constants.js';
|
|
4
4
|
import { detectHarness, detectHumanInitiated } from './harness.js';
|
|
5
5
|
import { captureNotice } from './notice.js';
|
|
6
6
|
import { ApiError, parseJson } from './output.js';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import * as research from './research.js';
|
|
8
9
|
// Install-level connect context (PF-3169). A known caller is credited only to its own
|
|
9
10
|
// install; under an unknown caller we credit a lone install, else send source only.
|
|
10
11
|
// Never guess a client. Request-level harness truth is client_name (PF-3168).
|
|
@@ -84,6 +85,19 @@ class TinyFishCliClient extends TinyFish {
|
|
|
84
85
|
}
|
|
85
86
|
return headers;
|
|
86
87
|
}
|
|
88
|
+
// The SDK lacks GET streaming; reuse its authentication headers.
|
|
89
|
+
async researchEvents(path, signal) {
|
|
90
|
+
const response = await fetch(`${this.baseURL}${path}`, {
|
|
91
|
+
headers: { ...this._buildHeaders(), Accept: 'text/event-stream' },
|
|
92
|
+
signal,
|
|
93
|
+
});
|
|
94
|
+
captureNotice(response.headers);
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
throw await apiErrorFrom(response);
|
|
97
|
+
if (!response.body)
|
|
98
|
+
throw new Error('Research events response has no body');
|
|
99
|
+
return response.body.pipeThrough(new TextDecoderStream());
|
|
100
|
+
}
|
|
87
101
|
// SDK has no PATCH and its delete() throws on 204.
|
|
88
102
|
async send(method, path, json) {
|
|
89
103
|
const response = await fetch(`${this.baseURL}${path}`, {
|
|
@@ -103,9 +117,10 @@ class TinyFishCliClient extends TinyFish {
|
|
|
103
117
|
async function apiErrorFrom(response) {
|
|
104
118
|
const text = await response.text().catch(() => '');
|
|
105
119
|
const body = parseJson(text);
|
|
106
|
-
const
|
|
120
|
+
const error = typeof body?.error === 'string' ? undefined : body?.error;
|
|
121
|
+
const message = [error?.message, body?.message].find((m) => typeof m === 'string') ??
|
|
107
122
|
(text || `HTTP ${response.status}`);
|
|
108
|
-
const code = typeof
|
|
123
|
+
const code = typeof error?.code === 'string' ? error.code : undefined;
|
|
109
124
|
return new ApiError(response.status, message, code);
|
|
110
125
|
}
|
|
111
126
|
function createSdkClient(apiKey, timeout, baseUrl) {
|
|
@@ -120,18 +135,29 @@ function createSdkClient(apiKey, timeout, baseUrl) {
|
|
|
120
135
|
...(timeout !== undefined ? { timeout } : {}),
|
|
121
136
|
});
|
|
122
137
|
}
|
|
123
|
-
function sdk(apiKey, call) {
|
|
124
|
-
|
|
138
|
+
function sdk(apiKey, call, timeout) {
|
|
139
|
+
const client = createSdkClient(apiKey, timeout);
|
|
140
|
+
return call(client).catch((error) => rethrowSdkError(error, client.baseURL));
|
|
125
141
|
}
|
|
126
|
-
function rethrowSdkError(error) {
|
|
142
|
+
function rethrowSdkError(error, baseURL) {
|
|
127
143
|
if (error instanceof APIStatusError) {
|
|
128
144
|
throw new ApiError(error.statusCode, error.message, error.code);
|
|
129
145
|
}
|
|
146
|
+
if (error instanceof APIConnectionError || (error instanceof TypeError && error.cause)) {
|
|
147
|
+
throw new Error(networkMessage(error, baseURL));
|
|
148
|
+
}
|
|
130
149
|
if (error instanceof Error) {
|
|
131
150
|
throw error;
|
|
132
151
|
}
|
|
133
152
|
throw new Error(String(error));
|
|
134
153
|
}
|
|
154
|
+
// Node's fetch keeps the reason (ECONNREFUSED, ENOTFOUND) in cause.
|
|
155
|
+
function networkMessage(error, baseURL) {
|
|
156
|
+
const cause = z.looseObject({ code: z.string() }).safeParse(error.cause).data?.code;
|
|
157
|
+
const detail = cause ?? (error.cause instanceof Error ? error.cause.message : undefined);
|
|
158
|
+
const reason = detail ? `${error.message} (${detail})` : error.message;
|
|
159
|
+
return baseURL ? `Cannot reach ${new URL(baseURL).host}: ${reason}` : reason;
|
|
160
|
+
}
|
|
135
161
|
const cancelRunResponseSchema = z.object({
|
|
136
162
|
run_id: z.string(),
|
|
137
163
|
status: z.string(),
|
|
@@ -172,12 +198,14 @@ const runStepsResponseSchema = z.object({
|
|
|
172
198
|
status: runStatusSchema,
|
|
173
199
|
steps: z.array(runStepSchema),
|
|
174
200
|
});
|
|
175
|
-
|
|
201
|
+
// Zod's own message is a JSON dump of every issue.
|
|
202
|
+
export function parseWithSchema(schema, value, message) {
|
|
176
203
|
const result = schema.safeParse(value);
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
if (result.success)
|
|
205
|
+
return result.data;
|
|
206
|
+
const issues = result.error.issues.map((i) => [...i.path.map(String), i.message].join(': '));
|
|
207
|
+
const more = issues.length > 3 ? `; +${issues.length - 3} more` : '';
|
|
208
|
+
throw new Error(`${message}: ${issues.slice(0, 3).join('; ')}${more}`);
|
|
181
209
|
}
|
|
182
210
|
function normalizeStreamEvent(event) {
|
|
183
211
|
if (typeof event !== 'object' ||
|
|
@@ -587,3 +615,48 @@ export function vaultSync(apiKey) {
|
|
|
587
615
|
return parseWithSchema(vaultItemsSyncResponseSchema, response, 'Invalid vault sync response');
|
|
588
616
|
});
|
|
589
617
|
}
|
|
618
|
+
// A run may exist; never lose its ID.
|
|
619
|
+
function parseResearch(schema, body) {
|
|
620
|
+
const id = z.looseObject({ research_run_id: research.idSchema }).safeParse(body).data;
|
|
621
|
+
const suffix = id ? ` for ${id.research_run_id}` : '';
|
|
622
|
+
return parseWithSchema(schema, body, `Invalid research response${suffix}`);
|
|
623
|
+
}
|
|
624
|
+
export const parseResearchId = (id) => parseWithSchema(research.idSchema, id, 'Invalid research ID');
|
|
625
|
+
const runPath = (id) => `/v1/research-run/${parseResearchId(id)}`;
|
|
626
|
+
export function startResearchAsync(req, apiKey) {
|
|
627
|
+
return sdk(apiKey, async (client) => parseResearch(research.acceptedSchema, await client.post('/v1/automation/run-research-async', {
|
|
628
|
+
json: research.requestSchema.parse(req),
|
|
629
|
+
})), research.START_TIMEOUT_MS);
|
|
630
|
+
}
|
|
631
|
+
export function getResearch(id, apiKey) {
|
|
632
|
+
return sdk(apiKey, async (client) => parseResearch(research.detailSchema, await client.get(runPath(id))), research.READ_TIMEOUT_MS);
|
|
633
|
+
}
|
|
634
|
+
export function listResearch(opts, apiKey) {
|
|
635
|
+
return sdk(apiKey, async (client) => parseResearch(research.listSchema, await client.get('/v1/research-run', { params: research.listQuerySchema.parse(opts) })), research.READ_TIMEOUT_MS);
|
|
636
|
+
}
|
|
637
|
+
export function cancelResearch(id, apiKey) {
|
|
638
|
+
return sdk(apiKey, async (client) => parseResearch(research.cancelSchema, await client.post(`${runPath(id)}/cancel`)), research.CANCEL_TIMEOUT_MS);
|
|
639
|
+
}
|
|
640
|
+
export async function* researchStream(input, apiKey, signal) {
|
|
641
|
+
let stream;
|
|
642
|
+
const client = createSdkClient(apiKey, research.RESEARCH_TIMEOUT_MS);
|
|
643
|
+
try {
|
|
644
|
+
stream =
|
|
645
|
+
typeof input === 'string'
|
|
646
|
+
? await client.researchEvents(`${runPath(input)}/events`, signal)
|
|
647
|
+
: await client.postStream('/v1/automation/run-research', {
|
|
648
|
+
json: research.requestSchema.parse(input),
|
|
649
|
+
signal,
|
|
650
|
+
});
|
|
651
|
+
for await (const event of parseSseStream(stream, () => {
|
|
652
|
+
throw new Error('Invalid research event JSON');
|
|
653
|
+
}))
|
|
654
|
+
yield parseWithSchema(research.eventSchema, event, 'Invalid research event');
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
rethrowSdkError(error, client.baseURL);
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
await stream?.cancel().catch(() => { });
|
|
661
|
+
}
|
|
662
|
+
}
|
package/dist/lib/list-options.js
CHANGED
|
@@ -3,7 +3,8 @@ import { out, outLine } from './output.js';
|
|
|
3
3
|
const MAX_PAGE_SIZE = 1000;
|
|
4
4
|
export function integerInRange(min, max) {
|
|
5
5
|
return (value) => {
|
|
6
|
-
|
|
6
|
+
// Number() also accepts 0x10, 1e3 and blank strings.
|
|
7
|
+
const parsed = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
7
8
|
if (!Number.isSafeInteger(parsed) || parsed < min || (max !== undefined && parsed > max)) {
|
|
8
9
|
throw new InvalidArgumentError(max === undefined
|
|
9
10
|
? 'must be a positive integer'
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const RESEARCH_TIMEOUT_MS = 2700000;
|
|
3
|
+
export declare const START_TIMEOUT_MS = 30000;
|
|
4
|
+
export declare const READ_TIMEOUT_MS = 15000;
|
|
5
|
+
export declare const CANCEL_TIMEOUT_MS = 5000;
|
|
6
|
+
export declare const requestSchema: z.ZodObject<{
|
|
7
|
+
query: z.ZodString;
|
|
8
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
deep: "deep";
|
|
10
|
+
max: "max";
|
|
11
|
+
}>>;
|
|
12
|
+
output_language: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
export type Request = z.infer<typeof requestSchema>;
|
|
15
|
+
export declare const idSchema: z.ZodUUID;
|
|
16
|
+
export declare const statusSchema: z.ZodEnum<{
|
|
17
|
+
RUNNING: "RUNNING";
|
|
18
|
+
COMPLETED: "COMPLETED";
|
|
19
|
+
FAILED: "FAILED";
|
|
20
|
+
CANCELLED: "CANCELLED";
|
|
21
|
+
QUEUED: "QUEUED";
|
|
22
|
+
TIMED_OUT: "TIMED_OUT";
|
|
23
|
+
}>;
|
|
24
|
+
export declare const acceptedSchema: z.ZodObject<{
|
|
25
|
+
research_run_id: z.ZodUUID;
|
|
26
|
+
error: z.ZodNullable<z.ZodObject<{
|
|
27
|
+
code: z.ZodString;
|
|
28
|
+
message: z.ZodString;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
}, z.core.$loose>;
|
|
31
|
+
export declare const reportSchema: z.ZodObject<{
|
|
32
|
+
result: z.ZodString;
|
|
33
|
+
citations: z.ZodArray<z.ZodString>;
|
|
34
|
+
}, z.core.$loose>;
|
|
35
|
+
export type Report = z.infer<typeof reportSchema>;
|
|
36
|
+
export declare const runSchema: z.ZodObject<{
|
|
37
|
+
research_run_id: z.ZodUUID;
|
|
38
|
+
status: z.ZodEnum<{
|
|
39
|
+
RUNNING: "RUNNING";
|
|
40
|
+
COMPLETED: "COMPLETED";
|
|
41
|
+
FAILED: "FAILED";
|
|
42
|
+
CANCELLED: "CANCELLED";
|
|
43
|
+
QUEUED: "QUEUED";
|
|
44
|
+
TIMED_OUT: "TIMED_OUT";
|
|
45
|
+
}>;
|
|
46
|
+
query: z.ZodString;
|
|
47
|
+
quick_result: z.ZodNullable<z.ZodObject<{
|
|
48
|
+
answer: z.ZodString;
|
|
49
|
+
citations: z.ZodArray<z.ZodString>;
|
|
50
|
+
}, z.core.$loose>>;
|
|
51
|
+
quick_result_summary: z.ZodNullable<z.ZodString>;
|
|
52
|
+
deep_result: z.ZodNullable<z.ZodObject<{
|
|
53
|
+
result: z.ZodString;
|
|
54
|
+
citations: z.ZodArray<z.ZodString>;
|
|
55
|
+
}, z.core.$loose>>;
|
|
56
|
+
run_ids: z.ZodArray<z.ZodString>;
|
|
57
|
+
created_at: z.ZodNullable<z.ZodString>;
|
|
58
|
+
completed_at: z.ZodNullable<z.ZodString>;
|
|
59
|
+
}, z.core.$loose>;
|
|
60
|
+
export type Run = z.infer<typeof runSchema>;
|
|
61
|
+
export declare const detailSchema: z.ZodObject<{
|
|
62
|
+
research_run_id: z.ZodUUID;
|
|
63
|
+
status: z.ZodEnum<{
|
|
64
|
+
RUNNING: "RUNNING";
|
|
65
|
+
COMPLETED: "COMPLETED";
|
|
66
|
+
FAILED: "FAILED";
|
|
67
|
+
CANCELLED: "CANCELLED";
|
|
68
|
+
QUEUED: "QUEUED";
|
|
69
|
+
TIMED_OUT: "TIMED_OUT";
|
|
70
|
+
}>;
|
|
71
|
+
query: z.ZodString;
|
|
72
|
+
quick_result: z.ZodNullable<z.ZodObject<{
|
|
73
|
+
answer: z.ZodString;
|
|
74
|
+
citations: z.ZodArray<z.ZodString>;
|
|
75
|
+
}, z.core.$loose>>;
|
|
76
|
+
quick_result_summary: z.ZodNullable<z.ZodString>;
|
|
77
|
+
deep_result: z.ZodNullable<z.ZodObject<{
|
|
78
|
+
result: z.ZodString;
|
|
79
|
+
citations: z.ZodArray<z.ZodString>;
|
|
80
|
+
}, z.core.$loose>>;
|
|
81
|
+
run_ids: z.ZodArray<z.ZodString>;
|
|
82
|
+
created_at: z.ZodNullable<z.ZodString>;
|
|
83
|
+
completed_at: z.ZodNullable<z.ZodString>;
|
|
84
|
+
error: z.ZodNullable<z.ZodObject<{
|
|
85
|
+
code: z.ZodString;
|
|
86
|
+
message: z.ZodString;
|
|
87
|
+
}, z.core.$strip>>;
|
|
88
|
+
research_plan: z.ZodNullable<z.ZodJSONSchema>;
|
|
89
|
+
evidence: z.ZodNullable<z.ZodArray<z.ZodJSONSchema>>;
|
|
90
|
+
iterations: z.ZodNullable<z.ZodNumber>;
|
|
91
|
+
}, z.core.$loose>;
|
|
92
|
+
export declare const listQuerySchema: z.ZodObject<{
|
|
93
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
94
|
+
RUNNING: "RUNNING";
|
|
95
|
+
COMPLETED: "COMPLETED";
|
|
96
|
+
FAILED: "FAILED";
|
|
97
|
+
CANCELLED: "CANCELLED";
|
|
98
|
+
QUEUED: "QUEUED";
|
|
99
|
+
TIMED_OUT: "TIMED_OUT";
|
|
100
|
+
}>>;
|
|
101
|
+
query: z.ZodOptional<z.ZodString>;
|
|
102
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
103
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
104
|
+
sort_direction: z.ZodOptional<z.ZodEnum<{
|
|
105
|
+
asc: "asc";
|
|
106
|
+
desc: "desc";
|
|
107
|
+
}>>;
|
|
108
|
+
}, z.core.$strip>;
|
|
109
|
+
export type ListQuery = z.infer<typeof listQuerySchema>;
|
|
110
|
+
export declare const listSchema: z.ZodObject<{
|
|
111
|
+
data: z.ZodArray<z.ZodObject<{
|
|
112
|
+
research_run_id: z.ZodUUID;
|
|
113
|
+
status: z.ZodEnum<{
|
|
114
|
+
RUNNING: "RUNNING";
|
|
115
|
+
COMPLETED: "COMPLETED";
|
|
116
|
+
FAILED: "FAILED";
|
|
117
|
+
CANCELLED: "CANCELLED";
|
|
118
|
+
QUEUED: "QUEUED";
|
|
119
|
+
TIMED_OUT: "TIMED_OUT";
|
|
120
|
+
}>;
|
|
121
|
+
query: z.ZodString;
|
|
122
|
+
quick_result: z.ZodNullable<z.ZodObject<{
|
|
123
|
+
answer: z.ZodString;
|
|
124
|
+
citations: z.ZodArray<z.ZodString>;
|
|
125
|
+
}, z.core.$loose>>;
|
|
126
|
+
quick_result_summary: z.ZodNullable<z.ZodString>;
|
|
127
|
+
deep_result: z.ZodNullable<z.ZodObject<{
|
|
128
|
+
result: z.ZodString;
|
|
129
|
+
citations: z.ZodArray<z.ZodString>;
|
|
130
|
+
}, z.core.$loose>>;
|
|
131
|
+
run_ids: z.ZodArray<z.ZodString>;
|
|
132
|
+
created_at: z.ZodNullable<z.ZodString>;
|
|
133
|
+
completed_at: z.ZodNullable<z.ZodString>;
|
|
134
|
+
}, z.core.$loose>>;
|
|
135
|
+
pagination: z.ZodObject<{
|
|
136
|
+
total: z.ZodNumber;
|
|
137
|
+
next_cursor: z.ZodNullable<z.ZodString>;
|
|
138
|
+
has_more: z.ZodBoolean;
|
|
139
|
+
}, z.core.$loose>;
|
|
140
|
+
}, z.core.$loose>;
|
|
141
|
+
export declare const cancelSchema: z.ZodObject<{
|
|
142
|
+
research_run_id: z.ZodUUID;
|
|
143
|
+
status: z.ZodEnum<{
|
|
144
|
+
RUNNING: "RUNNING";
|
|
145
|
+
COMPLETED: "COMPLETED";
|
|
146
|
+
FAILED: "FAILED";
|
|
147
|
+
CANCELLED: "CANCELLED";
|
|
148
|
+
QUEUED: "QUEUED";
|
|
149
|
+
TIMED_OUT: "TIMED_OUT";
|
|
150
|
+
}>;
|
|
151
|
+
cancelled: z.ZodBoolean;
|
|
152
|
+
}, z.core.$loose>;
|
|
153
|
+
export declare const eventSchema: z.ZodObject<{
|
|
154
|
+
event: z.ZodString;
|
|
155
|
+
research_run_id: z.ZodOptional<z.ZodUUID>;
|
|
156
|
+
message: z.ZodOptional<z.ZodString>;
|
|
157
|
+
}, z.core.$loose>;
|
|
158
|
+
export type Event = z.infer<typeof eventSchema>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Research can exceed the SDK's ten-minute timeout.
|
|
3
|
+
export const RESEARCH_TIMEOUT_MS = 2_700_000;
|
|
4
|
+
export const START_TIMEOUT_MS = 30_000;
|
|
5
|
+
export const READ_TIMEOUT_MS = 15_000;
|
|
6
|
+
export const CANCEL_TIMEOUT_MS = 5_000;
|
|
7
|
+
export const requestSchema = z.object({
|
|
8
|
+
query: z.string().trim().min(1).max(2000),
|
|
9
|
+
mode: z.enum(['deep', 'max']).optional(),
|
|
10
|
+
output_language: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
export const idSchema = z.uuid();
|
|
13
|
+
export const statusSchema = z.enum([
|
|
14
|
+
'QUEUED',
|
|
15
|
+
'RUNNING',
|
|
16
|
+
'COMPLETED',
|
|
17
|
+
'FAILED',
|
|
18
|
+
'CANCELLED',
|
|
19
|
+
'TIMED_OUT',
|
|
20
|
+
]);
|
|
21
|
+
const errorSchema = z.object({ code: z.string(), message: z.string() }).nullable();
|
|
22
|
+
export const acceptedSchema = z.looseObject({
|
|
23
|
+
research_run_id: idSchema,
|
|
24
|
+
error: errorSchema,
|
|
25
|
+
});
|
|
26
|
+
export const reportSchema = z.looseObject({ result: z.string(), citations: z.array(z.string()) });
|
|
27
|
+
export const runSchema = z.looseObject({
|
|
28
|
+
research_run_id: idSchema,
|
|
29
|
+
status: statusSchema,
|
|
30
|
+
query: z.string(),
|
|
31
|
+
quick_result: z.looseObject({ answer: z.string(), citations: z.array(z.string()) }).nullable(),
|
|
32
|
+
quick_result_summary: z.string().nullable(),
|
|
33
|
+
deep_result: reportSchema.nullable(),
|
|
34
|
+
run_ids: z.array(z.string()),
|
|
35
|
+
created_at: z.string().nullable(),
|
|
36
|
+
completed_at: z.string().nullable(),
|
|
37
|
+
});
|
|
38
|
+
export const detailSchema = runSchema.extend({
|
|
39
|
+
error: errorSchema,
|
|
40
|
+
research_plan: z.json().nullable(),
|
|
41
|
+
evidence: z.array(z.json()).nullable(),
|
|
42
|
+
iterations: z.number().int().nonnegative().nullable(),
|
|
43
|
+
});
|
|
44
|
+
export const listQuerySchema = z.object({
|
|
45
|
+
status: statusSchema.optional(),
|
|
46
|
+
query: z.string().max(500).optional(),
|
|
47
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
48
|
+
cursor: z.string().optional(),
|
|
49
|
+
sort_direction: z.enum(['asc', 'desc']).optional(),
|
|
50
|
+
});
|
|
51
|
+
export const listSchema = z.looseObject({
|
|
52
|
+
data: z.array(runSchema),
|
|
53
|
+
pagination: z.looseObject({
|
|
54
|
+
total: z.number(),
|
|
55
|
+
next_cursor: z.string().nullable(),
|
|
56
|
+
has_more: z.boolean(),
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
export const cancelSchema = z.looseObject({
|
|
60
|
+
research_run_id: idSchema,
|
|
61
|
+
status: statusSchema,
|
|
62
|
+
cancelled: z.boolean(),
|
|
63
|
+
});
|
|
64
|
+
export const eventSchema = z
|
|
65
|
+
.looseObject({
|
|
66
|
+
event: z.string().min(1),
|
|
67
|
+
research_run_id: idSchema.optional(),
|
|
68
|
+
message: z.string().optional(),
|
|
69
|
+
})
|
|
70
|
+
.superRefine((event, ctx) => {
|
|
71
|
+
if (event.event === 'final_result' && !reportSchema.safeParse(event).success)
|
|
72
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid final research report' });
|
|
73
|
+
const required = { created: 'research_run_id', error: 'message' };
|
|
74
|
+
for (const [name, field] of Object.entries(required)) {
|
|
75
|
+
if (event.event === name && event[field] === undefined)
|
|
76
|
+
ctx.addIssue({ code: 'custom', message: `Missing ${field}`, path: [field] });
|
|
77
|
+
}
|
|
78
|
+
});
|
package/dist/lib/signals.d.ts
CHANGED
|
@@ -10,3 +10,6 @@ export type TerminalSignal = keyof typeof SIGNAL_EXIT_CODES;
|
|
|
10
10
|
* exits rather than waiting on it.
|
|
11
11
|
*/
|
|
12
12
|
export declare function installSignalGuard(onSignal: (signal: TerminalSignal) => Promise<void> | void): () => void;
|
|
13
|
+
/** Registers cleanup for a closed reader (`| head`); returns its remover. */
|
|
14
|
+
export declare function onBrokenPipe(handler: () => Promise<void> | void): () => void;
|
|
15
|
+
export declare function handleBrokenPipe(): void;
|
package/dist/lib/signals.js
CHANGED
|
@@ -25,3 +25,22 @@ export function installSignalGuard(onSignal) {
|
|
|
25
25
|
process.off(signal, handler);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
let brokenPipeHandler;
|
|
29
|
+
/** Registers cleanup for a closed reader (`| head`); returns its remover. */
|
|
30
|
+
export function onBrokenPipe(handler) {
|
|
31
|
+
brokenPipeHandler = handler;
|
|
32
|
+
return () => {
|
|
33
|
+
if (brokenPipeHandler === handler)
|
|
34
|
+
brokenPipeHandler = undefined;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let pipeClosed = false;
|
|
38
|
+
// Disconnecting never cancels a run server-side; the handler must.
|
|
39
|
+
export function handleBrokenPipe() {
|
|
40
|
+
if (pipeClosed)
|
|
41
|
+
return;
|
|
42
|
+
pipeClosed = true;
|
|
43
|
+
void Promise.resolve(brokenPipeHandler?.())
|
|
44
|
+
.catch(() => { })
|
|
45
|
+
.finally(() => process.exit(0));
|
|
46
|
+
}
|
package/dist/program.js
CHANGED
|
@@ -24,6 +24,7 @@ const GROUPS = {
|
|
|
24
24
|
(await import('./commands/runs.js')).registerRuns(runCmd);
|
|
25
25
|
(await import('./commands/batch.js')).registerBatch(agentCmd);
|
|
26
26
|
},
|
|
27
|
+
research: async (p) => (await import('./commands/research.js')).registerResearch(p),
|
|
27
28
|
search: async (p) => (await import('./commands/search.js')).registerSearch(p),
|
|
28
29
|
fetch: async (p) => (await import('./commands/fetch.js')).registerFetch(p),
|
|
29
30
|
monitor: async (p) => (await import('./commands/monitor.js')).registerMonitor(p),
|