@margay/ccl-core 1.4.5-rc.9 → 1.5.0-alpha.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 +13315 -9910
- 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 +84 -32
- package/dist/sdk/queryHandle.d.ts +4 -4
- package/dist/sdk/queryHandle.js +44 -9
- package/dist/sdk/queryInput.d.ts +14 -0
- package/dist/sdk/queryInput.js +82 -0
- package/dist/sdk/requestProgress.d.ts +14 -0
- package/dist/sdk/requestProgress.js +26 -0
- package/dist/sdk/session.d.ts +53 -0
- package/dist/sdk/session.js +222 -0
- package/dist/sdk/types.d.ts +57 -1
- package/docs/ccl-duo-agent.md +2 -0
- package/docs/cli-installation.md +86 -0
- package/docs/context-checkpoints-and-daemon.md +91 -0
- package/docs/gateway-quickstart.md +52 -0
- package/docs/releases/1.4.5-rc.10.md +50 -0
- package/docs/releases/1.4.6-beta.0.md +54 -0
- package/docs/releases/1.5.0-alpha.0.md +44 -0
- package/examples/assistant/README.md +23 -0
- package/examples/assistant/client.mjs +227 -0
- package/examples/assistant/office/SKILL.md +18 -0
- package/examples/assistant/office/build.py +113 -0
- package/examples/assistant/office/check.py +174 -0
- package/examples/assistant/office/render.py +99 -0
- package/examples/assistant/office/workflow.md +46 -0
- package/examples/assistant/terminal.mjs +60 -0
- package/package.json +11 -4
- package/sdk/API.md +51 -3
- package/sdk/README.md +101 -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, SessionLifecycleEvent, 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, normalizeUserInput, 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 }));
|
|
@@ -249,10 +253,13 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
249
253
|
const hooks = options.hooks ?? {};
|
|
250
254
|
const ownRequestIds = new Set();
|
|
251
255
|
const toolNamesById = new Map();
|
|
256
|
+
const toolOrigins = new Map();
|
|
252
257
|
let lastSessionId = options.sessionId ?? options.resume;
|
|
253
258
|
let turn = 0;
|
|
254
259
|
let sawResult = false;
|
|
255
260
|
let sessionEnded = false;
|
|
261
|
+
const streamingInput = isAsyncInput(prompt);
|
|
262
|
+
const inputState = { finished: false };
|
|
256
263
|
// resumeWithDecision path: the recorded approval this run must settle.
|
|
257
264
|
let pendingApproval = null;
|
|
258
265
|
let pendingDecision = options.pendingDecision;
|
|
@@ -267,6 +274,8 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
267
274
|
}
|
|
268
275
|
const onAbort = () => void transport.close();
|
|
269
276
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
277
|
+
if (options.signal?.aborted)
|
|
278
|
+
onAbort();
|
|
270
279
|
// slice sdk-in-process-mcp-server: host-defined in-process MCP servers, bridged to the subprocess
|
|
271
280
|
// agent over the control protocol. The registry replies to each `mcp_message` control_request with
|
|
272
281
|
// its server's JSONRPC response ({ mcp_response }). null when the host passed no sdkMcpServers.
|
|
@@ -307,28 +316,50 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
307
316
|
});
|
|
308
317
|
// On pending-decision resume the CLI auto-continues the interrupted turn
|
|
309
318
|
// (CLAUDE_CODE_RESUME_INTERRUPTED_TURN); a prompt would double-drive it.
|
|
310
|
-
if (prompt
|
|
319
|
+
if (typeof prompt === 'string' && !pendingDecision) {
|
|
311
320
|
// slice sdk-input-external-mcp (G6): multimodal input — host-provided content blocks replace
|
|
312
321
|
// the plain-text content when set; otherwise the string prompt is a single text block.
|
|
313
322
|
const userContent = options.promptContentBlocks && options.promptContentBlocks.length > 0
|
|
314
323
|
? options.promptContentBlocks
|
|
315
324
|
: [{ type: 'text', text: prompt }];
|
|
316
|
-
await transport.write({
|
|
325
|
+
await transport.write(normalizeUserInput({
|
|
317
326
|
type: 'user',
|
|
318
327
|
message: { role: 'user', content: userContent },
|
|
319
328
|
parent_tool_use_id: null,
|
|
320
|
-
});
|
|
329
|
+
}));
|
|
321
330
|
// slice sdk-observability-hooks-usage (G5): the host's prompt was submitted to the agent.
|
|
322
331
|
await runHook(hooks.onUserPromptSubmit &&
|
|
323
332
|
(() => hooks.onUserPromptSubmit({ prompt, sessionId: lastSessionId })));
|
|
324
333
|
}
|
|
325
334
|
// slice sdk-observability-hooks-usage (G5): fire onSessionStart once, on the first session_id.
|
|
326
335
|
let sessionStarted = false;
|
|
327
|
-
|
|
336
|
+
const messages = streamingInput
|
|
337
|
+
? readWithInput(prompt, transport, inputState, async (message) => {
|
|
338
|
+
sawResult = false;
|
|
339
|
+
const text = typeof message.message.content === 'string' ? message.message.content
|
|
340
|
+
: message.message.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
341
|
+
await runHook(hooks.onUserPromptSubmit && (() => hooks.onUserPromptSubmit({ prompt: text, sessionId: lastSessionId })));
|
|
342
|
+
})
|
|
343
|
+
: transport.read();
|
|
344
|
+
for await (const rawMessage of messages) {
|
|
345
|
+
let attribution = rawMessage.input_uuid === null
|
|
346
|
+
? { origin: 'background' } : typeof rawMessage.input_uuid === 'string'
|
|
347
|
+
? { origin: 'user', turnId: rawMessage.input_uuid }
|
|
348
|
+
: streamingInput && inputState.turnId ? { turnId: inputState.turnId } : {};
|
|
328
349
|
// Keep ordinary raw passthrough unchanged. Diagnostic payloads are an
|
|
329
350
|
// untrusted error boundary even when a host asks for raw wire events.
|
|
330
|
-
|
|
331
|
-
|
|
351
|
+
if (rawMessage.type === 'system' && rawMessage.subtype === 'request_progress' && !options.includeRequestProgress)
|
|
352
|
+
continue;
|
|
353
|
+
const progress = rawMessage.type === 'system' && rawMessage.subtype === 'request_progress'
|
|
354
|
+
? sanitizeRequestProgress(rawMessage.progress) : undefined;
|
|
355
|
+
if (rawMessage.type === 'system' && rawMessage.subtype === 'request_progress' && !progress)
|
|
356
|
+
continue;
|
|
357
|
+
const message = progress
|
|
358
|
+
? { type: 'system', subtype: 'request_progress', progress,
|
|
359
|
+
...(typeof rawMessage.session_id === 'string' ? { session_id: rawMessage.session_id } : {}),
|
|
360
|
+
...(typeof rawMessage.uuid === 'string' ? { uuid: rawMessage.uuid } : {}) }
|
|
361
|
+
: rawMessage.type === 'system' && rawMessage.subtype === 'error_diagnostic'
|
|
362
|
+
? sanitizeErrorEnvelope(rawMessage) : rawMessage;
|
|
332
363
|
if (typeof message.session_id === 'string') {
|
|
333
364
|
lastSessionId = message.session_id;
|
|
334
365
|
if (!sessionStarted) {
|
|
@@ -338,13 +369,18 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
338
369
|
}
|
|
339
370
|
}
|
|
340
371
|
if (options.includeRaw)
|
|
341
|
-
yield { type: 'raw', message };
|
|
372
|
+
yield { ...attribution, type: 'raw', message };
|
|
342
373
|
switch (message.type) {
|
|
343
374
|
case 'system': {
|
|
375
|
+
if (message.subtype === 'request_progress' && progress)
|
|
376
|
+
yield { ...attribution,
|
|
377
|
+
type: 'request_progress', progress,
|
|
378
|
+
sessionId: typeof message.session_id === 'string' ? message.session_id : undefined,
|
|
379
|
+
};
|
|
344
380
|
if (message.subtype === 'error_diagnostic') {
|
|
345
381
|
const diagnostic = sanitizeRecoveryDiagnostic(message.diagnostic);
|
|
346
382
|
if (diagnostic)
|
|
347
|
-
yield {
|
|
383
|
+
yield { ...attribution,
|
|
348
384
|
type: 'error_diagnostic', diagnostic,
|
|
349
385
|
sessionId: typeof message.session_id === 'string' ? message.session_id : undefined,
|
|
350
386
|
};
|
|
@@ -355,19 +391,19 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
355
391
|
const sessionId = message.session_id;
|
|
356
392
|
for (const block of contentBlocks(message.message)) {
|
|
357
393
|
if (block.type === 'text' && typeof block.text === 'string') {
|
|
358
|
-
yield { type: 'assistant_text', text: block.text, sessionId };
|
|
394
|
+
yield { ...attribution, type: 'assistant_text', text: block.text, sessionId };
|
|
359
395
|
}
|
|
360
396
|
else if (block.type === 'tool_use') {
|
|
361
397
|
const toolUseId = String(block.id ?? '');
|
|
362
398
|
const toolName = String(block.name ?? '');
|
|
363
399
|
toolNamesById.set(toolUseId, toolName);
|
|
364
|
-
|
|
400
|
+
toolOrigins.set(toolUseId, attribution);
|
|
401
|
+
yield { ...attribution,
|
|
365
402
|
type: 'tool_use',
|
|
366
403
|
toolName,
|
|
367
404
|
input: (block.input ?? {}),
|
|
368
405
|
toolUseId,
|
|
369
|
-
sessionId,
|
|
370
|
-
};
|
|
406
|
+
sessionId, };
|
|
371
407
|
}
|
|
372
408
|
}
|
|
373
409
|
turn += 1;
|
|
@@ -389,7 +425,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
389
425
|
: Array.isArray(block.content) ? block.content.map(part => part?.type === 'text' && typeof part.text === 'string' ? { ...part, text: sanitizeErrorText(part.text) } : part)
|
|
390
426
|
: describeError(block.content)
|
|
391
427
|
: block.content;
|
|
392
|
-
yield {
|
|
428
|
+
yield { ...attribution,
|
|
393
429
|
type: 'tool_result',
|
|
394
430
|
toolUseId,
|
|
395
431
|
content: resultContent,
|
|
@@ -404,6 +440,8 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
404
440
|
isError,
|
|
405
441
|
sessionId,
|
|
406
442
|
})));
|
|
443
|
+
toolOrigins.delete(toolUseId);
|
|
444
|
+
toolNamesById.delete(toolUseId);
|
|
407
445
|
}
|
|
408
446
|
}
|
|
409
447
|
break;
|
|
@@ -442,8 +480,9 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
442
480
|
const toolName = String(request.tool_name ?? '');
|
|
443
481
|
const input = (request.input ?? {});
|
|
444
482
|
const toolUseId = String(request.tool_use_id ?? '');
|
|
483
|
+
attribution = toolOrigins.get(toolUseId) ?? attribution;
|
|
445
484
|
const agentId = typeof request.agent_id === 'string' ? request.agent_id : undefined;
|
|
446
|
-
yield {
|
|
485
|
+
yield { ...attribution,
|
|
447
486
|
type: 'permission_request',
|
|
448
487
|
requestId,
|
|
449
488
|
toolName,
|
|
@@ -478,7 +517,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
478
517
|
await store.deletePendingApproval(pendingApproval.sessionId);
|
|
479
518
|
pendingDecision = undefined;
|
|
480
519
|
pendingApproval = null;
|
|
481
|
-
yield {
|
|
520
|
+
yield { ...attribution,
|
|
482
521
|
type: 'permission_resolved',
|
|
483
522
|
requestId,
|
|
484
523
|
toolUseId,
|
|
@@ -515,13 +554,13 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
515
554
|
},
|
|
516
555
|
},
|
|
517
556
|
});
|
|
518
|
-
yield {
|
|
557
|
+
yield { ...attribution,
|
|
519
558
|
type: 'error',
|
|
520
559
|
message: 'pending approval requested before any session_id was observed',
|
|
521
560
|
fatal: false,
|
|
522
561
|
code: 'pending_without_session',
|
|
523
562
|
};
|
|
524
|
-
yield {
|
|
563
|
+
yield { ...attribution,
|
|
525
564
|
type: 'permission_resolved',
|
|
526
565
|
requestId,
|
|
527
566
|
toolUseId,
|
|
@@ -543,7 +582,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
543
582
|
// the model must not see a denial. Terminating here leaves the
|
|
544
583
|
// transcript ending at the unresolved tool_use, which the
|
|
545
584
|
// resume path drops and re-drives.
|
|
546
|
-
yield {
|
|
585
|
+
yield { ...attribution,
|
|
547
586
|
type: 'suspended',
|
|
548
587
|
sessionId,
|
|
549
588
|
requestId,
|
|
@@ -568,7 +607,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
568
607
|
response: outcome.decision,
|
|
569
608
|
},
|
|
570
609
|
});
|
|
571
|
-
yield {
|
|
610
|
+
yield { ...attribution,
|
|
572
611
|
type: 'permission_resolved',
|
|
573
612
|
requestId,
|
|
574
613
|
toolUseId,
|
|
@@ -587,7 +626,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
587
626
|
if (ownRequestIds.has(requestId)) {
|
|
588
627
|
ownRequestIds.delete(requestId);
|
|
589
628
|
if (response.subtype === 'error') {
|
|
590
|
-
yield {
|
|
629
|
+
yield { ...attribution,
|
|
591
630
|
type: 'error',
|
|
592
631
|
message: `initialize failed: ${String(response.error)}`,
|
|
593
632
|
fatal: false,
|
|
@@ -613,8 +652,9 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
613
652
|
// Metering must not break the conversation stream.
|
|
614
653
|
}
|
|
615
654
|
}
|
|
616
|
-
yield {
|
|
655
|
+
yield { ...attribution,
|
|
617
656
|
type: 'result',
|
|
657
|
+
...(streamingInput ? { turnId: inputState.resultTurnId } : {}),
|
|
618
658
|
subtype: String(message.subtype ?? 'unknown'),
|
|
619
659
|
usage,
|
|
620
660
|
totalCostUsd,
|
|
@@ -634,14 +674,21 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
634
674
|
structuredOutput: message
|
|
635
675
|
.structured_output,
|
|
636
676
|
};
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
677
|
+
for (const [id, origin] of toolOrigins) {
|
|
678
|
+
if (origin.turnId === attribution.turnId && origin.origin === attribution.origin) {
|
|
679
|
+
toolOrigins.delete(id);
|
|
680
|
+
toolNamesById.delete(id);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
sessionEnded = !streamingInput;
|
|
684
|
+
if (!streamingInput)
|
|
685
|
+
await runHook(hooks.onSessionEnd &&
|
|
686
|
+
(() => hooks.onSessionEnd({
|
|
687
|
+
sessionId,
|
|
688
|
+
subtype: String(message.subtype ?? 'unknown'),
|
|
689
|
+
isError: message.is_error === true,
|
|
690
|
+
usage,
|
|
691
|
+
})));
|
|
645
692
|
break;
|
|
646
693
|
}
|
|
647
694
|
case 'stream_event': {
|
|
@@ -649,7 +696,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
649
696
|
// (token-level delta). Only arrives when includePartialMessages passed the flag.
|
|
650
697
|
const streamEvent = message.event;
|
|
651
698
|
if (streamEvent && typeof streamEvent.type === 'string') {
|
|
652
|
-
yield {
|
|
699
|
+
yield { ...attribution,
|
|
653
700
|
type: 'stream_event',
|
|
654
701
|
event: streamEvent,
|
|
655
702
|
sessionId: message.session_id,
|
|
@@ -660,10 +707,15 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
660
707
|
default:
|
|
661
708
|
break;
|
|
662
709
|
}
|
|
663
|
-
if (sawResult)
|
|
710
|
+
if (sawResult && !streamingInput)
|
|
664
711
|
break;
|
|
665
712
|
}
|
|
666
|
-
if (
|
|
713
|
+
if (streamingInput && inputState.finished) {
|
|
714
|
+
await runHook(hooks.onSessionEnd && (() => hooks.onSessionEnd({
|
|
715
|
+
sessionId: lastSessionId, subtype: 'input_complete', isError: false,
|
|
716
|
+
})));
|
|
717
|
+
}
|
|
718
|
+
else if (!sawResult || streamingInput) {
|
|
667
719
|
const code = await transport.exited;
|
|
668
720
|
const aborted = options.signal?.aborted === true;
|
|
669
721
|
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,14 @@
|
|
|
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
|
+
resultTurnId?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* One input is in flight at a time. Keep reading controls while the producer
|
|
12
|
+
* waits, without reading ahead into an unbounded queue of future prompts.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readWithInput(input: AsyncIterable<SDKUserMessage>, transport: Transport, state: InputState, submitted: (message: SDKUserMessage) => Promise<void>): AsyncGenerator<WireMessage>;
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
const message = next.value.value;
|
|
57
|
+
const resultId = Object.prototype.hasOwnProperty.call(message, 'input_uuid')
|
|
58
|
+
? typeof message.input_uuid === 'string' ? message.input_uuid : undefined
|
|
59
|
+
: state.turnId; // Compatibility with CLIs predating explicit attribution.
|
|
60
|
+
const completesInput = message.type === 'result' && state.turnId !== undefined && resultId === state.turnId;
|
|
61
|
+
if (message.type === 'result')
|
|
62
|
+
state.resultTurnId = resultId;
|
|
63
|
+
yield next.value.value;
|
|
64
|
+
if (next.value.value.type === 'result') {
|
|
65
|
+
if (completesInput)
|
|
66
|
+
state.turnId = undefined;
|
|
67
|
+
// Scheduled work can finish while the host is already waiting for
|
|
68
|
+
// the next user input. Replacing that pull loses its eventual value
|
|
69
|
+
// and strands a Session turn behind another source.next().
|
|
70
|
+
if (!state.turnId)
|
|
71
|
+
pendingInput ??= pull();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
// A producer may be waiting on user code forever. Request cancellation,
|
|
78
|
+
// but never let an uncooperative iterator prevent subprocess cleanup.
|
|
79
|
+
void Promise.resolve().then(() => source.return?.()).catch(() => { });
|
|
80
|
+
void Promise.resolve().then(() => wire.return?.()).catch(() => { });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -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
|
+
}
|