@margay/ccl-core 1.4.5-rc.8 → 1.4.6-beta.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/QUICKSTART.md +6 -5
- package/README.md +7 -6
- package/dist/cli.js +9488 -6652
- package/dist/sdk/index.d.ts +3 -1
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/query.d.ts +2 -29
- package/dist/sdk/query.js +52 -14
- package/dist/sdk/queryHandle.d.ts +4 -4
- package/dist/sdk/queryHandle.js +44 -9
- package/dist/sdk/queryInput.d.ts +13 -0
- package/dist/sdk/queryInput.js +70 -0
- package/dist/sdk/requestProgress.d.ts +14 -0
- package/dist/sdk/requestProgress.js +26 -0
- package/dist/sdk/session.d.ts +48 -0
- package/dist/sdk/session.js +167 -0
- package/dist/sdk/types.d.ts +24 -1
- package/docs/ccl-duo-agent.md +2 -0
- package/docs/cli-installation.md +86 -0
- package/docs/context-checkpoints-and-daemon.md +54 -0
- package/docs/error-recovery-usage.md +29 -0
- package/docs/gateway-quickstart.md +52 -0
- package/docs/releases/1.4.5-rc.10.md +50 -0
- package/docs/releases/1.4.5-rc.9.md +22 -0
- package/docs/releases/1.4.6-beta.0.md +54 -0
- package/package.json +10 -4
- package/sdk/API.md +35 -3
- package/sdk/README.md +94 -0
package/dist/sdk/index.d.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { buildCliArgs, buildCliEnv, decideFailClosed, getSessionUsage, prepareSkillsWrappers, queryWithTransport, resumeWithDecision, } from './query.js';
|
|
9
9
|
export { query, queryHandleWithTransport } from './queryHandle.js';
|
|
10
|
+
export { createSession, Session, type SessionOptions, type SessionControls, type Turn, type TurnResult } from './session.js';
|
|
10
11
|
export { createSdkMcpServer, tool, SdkMcpRegistry, type SdkMcpToolDefinition, type McpSdkServerConfigWithInstance, } from './mcpServer.js';
|
|
11
12
|
export { defaultStoreDir, fileSessionStore } from './store.js';
|
|
12
13
|
export { spawnCliTransport } from './transport.js';
|
|
13
14
|
export type { RecoveryDiagnostic } from './errorDiagnostics.js';
|
|
14
|
-
export type {
|
|
15
|
+
export type { GatewayRequestPhase, GatewayRequestProgress } from './requestProgress.js';
|
|
16
|
+
export type { AccumulatedUsage, CanUseToolContext, CanUseToolFn, CanUseToolResult, InitializationResult, LifecycleHooks, PendingApproval, PendingDecision, ProviderConfig, Query, QueryOptions, SDKEvent, SDKUserMessage, SessionStore, Transport, Usage, WireMessage, } from './types.js';
|
package/dist/sdk/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { buildCliArgs, buildCliEnv, decideFailClosed, getSessionUsage, prepareSkillsWrappers, queryWithTransport, resumeWithDecision, } from './query.js';
|
|
9
9
|
export { query, queryHandleWithTransport } from './queryHandle.js';
|
|
10
|
+
export { createSession, Session } from './session.js';
|
|
10
11
|
export { createSdkMcpServer, tool, SdkMcpRegistry, } from './mcpServer.js';
|
|
11
12
|
export { defaultStoreDir, fileSessionStore } from './store.js';
|
|
12
13
|
export { spawnCliTransport } from './transport.js';
|
package/dist/sdk/query.d.ts
CHANGED
|
@@ -1,32 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ccl-sdk query() — the programmatic headless entry (issue P0-1) with the
|
|
3
|
-
* canUseTool permission callback (P0-2 including pending suspend/resume),
|
|
4
|
-
* session store integration (P0-3), provider mapping (P0-4), skills dirs
|
|
5
|
-
* (P1-5), lifecycle hooks (P1-6), and usage accumulation (P1-7).
|
|
6
|
-
*
|
|
7
|
-
* Architecture: the SDK is the HOST side of the stream-json control protocol
|
|
8
|
-
* that cli/print.ts already implements. We run the CLI as a managed
|
|
9
|
-
* subprocess (explicitly allowed by the issue's "进程内(或受管子进程)"),
|
|
10
|
-
* which keeps every user-visible CLI behavior — tools, skills, hooks, session
|
|
11
|
-
* persistence, gateway routing — byte-identical between CLI and SDK use.
|
|
12
|
-
*
|
|
13
|
-
* Pending suspend/resume mechanism (P0-2, zero CLI changes):
|
|
14
|
-
* - Suspend: on a `pending` callback result we persist the approval record,
|
|
15
|
-
* terminate the subprocess WITHOUT answering the can_use_tool request, and
|
|
16
|
-
* end the stream with a `suspended` event. The transcript on disk ends at
|
|
17
|
-
* the assistant tool_use whose tool never ran.
|
|
18
|
-
* - Resume: resumeWithDecision() relaunches with --resume <sessionId> and
|
|
19
|
-
* CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1. The CLI's resume path drops the
|
|
20
|
-
* unresolved tool_use (filterUnresolvedToolUses) and auto-continues the
|
|
21
|
-
* interrupted turn (cli/print.ts:1195), so the model re-issues the tool
|
|
22
|
-
* call; the SDK intercepts the matching can_use_tool request and applies
|
|
23
|
-
* the recorded decision — on allow, with EXACTLY the approved input (the
|
|
24
|
-
* recorded pending input, or the host's updatedInput), regardless of what
|
|
25
|
-
* the model re-generated. Deny works symmetrically. One-shot: the record
|
|
26
|
-
* is deleted after it is applied.
|
|
27
|
-
*/
|
|
28
1
|
import { type InteractionResponse } from './interactionResponseSchema.js';
|
|
29
|
-
import type { CanUseToolFn, PendingDecision, QueryOptions, SDKEvent, SessionStore, Transport } from './types.js';
|
|
2
|
+
import type { CanUseToolFn, PendingDecision, QueryOptions, SDKEvent, SDKUserMessage, SessionStore, Transport } from './types.js';
|
|
30
3
|
/** Builds the CLI argv for a query. Exported for unit tests. */
|
|
31
4
|
export declare function buildCliArgs(options: QueryOptions): string[];
|
|
32
5
|
/** Builds subprocess env additions. Exported for unit tests. */
|
|
@@ -68,7 +41,7 @@ export declare function decideFailClosed(callback: CanUseToolFn, toolName: strin
|
|
|
68
41
|
* Core loop, transport-injectable for tests. Production callers use query()
|
|
69
42
|
* or resumeWithDecision().
|
|
70
43
|
*/
|
|
71
|
-
export declare function queryWithTransport(prompt: string | null
|
|
44
|
+
export declare function queryWithTransport(prompt: string | null | AsyncIterable<SDKUserMessage>, options: QueryOptions, transport: Transport): AsyncGenerator<SDKEvent>;
|
|
72
45
|
/** Spawns the managed CLI subprocess for a set of options. Shared with the P1 Query handle. */
|
|
73
46
|
export declare function spawnForOptions(options: QueryOptions): Transport;
|
|
74
47
|
/**
|
package/dist/sdk/query.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeRequestProgress } from './requestProgress.js';
|
|
1
2
|
/**
|
|
2
3
|
* ccl-sdk query() — the programmatic headless entry (issue P0-1) with the
|
|
3
4
|
* canUseTool permission callback (P0-2 including pending suspend/resume),
|
|
@@ -34,6 +35,7 @@ import { fileURLToPath } from 'url';
|
|
|
34
35
|
import { SdkMcpRegistry } from './mcpServer.js';
|
|
35
36
|
import { fileSessionStore } from './store.js';
|
|
36
37
|
import { spawnCliTransport } from './transport.js';
|
|
38
|
+
import { isAsyncInput, readWithInput } from './queryInput.js';
|
|
37
39
|
const DEFAULT_CAN_USE_TOOL_TIMEOUT_MS = 300_000;
|
|
38
40
|
/** Builds the CLI argv for a query. Exported for unit tests. */
|
|
39
41
|
export function buildCliArgs(options) {
|
|
@@ -89,6 +91,8 @@ export function buildCliArgs(options) {
|
|
|
89
91
|
args.push('--include-partial-messages');
|
|
90
92
|
if (options.includeErrorDiagnostics)
|
|
91
93
|
args.push('--include-error-diagnostics');
|
|
94
|
+
if (options.includeRequestProgress)
|
|
95
|
+
args.push('--include-request-progress');
|
|
92
96
|
// slice sdk-input-external-mcp (G2): external MCP servers via --mcp-config (JSON string).
|
|
93
97
|
if (options.mcpServers && Object.keys(options.mcpServers).length > 0) {
|
|
94
98
|
args.push('--mcp-config', JSON.stringify({ mcpServers: options.mcpServers }));
|
|
@@ -253,6 +257,8 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
253
257
|
let turn = 0;
|
|
254
258
|
let sawResult = false;
|
|
255
259
|
let sessionEnded = false;
|
|
260
|
+
const streamingInput = isAsyncInput(prompt);
|
|
261
|
+
const inputState = { finished: false };
|
|
256
262
|
// resumeWithDecision path: the recorded approval this run must settle.
|
|
257
263
|
let pendingApproval = null;
|
|
258
264
|
let pendingDecision = options.pendingDecision;
|
|
@@ -267,6 +273,8 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
267
273
|
}
|
|
268
274
|
const onAbort = () => void transport.close();
|
|
269
275
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
276
|
+
if (options.signal?.aborted)
|
|
277
|
+
onAbort();
|
|
270
278
|
// slice sdk-in-process-mcp-server: host-defined in-process MCP servers, bridged to the subprocess
|
|
271
279
|
// agent over the control protocol. The registry replies to each `mcp_message` control_request with
|
|
272
280
|
// its server's JSONRPC response ({ mcp_response }). null when the host passed no sdkMcpServers.
|
|
@@ -307,7 +315,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
307
315
|
});
|
|
308
316
|
// On pending-decision resume the CLI auto-continues the interrupted turn
|
|
309
317
|
// (CLAUDE_CODE_RESUME_INTERRUPTED_TURN); a prompt would double-drive it.
|
|
310
|
-
if (prompt
|
|
318
|
+
if (typeof prompt === 'string' && !pendingDecision) {
|
|
311
319
|
// slice sdk-input-external-mcp (G6): multimodal input — host-provided content blocks replace
|
|
312
320
|
// the plain-text content when set; otherwise the string prompt is a single text block.
|
|
313
321
|
const userContent = options.promptContentBlocks && options.promptContentBlocks.length > 0
|
|
@@ -324,11 +332,29 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
324
332
|
}
|
|
325
333
|
// slice sdk-observability-hooks-usage (G5): fire onSessionStart once, on the first session_id.
|
|
326
334
|
let sessionStarted = false;
|
|
327
|
-
|
|
335
|
+
const messages = streamingInput
|
|
336
|
+
? readWithInput(prompt, transport, inputState, async (message) => {
|
|
337
|
+
sawResult = false;
|
|
338
|
+
const text = typeof message.message.content === 'string' ? message.message.content
|
|
339
|
+
: message.message.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
340
|
+
await runHook(hooks.onUserPromptSubmit && (() => hooks.onUserPromptSubmit({ prompt: text, sessionId: lastSessionId })));
|
|
341
|
+
})
|
|
342
|
+
: transport.read();
|
|
343
|
+
for await (const rawMessage of messages) {
|
|
328
344
|
// Keep ordinary raw passthrough unchanged. Diagnostic payloads are an
|
|
329
345
|
// untrusted error boundary even when a host asks for raw wire events.
|
|
330
|
-
|
|
331
|
-
|
|
346
|
+
if (rawMessage.type === 'system' && rawMessage.subtype === 'request_progress' && !options.includeRequestProgress)
|
|
347
|
+
continue;
|
|
348
|
+
const progress = rawMessage.type === 'system' && rawMessage.subtype === 'request_progress'
|
|
349
|
+
? sanitizeRequestProgress(rawMessage.progress) : undefined;
|
|
350
|
+
if (rawMessage.type === 'system' && rawMessage.subtype === 'request_progress' && !progress)
|
|
351
|
+
continue;
|
|
352
|
+
const message = progress
|
|
353
|
+
? { type: 'system', subtype: 'request_progress', progress,
|
|
354
|
+
...(typeof rawMessage.session_id === 'string' ? { session_id: rawMessage.session_id } : {}),
|
|
355
|
+
...(typeof rawMessage.uuid === 'string' ? { uuid: rawMessage.uuid } : {}) }
|
|
356
|
+
: rawMessage.type === 'system' && rawMessage.subtype === 'error_diagnostic'
|
|
357
|
+
? sanitizeErrorEnvelope(rawMessage) : rawMessage;
|
|
332
358
|
if (typeof message.session_id === 'string') {
|
|
333
359
|
lastSessionId = message.session_id;
|
|
334
360
|
if (!sessionStarted) {
|
|
@@ -341,6 +367,11 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
341
367
|
yield { type: 'raw', message };
|
|
342
368
|
switch (message.type) {
|
|
343
369
|
case 'system': {
|
|
370
|
+
if (message.subtype === 'request_progress' && progress)
|
|
371
|
+
yield {
|
|
372
|
+
type: 'request_progress', progress,
|
|
373
|
+
sessionId: typeof message.session_id === 'string' ? message.session_id : undefined,
|
|
374
|
+
};
|
|
344
375
|
if (message.subtype === 'error_diagnostic') {
|
|
345
376
|
const diagnostic = sanitizeRecoveryDiagnostic(message.diagnostic);
|
|
346
377
|
if (diagnostic)
|
|
@@ -615,6 +646,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
615
646
|
}
|
|
616
647
|
yield {
|
|
617
648
|
type: 'result',
|
|
649
|
+
...(streamingInput ? { turnId: inputState.turnId } : {}),
|
|
618
650
|
subtype: String(message.subtype ?? 'unknown'),
|
|
619
651
|
usage,
|
|
620
652
|
totalCostUsd,
|
|
@@ -634,14 +666,15 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
634
666
|
structuredOutput: message
|
|
635
667
|
.structured_output,
|
|
636
668
|
};
|
|
637
|
-
sessionEnded =
|
|
638
|
-
|
|
639
|
-
(
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
669
|
+
sessionEnded = !streamingInput;
|
|
670
|
+
if (!streamingInput)
|
|
671
|
+
await runHook(hooks.onSessionEnd &&
|
|
672
|
+
(() => hooks.onSessionEnd({
|
|
673
|
+
sessionId,
|
|
674
|
+
subtype: String(message.subtype ?? 'unknown'),
|
|
675
|
+
isError: message.is_error === true,
|
|
676
|
+
usage,
|
|
677
|
+
})));
|
|
645
678
|
break;
|
|
646
679
|
}
|
|
647
680
|
case 'stream_event': {
|
|
@@ -660,10 +693,15 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
660
693
|
default:
|
|
661
694
|
break;
|
|
662
695
|
}
|
|
663
|
-
if (sawResult)
|
|
696
|
+
if (sawResult && !streamingInput)
|
|
664
697
|
break;
|
|
665
698
|
}
|
|
666
|
-
if (
|
|
699
|
+
if (streamingInput && inputState.finished) {
|
|
700
|
+
await runHook(hooks.onSessionEnd && (() => hooks.onSessionEnd({
|
|
701
|
+
sessionId: lastSessionId, subtype: 'input_complete', isError: false,
|
|
702
|
+
})));
|
|
703
|
+
}
|
|
704
|
+
else if (!sawResult || streamingInput) {
|
|
667
705
|
const code = await transport.exited;
|
|
668
706
|
const aborted = options.signal?.aborted === true;
|
|
669
707
|
yield {
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
* supportedAgents()/accountInfo() serve it, and reinitialize() sends a new
|
|
24
24
|
* initialize (fresh request_id) and refreshes the cache.
|
|
25
25
|
*/
|
|
26
|
-
import type { Query, QueryOptions, Transport } from './types.js';
|
|
26
|
+
import type { Query, QueryOptions, SDKUserMessage, Transport } from './types.js';
|
|
27
27
|
/**
|
|
28
28
|
* Test-injection seam for the P1 Query handle, mirroring queryWithTransport.
|
|
29
29
|
* Production callers use query().
|
|
30
30
|
*/
|
|
31
|
-
export declare function queryHandleWithTransport(prompt: string | null
|
|
31
|
+
export declare function queryHandleWithTransport(prompt: string | null | AsyncIterable<SDKUserMessage>, options: QueryOptions, transport: Transport): Query;
|
|
32
32
|
/** @deprecated Use the object form query({ prompt, options }) instead. */
|
|
33
|
-
export declare function query(prompt: string | AsyncIterable<
|
|
33
|
+
export declare function query(prompt: string | AsyncIterable<SDKUserMessage>, options?: QueryOptions): Query;
|
|
34
34
|
/**
|
|
35
35
|
* Run one prompt against a CCL agent session and return a Query handle: the
|
|
36
36
|
* async stream of SDKEvents plus the control methods of the wire protocol.
|
|
37
37
|
*/
|
|
38
38
|
export declare function query(params: {
|
|
39
|
-
prompt: string
|
|
39
|
+
prompt: string | AsyncIterable<SDKUserMessage>;
|
|
40
40
|
options?: QueryOptions;
|
|
41
41
|
}): Query;
|
package/dist/sdk/queryHandle.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* initialize (fresh request_id) and refreshes the cache.
|
|
25
25
|
*/
|
|
26
26
|
import { queryWithTransport, spawnForOptions } from './query.js';
|
|
27
|
+
import { isAsyncInput } from './queryInput.js';
|
|
27
28
|
let requestCounter = 0;
|
|
28
29
|
const nextRequestId = () => `ccl-sdk-req-${++requestCounter}-${Math.random().toString(36).slice(2, 10)}`;
|
|
29
30
|
class QueryHandle {
|
|
@@ -66,7 +67,10 @@ class QueryHandle {
|
|
|
66
67
|
initPromise;
|
|
67
68
|
initResolve;
|
|
68
69
|
initReject;
|
|
70
|
+
streamingInput;
|
|
71
|
+
cancelPermission;
|
|
69
72
|
constructor(prompt, options, transport) {
|
|
73
|
+
this.streamingInput = isAsyncInput(prompt);
|
|
70
74
|
this.transport = transport;
|
|
71
75
|
this.options = options;
|
|
72
76
|
this.innerRead = transport.read()[Symbol.asyncIterator]();
|
|
@@ -77,16 +81,41 @@ class QueryHandle {
|
|
|
77
81
|
// The init promise is settled internally; hosts that never ask for the
|
|
78
82
|
// initialization result must not see an unhandled rejection.
|
|
79
83
|
this.initPromise.catch(() => { });
|
|
80
|
-
void this.
|
|
84
|
+
void transport.exited.then(() => this.cancelPermission?.(), () => this.cancelPermission?.());
|
|
85
|
+
const callback = options.canUseTool;
|
|
86
|
+
void this.pump(prompt, callback ? {
|
|
87
|
+
...options,
|
|
88
|
+
canUseTool: async (...args) => {
|
|
89
|
+
let cancel;
|
|
90
|
+
const cancelled = new Promise(resolve => {
|
|
91
|
+
cancel = () => resolve({ behavior: 'deny', message: 'Permission wait cancelled by host', interrupt: true });
|
|
92
|
+
});
|
|
93
|
+
this.cancelPermission = cancel;
|
|
94
|
+
try {
|
|
95
|
+
return await Promise.race([Promise.resolve().then(() => callback(...args)), cancelled]);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (this.cancelPermission === cancel)
|
|
99
|
+
this.cancelPermission = undefined;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
} : options);
|
|
81
103
|
}
|
|
82
104
|
// ------------------------------------------------------------- event pump
|
|
83
105
|
async pump(prompt, options) {
|
|
84
106
|
try {
|
|
85
107
|
for await (const event of queryWithTransport(prompt, options, this.tap())) {
|
|
108
|
+
// Long-lived streams must not retain unlimited output when the host
|
|
109
|
+
// stops reading. Fail explicitly rather than dropping agent events.
|
|
110
|
+
if (this.streamingInput && this.eventQueue.length >= 1024) {
|
|
111
|
+
await this.transport.close();
|
|
112
|
+
throw new Error('ccl-sdk: streamed query event buffer exceeded 1024 events; consume events as they arrive');
|
|
113
|
+
}
|
|
86
114
|
this.eventQueue.push(event);
|
|
87
115
|
if (event.type === 'result') {
|
|
88
116
|
this.resultDelivered = true;
|
|
89
|
-
this.
|
|
117
|
+
if (!this.streamingInput)
|
|
118
|
+
this.markEnded('ccl-sdk: query has ended (result delivered)');
|
|
90
119
|
}
|
|
91
120
|
this.notifyEventWaiters();
|
|
92
121
|
}
|
|
@@ -99,7 +128,7 @@ class QueryHandle {
|
|
|
99
128
|
// Normal completion (result) is a SOFT end — the child stays alive for
|
|
100
129
|
// post-run control requests until close(). Every other exit (stream
|
|
101
130
|
// ended, suspended, error) really closed the transport: hard end.
|
|
102
|
-
if (!this.resultDelivered) {
|
|
131
|
+
if (!this.resultDelivered || this.streamingInput) {
|
|
103
132
|
this.markDead('ccl-sdk: query has ended (transport closed)');
|
|
104
133
|
}
|
|
105
134
|
this.notifyEventWaiters();
|
|
@@ -130,7 +159,7 @@ class QueryHandle {
|
|
|
130
159
|
// requests after the result, which is what makes the post-run
|
|
131
160
|
// rewind_files round-trip possible. Abort/error paths (no result)
|
|
132
161
|
// close for real, exactly as before.
|
|
133
|
-
if (this.resultDelivered && !this.transportDead)
|
|
162
|
+
if (this.resultDelivered && !this.transportDead && !this.streamingInput)
|
|
134
163
|
return;
|
|
135
164
|
await inner.close();
|
|
136
165
|
},
|
|
@@ -153,7 +182,8 @@ class QueryHandle {
|
|
|
153
182
|
// The inner event loop stopped consuming (result delivered / early
|
|
154
183
|
// return). Keep observing control_responses on the SAME iterator so
|
|
155
184
|
// post-run control requests (rewind_files) still settle.
|
|
156
|
-
this.
|
|
185
|
+
if (!this.streamingInput)
|
|
186
|
+
this.startResidualReader();
|
|
157
187
|
}
|
|
158
188
|
}
|
|
159
189
|
/** Post-loop observer: drains the shared read iterator after the inner
|
|
@@ -259,6 +289,7 @@ class QueryHandle {
|
|
|
259
289
|
});
|
|
260
290
|
}
|
|
261
291
|
interrupt() {
|
|
292
|
+
this.cancelPermission?.();
|
|
262
293
|
return this.controlRequest({ subtype: 'interrupt' });
|
|
263
294
|
}
|
|
264
295
|
setPermissionMode(mode) {
|
|
@@ -341,6 +372,7 @@ class QueryHandle {
|
|
|
341
372
|
return (await this.initializationResult()).account;
|
|
342
373
|
}
|
|
343
374
|
async close() {
|
|
375
|
+
this.cancelPermission?.();
|
|
344
376
|
this.markDead('ccl-sdk: query closed');
|
|
345
377
|
await this.transport.close();
|
|
346
378
|
}
|
|
@@ -400,15 +432,18 @@ export function queryHandleWithTransport(prompt, options, transport) {
|
|
|
400
432
|
return new QueryHandle(prompt, options, transport);
|
|
401
433
|
}
|
|
402
434
|
export function query(promptOrParams, positionalOptions) {
|
|
403
|
-
const positional = typeof promptOrParams === 'string' ||
|
|
404
|
-
(typeof promptOrParams === 'object' &&
|
|
405
|
-
promptOrParams !== null &&
|
|
406
|
-
Symbol.asyncIterator in promptOrParams);
|
|
435
|
+
const positional = typeof promptOrParams === 'string' || isAsyncInput(promptOrParams);
|
|
407
436
|
const prompt = positional
|
|
408
437
|
? promptOrParams
|
|
409
438
|
: promptOrParams.prompt;
|
|
410
439
|
const options = (positional
|
|
411
440
|
? positionalOptions
|
|
412
441
|
: promptOrParams.options) ?? {};
|
|
442
|
+
if (typeof prompt !== 'string' && !isAsyncInput(prompt)) {
|
|
443
|
+
throw new TypeError('ccl-sdk: prompt must be a string or an AsyncIterable of user messages');
|
|
444
|
+
}
|
|
445
|
+
if (isAsyncInput(prompt) && options.pendingDecision) {
|
|
446
|
+
throw new TypeError('ccl-sdk: use resumeWithDecision to resolve a suspended approval before streaming new input');
|
|
447
|
+
}
|
|
413
448
|
return queryHandleWithTransport(prompt, options, spawnForOptions(options));
|
|
414
449
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SDKUserMessage, Transport, WireMessage } from './types.js';
|
|
2
|
+
export declare function isAsyncInput(value: unknown): value is AsyncIterable<SDKUserMessage>;
|
|
3
|
+
/** Validate at the public input boundary, before sending anything to the CLI. */
|
|
4
|
+
export declare function normalizeUserInput(value: unknown): SDKUserMessage;
|
|
5
|
+
export type InputState = {
|
|
6
|
+
finished: boolean;
|
|
7
|
+
turnId?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* One input is in flight at a time. Keep reading controls while the producer
|
|
11
|
+
* waits, without reading ahead into an unbounded queue of future prompts.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readWithInput(input: AsyncIterable<SDKUserMessage>, transport: Transport, state: InputState, submitted: (message: SDKUserMessage) => Promise<void>): AsyncGenerator<WireMessage>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export function isAsyncInput(value) {
|
|
3
|
+
return value !== null && typeof value === 'object' &&
|
|
4
|
+
typeof value[Symbol.asyncIterator] === 'function';
|
|
5
|
+
}
|
|
6
|
+
/** Validate at the public input boundary, before sending anything to the CLI. */
|
|
7
|
+
export function normalizeUserInput(value) {
|
|
8
|
+
const input = value;
|
|
9
|
+
if (!input || input.type !== 'user' || input.message?.role !== 'user') {
|
|
10
|
+
throw new TypeError('ccl-sdk: streamed input must be a user message with message.role="user"');
|
|
11
|
+
}
|
|
12
|
+
const content = input.message.content;
|
|
13
|
+
if (typeof content !== 'string' && !(Array.isArray(content) && content.every(block => block !== null && typeof block === 'object' && typeof block.type === 'string' &&
|
|
14
|
+
(block.type !== 'text' || typeof block.text === 'string')))) {
|
|
15
|
+
throw new TypeError('ccl-sdk: user message content must be text or content blocks');
|
|
16
|
+
}
|
|
17
|
+
if (input.uuid !== undefined && (typeof input.uuid !== 'string' || !input.uuid)) {
|
|
18
|
+
throw new TypeError('ccl-sdk: user message uuid must be a nonempty string');
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
type: 'user', uuid: input.uuid ?? randomUUID(),
|
|
22
|
+
message: { role: 'user', content }, parent_tool_use_id: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One input is in flight at a time. Keep reading controls while the producer
|
|
27
|
+
* waits, without reading ahead into an unbounded queue of future prompts.
|
|
28
|
+
*/
|
|
29
|
+
export async function* readWithInput(input, transport, state, submitted) {
|
|
30
|
+
const source = input[Symbol.asyncIterator]();
|
|
31
|
+
const wire = transport.read()[Symbol.asyncIterator]();
|
|
32
|
+
const pull = () => Promise.resolve().then(() => source.next()).then(value => ({ kind: 'input', value }), error => ({ kind: 'input-error', error }));
|
|
33
|
+
let pendingInput = pull();
|
|
34
|
+
let pendingWire;
|
|
35
|
+
try {
|
|
36
|
+
for (;;) {
|
|
37
|
+
pendingWire ??= wire.next().then(value => ({ kind: 'wire', value }));
|
|
38
|
+
const next = await Promise.race(pendingInput ? [pendingWire, pendingInput] : [pendingWire]);
|
|
39
|
+
if (next.kind === 'input-error')
|
|
40
|
+
throw next.error;
|
|
41
|
+
if (next.kind === 'input') {
|
|
42
|
+
pendingInput = undefined;
|
|
43
|
+
if (next.value.done) {
|
|
44
|
+
state.finished = true;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const message = normalizeUserInput(next.value.value);
|
|
48
|
+
state.turnId = message.uuid;
|
|
49
|
+
await transport.write(message);
|
|
50
|
+
await submitted(message);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
pendingWire = undefined;
|
|
54
|
+
if (next.value.done)
|
|
55
|
+
return;
|
|
56
|
+
yield next.value.value;
|
|
57
|
+
if (next.value.value.type === 'result') {
|
|
58
|
+
state.turnId = undefined;
|
|
59
|
+
pendingInput = pull();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
// A producer may be waiting on user code forever. Request cancellation,
|
|
66
|
+
// but never let an uncooperative iterator prevent subprocess cleanup.
|
|
67
|
+
void Promise.resolve().then(() => source.return?.()).catch(() => { });
|
|
68
|
+
void Promise.resolve().then(() => wire.return?.()).catch(() => { });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Nonterminal gateway status. It is not model output or permission to retry. */
|
|
2
|
+
export type GatewayRequestPhase = 'preparing' | 'connecting' | 'sending' | 'waiting_response' | 'response_headers' | 'retry_wait' | 'streaming' | 'finished';
|
|
3
|
+
export type GatewayRequestProgress = {
|
|
4
|
+
requestId: string;
|
|
5
|
+
phase: GatewayRequestPhase;
|
|
6
|
+
attempt: number;
|
|
7
|
+
elapsedMs: number;
|
|
8
|
+
/** Age of the last effective model event, never of a status notification. */
|
|
9
|
+
idleMs: number;
|
|
10
|
+
slow: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Allow-listed projection at the child-process boundary; never forwards arbitrary text. */
|
|
13
|
+
export declare function sanitizeRequestProgress(value: unknown): GatewayRequestProgress | undefined;
|
|
14
|
+
export declare function formatRequestProgress(event: GatewayRequestProgress): string | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const phases = new Set(['preparing', 'connecting', 'sending', 'waiting_response', 'response_headers', 'retry_wait', 'streaming', 'finished']);
|
|
2
|
+
/** Allow-listed projection at the child-process boundary; never forwards arbitrary text. */
|
|
3
|
+
export function sanitizeRequestProgress(value) {
|
|
4
|
+
if (!value || typeof value !== 'object')
|
|
5
|
+
return;
|
|
6
|
+
const v = value;
|
|
7
|
+
if (typeof v.requestId !== 'string' || !/^[a-zA-Z0-9_.:-]{1,128}$/.test(v.requestId) ||
|
|
8
|
+
!phases.has(v.phase) || typeof v.slow !== 'boolean')
|
|
9
|
+
return;
|
|
10
|
+
for (const name of ['attempt', 'elapsedMs', 'idleMs']) {
|
|
11
|
+
if (!Number.isSafeInteger(v[name]) || v[name] < 0)
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
return { requestId: v.requestId, phase: v.phase, attempt: v.attempt,
|
|
15
|
+
elapsedMs: v.elapsedMs, idleMs: v.idleMs, slow: v.slow };
|
|
16
|
+
}
|
|
17
|
+
export function formatRequestProgress(event) {
|
|
18
|
+
if (event.phase === 'finished' || (event.phase === 'streaming' && !event.slow))
|
|
19
|
+
return null;
|
|
20
|
+
const labels = {
|
|
21
|
+
preparing: 'Preparing request', connecting: 'Connecting to gateway', sending: 'Sending request',
|
|
22
|
+
waiting_response: 'Waiting for gateway response', response_headers: 'Reading gateway response',
|
|
23
|
+
retry_wait: 'Waiting to retry gateway request', streaming: 'Waiting for model progress',
|
|
24
|
+
};
|
|
25
|
+
return `${labels[event.phase]}${event.slow ? ` (${Math.floor(event.idleMs / 1000)}s without model progress; Esc to cancel)` : ''}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Query, QueryOptions, SDKEvent, SDKUserMessage, Transport } from './types.js';
|
|
2
|
+
export type TurnResult = Extract<SDKEvent, {
|
|
3
|
+
type: 'result';
|
|
4
|
+
}>;
|
|
5
|
+
export type SessionControls = Omit<Query, keyof AsyncGenerator<SDKEvent, void, unknown>>;
|
|
6
|
+
export type SessionOptions = QueryOptions & {
|
|
7
|
+
/** Maximum accepted but not yet completed turns, including the active turn. */
|
|
8
|
+
maxPendingTurns?: number;
|
|
9
|
+
/** Events are delivered as they arrive; Session does not retain an event history. */
|
|
10
|
+
onEvent?: (event: SDKEvent) => void;
|
|
11
|
+
};
|
|
12
|
+
export interface Turn {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly result: Promise<TurnResult>;
|
|
15
|
+
cancel(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** One CLI process, sequential turns, and a bounded host-side input queue. */
|
|
18
|
+
export declare class Session {
|
|
19
|
+
readonly pid: number | undefined;
|
|
20
|
+
/** Existing SDK controls, including initialization, model and task controls. */
|
|
21
|
+
readonly controls: SessionControls;
|
|
22
|
+
private readonly handle;
|
|
23
|
+
private readonly limit;
|
|
24
|
+
private readonly options;
|
|
25
|
+
private queue;
|
|
26
|
+
private active?;
|
|
27
|
+
private wake?;
|
|
28
|
+
private accepting;
|
|
29
|
+
private closed;
|
|
30
|
+
private drainWaiters;
|
|
31
|
+
private readonly pumping;
|
|
32
|
+
constructor(options?: SessionOptions, transport?: Transport);
|
|
33
|
+
prompt(content: SDKUserMessage['message']['content']): Turn;
|
|
34
|
+
followUp(content: SDKUserMessage['message']['content']): Turn;
|
|
35
|
+
/** Interrupt the active turn; correction runs before queued follow-ups. */
|
|
36
|
+
steer(content: SDKUserMessage['message']['content']): Turn;
|
|
37
|
+
private enqueue;
|
|
38
|
+
private inputs;
|
|
39
|
+
private consume;
|
|
40
|
+
private fail;
|
|
41
|
+
private notifyDrained;
|
|
42
|
+
interrupt(): Promise<void>;
|
|
43
|
+
private cancel;
|
|
44
|
+
/** Stop accepting inputs and drain accepted turns, keeping controls alive. */
|
|
45
|
+
endInput(): Promise<void>;
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare function createSession(options?: SessionOptions): Session;
|