@margay/ccl-core 1.4.6-beta.0 → 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/dist/cli.js +7017 -6338
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/query.js +35 -21
- package/dist/sdk/queryInput.d.ts +1 -0
- package/dist/sdk/queryInput.js +14 -2
- package/dist/sdk/session.d.ts +6 -1
- package/dist/sdk/session.js +65 -10
- package/dist/sdk/types.d.ts +33 -0
- package/docs/context-checkpoints-and-daemon.md +37 -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 +4 -2
- package/sdk/API.md +16 -0
- package/sdk/README.md +7 -0
package/dist/sdk/index.d.ts
CHANGED
|
@@ -13,4 +13,4 @@ export { defaultStoreDir, fileSessionStore } from './store.js';
|
|
|
13
13
|
export { spawnCliTransport } from './transport.js';
|
|
14
14
|
export type { RecoveryDiagnostic } from './errorDiagnostics.js';
|
|
15
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';
|
|
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/query.js
CHANGED
|
@@ -35,7 +35,7 @@ import { fileURLToPath } from 'url';
|
|
|
35
35
|
import { SdkMcpRegistry } from './mcpServer.js';
|
|
36
36
|
import { fileSessionStore } from './store.js';
|
|
37
37
|
import { spawnCliTransport } from './transport.js';
|
|
38
|
-
import { isAsyncInput, readWithInput } from './queryInput.js';
|
|
38
|
+
import { isAsyncInput, normalizeUserInput, readWithInput } from './queryInput.js';
|
|
39
39
|
const DEFAULT_CAN_USE_TOOL_TIMEOUT_MS = 300_000;
|
|
40
40
|
/** Builds the CLI argv for a query. Exported for unit tests. */
|
|
41
41
|
export function buildCliArgs(options) {
|
|
@@ -253,6 +253,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
253
253
|
const hooks = options.hooks ?? {};
|
|
254
254
|
const ownRequestIds = new Set();
|
|
255
255
|
const toolNamesById = new Map();
|
|
256
|
+
const toolOrigins = new Map();
|
|
256
257
|
let lastSessionId = options.sessionId ?? options.resume;
|
|
257
258
|
let turn = 0;
|
|
258
259
|
let sawResult = false;
|
|
@@ -321,11 +322,11 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
321
322
|
const userContent = options.promptContentBlocks && options.promptContentBlocks.length > 0
|
|
322
323
|
? options.promptContentBlocks
|
|
323
324
|
: [{ type: 'text', text: prompt }];
|
|
324
|
-
await transport.write({
|
|
325
|
+
await transport.write(normalizeUserInput({
|
|
325
326
|
type: 'user',
|
|
326
327
|
message: { role: 'user', content: userContent },
|
|
327
328
|
parent_tool_use_id: null,
|
|
328
|
-
});
|
|
329
|
+
}));
|
|
329
330
|
// slice sdk-observability-hooks-usage (G5): the host's prompt was submitted to the agent.
|
|
330
331
|
await runHook(hooks.onUserPromptSubmit &&
|
|
331
332
|
(() => hooks.onUserPromptSubmit({ prompt, sessionId: lastSessionId })));
|
|
@@ -341,6 +342,10 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
341
342
|
})
|
|
342
343
|
: transport.read();
|
|
343
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 } : {};
|
|
344
349
|
// Keep ordinary raw passthrough unchanged. Diagnostic payloads are an
|
|
345
350
|
// untrusted error boundary even when a host asks for raw wire events.
|
|
346
351
|
if (rawMessage.type === 'system' && rawMessage.subtype === 'request_progress' && !options.includeRequestProgress)
|
|
@@ -364,18 +369,18 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
364
369
|
}
|
|
365
370
|
}
|
|
366
371
|
if (options.includeRaw)
|
|
367
|
-
yield { type: 'raw', message };
|
|
372
|
+
yield { ...attribution, type: 'raw', message };
|
|
368
373
|
switch (message.type) {
|
|
369
374
|
case 'system': {
|
|
370
375
|
if (message.subtype === 'request_progress' && progress)
|
|
371
|
-
yield {
|
|
376
|
+
yield { ...attribution,
|
|
372
377
|
type: 'request_progress', progress,
|
|
373
378
|
sessionId: typeof message.session_id === 'string' ? message.session_id : undefined,
|
|
374
379
|
};
|
|
375
380
|
if (message.subtype === 'error_diagnostic') {
|
|
376
381
|
const diagnostic = sanitizeRecoveryDiagnostic(message.diagnostic);
|
|
377
382
|
if (diagnostic)
|
|
378
|
-
yield {
|
|
383
|
+
yield { ...attribution,
|
|
379
384
|
type: 'error_diagnostic', diagnostic,
|
|
380
385
|
sessionId: typeof message.session_id === 'string' ? message.session_id : undefined,
|
|
381
386
|
};
|
|
@@ -386,19 +391,19 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
386
391
|
const sessionId = message.session_id;
|
|
387
392
|
for (const block of contentBlocks(message.message)) {
|
|
388
393
|
if (block.type === 'text' && typeof block.text === 'string') {
|
|
389
|
-
yield { type: 'assistant_text', text: block.text, sessionId };
|
|
394
|
+
yield { ...attribution, type: 'assistant_text', text: block.text, sessionId };
|
|
390
395
|
}
|
|
391
396
|
else if (block.type === 'tool_use') {
|
|
392
397
|
const toolUseId = String(block.id ?? '');
|
|
393
398
|
const toolName = String(block.name ?? '');
|
|
394
399
|
toolNamesById.set(toolUseId, toolName);
|
|
395
|
-
|
|
400
|
+
toolOrigins.set(toolUseId, attribution);
|
|
401
|
+
yield { ...attribution,
|
|
396
402
|
type: 'tool_use',
|
|
397
403
|
toolName,
|
|
398
404
|
input: (block.input ?? {}),
|
|
399
405
|
toolUseId,
|
|
400
|
-
sessionId,
|
|
401
|
-
};
|
|
406
|
+
sessionId, };
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
turn += 1;
|
|
@@ -420,7 +425,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
420
425
|
: Array.isArray(block.content) ? block.content.map(part => part?.type === 'text' && typeof part.text === 'string' ? { ...part, text: sanitizeErrorText(part.text) } : part)
|
|
421
426
|
: describeError(block.content)
|
|
422
427
|
: block.content;
|
|
423
|
-
yield {
|
|
428
|
+
yield { ...attribution,
|
|
424
429
|
type: 'tool_result',
|
|
425
430
|
toolUseId,
|
|
426
431
|
content: resultContent,
|
|
@@ -435,6 +440,8 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
435
440
|
isError,
|
|
436
441
|
sessionId,
|
|
437
442
|
})));
|
|
443
|
+
toolOrigins.delete(toolUseId);
|
|
444
|
+
toolNamesById.delete(toolUseId);
|
|
438
445
|
}
|
|
439
446
|
}
|
|
440
447
|
break;
|
|
@@ -473,8 +480,9 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
473
480
|
const toolName = String(request.tool_name ?? '');
|
|
474
481
|
const input = (request.input ?? {});
|
|
475
482
|
const toolUseId = String(request.tool_use_id ?? '');
|
|
483
|
+
attribution = toolOrigins.get(toolUseId) ?? attribution;
|
|
476
484
|
const agentId = typeof request.agent_id === 'string' ? request.agent_id : undefined;
|
|
477
|
-
yield {
|
|
485
|
+
yield { ...attribution,
|
|
478
486
|
type: 'permission_request',
|
|
479
487
|
requestId,
|
|
480
488
|
toolName,
|
|
@@ -509,7 +517,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
509
517
|
await store.deletePendingApproval(pendingApproval.sessionId);
|
|
510
518
|
pendingDecision = undefined;
|
|
511
519
|
pendingApproval = null;
|
|
512
|
-
yield {
|
|
520
|
+
yield { ...attribution,
|
|
513
521
|
type: 'permission_resolved',
|
|
514
522
|
requestId,
|
|
515
523
|
toolUseId,
|
|
@@ -546,13 +554,13 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
546
554
|
},
|
|
547
555
|
},
|
|
548
556
|
});
|
|
549
|
-
yield {
|
|
557
|
+
yield { ...attribution,
|
|
550
558
|
type: 'error',
|
|
551
559
|
message: 'pending approval requested before any session_id was observed',
|
|
552
560
|
fatal: false,
|
|
553
561
|
code: 'pending_without_session',
|
|
554
562
|
};
|
|
555
|
-
yield {
|
|
563
|
+
yield { ...attribution,
|
|
556
564
|
type: 'permission_resolved',
|
|
557
565
|
requestId,
|
|
558
566
|
toolUseId,
|
|
@@ -574,7 +582,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
574
582
|
// the model must not see a denial. Terminating here leaves the
|
|
575
583
|
// transcript ending at the unresolved tool_use, which the
|
|
576
584
|
// resume path drops and re-drives.
|
|
577
|
-
yield {
|
|
585
|
+
yield { ...attribution,
|
|
578
586
|
type: 'suspended',
|
|
579
587
|
sessionId,
|
|
580
588
|
requestId,
|
|
@@ -599,7 +607,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
599
607
|
response: outcome.decision,
|
|
600
608
|
},
|
|
601
609
|
});
|
|
602
|
-
yield {
|
|
610
|
+
yield { ...attribution,
|
|
603
611
|
type: 'permission_resolved',
|
|
604
612
|
requestId,
|
|
605
613
|
toolUseId,
|
|
@@ -618,7 +626,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
618
626
|
if (ownRequestIds.has(requestId)) {
|
|
619
627
|
ownRequestIds.delete(requestId);
|
|
620
628
|
if (response.subtype === 'error') {
|
|
621
|
-
yield {
|
|
629
|
+
yield { ...attribution,
|
|
622
630
|
type: 'error',
|
|
623
631
|
message: `initialize failed: ${String(response.error)}`,
|
|
624
632
|
fatal: false,
|
|
@@ -644,9 +652,9 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
644
652
|
// Metering must not break the conversation stream.
|
|
645
653
|
}
|
|
646
654
|
}
|
|
647
|
-
yield {
|
|
655
|
+
yield { ...attribution,
|
|
648
656
|
type: 'result',
|
|
649
|
-
...(streamingInput ? { turnId: inputState.
|
|
657
|
+
...(streamingInput ? { turnId: inputState.resultTurnId } : {}),
|
|
650
658
|
subtype: String(message.subtype ?? 'unknown'),
|
|
651
659
|
usage,
|
|
652
660
|
totalCostUsd,
|
|
@@ -666,6 +674,12 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
666
674
|
structuredOutput: message
|
|
667
675
|
.structured_output,
|
|
668
676
|
};
|
|
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
|
+
}
|
|
669
683
|
sessionEnded = !streamingInput;
|
|
670
684
|
if (!streamingInput)
|
|
671
685
|
await runHook(hooks.onSessionEnd &&
|
|
@@ -682,7 +696,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
682
696
|
// (token-level delta). Only arrives when includePartialMessages passed the flag.
|
|
683
697
|
const streamEvent = message.event;
|
|
684
698
|
if (streamEvent && typeof streamEvent.type === 'string') {
|
|
685
|
-
yield {
|
|
699
|
+
yield { ...attribution,
|
|
686
700
|
type: 'stream_event',
|
|
687
701
|
event: streamEvent,
|
|
688
702
|
sessionId: message.session_id,
|
package/dist/sdk/queryInput.d.ts
CHANGED
package/dist/sdk/queryInput.js
CHANGED
|
@@ -53,10 +53,22 @@ export async function* readWithInput(input, transport, state, submitted) {
|
|
|
53
53
|
pendingWire = undefined;
|
|
54
54
|
if (next.value.done)
|
|
55
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;
|
|
56
63
|
yield next.value.value;
|
|
57
64
|
if (next.value.value.type === 'result') {
|
|
58
|
-
|
|
59
|
-
|
|
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();
|
|
60
72
|
}
|
|
61
73
|
}
|
|
62
74
|
}
|
package/dist/sdk/session.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Query, QueryOptions, SDKEvent, SDKUserMessage, Transport } from './types.js';
|
|
1
|
+
import type { Query, QueryOptions, SDKEvent, SDKUserMessage, SessionLifecycleEvent, Transport } from './types.js';
|
|
2
2
|
export type TurnResult = Extract<SDKEvent, {
|
|
3
3
|
type: 'result';
|
|
4
4
|
}>;
|
|
@@ -8,6 +8,9 @@ export type SessionOptions = QueryOptions & {
|
|
|
8
8
|
maxPendingTurns?: number;
|
|
9
9
|
/** Events are delivered as they arrive; Session does not retain an event history. */
|
|
10
10
|
onEvent?: (event: SDKEvent) => void;
|
|
11
|
+
/** Deterministic queue/dispatch/permission/terminal observations. No model call.
|
|
12
|
+
* Keep observers fast; thrown errors are isolated from execution. */
|
|
13
|
+
onLifecycle?: (event: SessionLifecycleEvent) => void;
|
|
11
14
|
};
|
|
12
15
|
export interface Turn {
|
|
13
16
|
readonly id: string;
|
|
@@ -29,6 +32,7 @@ export declare class Session {
|
|
|
29
32
|
private closed;
|
|
30
33
|
private drainWaiters;
|
|
31
34
|
private readonly pumping;
|
|
35
|
+
private sessionId?;
|
|
32
36
|
constructor(options?: SessionOptions, transport?: Transport);
|
|
33
37
|
prompt(content: SDKUserMessage['message']['content']): Turn;
|
|
34
38
|
followUp(content: SDKUserMessage['message']['content']): Turn;
|
|
@@ -37,6 +41,7 @@ export declare class Session {
|
|
|
37
41
|
private enqueue;
|
|
38
42
|
private inputs;
|
|
39
43
|
private consume;
|
|
44
|
+
private observe;
|
|
40
45
|
private fail;
|
|
41
46
|
private notifyDrained;
|
|
42
47
|
interrupt(): Promise<void>;
|
package/dist/sdk/session.js
CHANGED
|
@@ -16,6 +16,7 @@ export class Session {
|
|
|
16
16
|
closed = false;
|
|
17
17
|
drainWaiters = [];
|
|
18
18
|
pumping;
|
|
19
|
+
sessionId;
|
|
19
20
|
constructor(options = {}, transport) {
|
|
20
21
|
this.limit = options.maxPendingTurns ?? 32;
|
|
21
22
|
if (!Number.isSafeInteger(this.limit) || this.limit < 1) {
|
|
@@ -24,9 +25,23 @@ export class Session {
|
|
|
24
25
|
if (options.pendingDecision)
|
|
25
26
|
throw new Error('ccl-sdk: resume a pending decision before creating a Session');
|
|
26
27
|
this.options = options;
|
|
28
|
+
this.sessionId = options.sessionId ?? options.resume;
|
|
27
29
|
const owned = transport ?? spawnForOptions(options);
|
|
28
30
|
this.pid = owned.pid;
|
|
29
|
-
this.handle = queryHandleWithTransport(this.inputs(), options,
|
|
31
|
+
this.handle = queryHandleWithTransport(this.inputs(), { ...options, hooks: {
|
|
32
|
+
...options.hooks,
|
|
33
|
+
onUserPromptSubmit: async (event) => {
|
|
34
|
+
if (this.active && !this.active.started) {
|
|
35
|
+
this.active.started = true;
|
|
36
|
+
this.observe({ type: 'turn_started', turnId: this.active.turn.id });
|
|
37
|
+
}
|
|
38
|
+
await options.hooks?.onUserPromptSubmit?.(event);
|
|
39
|
+
},
|
|
40
|
+
onSessionStart: async (event) => {
|
|
41
|
+
this.sessionId = event.sessionId;
|
|
42
|
+
await options.hooks?.onSessionStart?.(event);
|
|
43
|
+
},
|
|
44
|
+
} }, owned);
|
|
30
45
|
this.controls = this.handle;
|
|
31
46
|
this.pumping = this.consume();
|
|
32
47
|
}
|
|
@@ -63,6 +78,7 @@ export class Session {
|
|
|
63
78
|
this.queue.unshift(entry);
|
|
64
79
|
else
|
|
65
80
|
this.queue.push(entry);
|
|
81
|
+
this.observe({ type: 'turn_queued', turnId: id });
|
|
66
82
|
this.wake?.();
|
|
67
83
|
return turn;
|
|
68
84
|
}
|
|
@@ -87,17 +103,31 @@ export class Session {
|
|
|
87
103
|
async consume() {
|
|
88
104
|
try {
|
|
89
105
|
for await (const event of this.handle) {
|
|
90
|
-
|
|
106
|
+
if ('sessionId' in event && event.sessionId)
|
|
107
|
+
this.sessionId = event.sessionId;
|
|
108
|
+
const correlated = { ...event, turnId: event.turnId ?? (event.type === 'result' || event.origin === 'background' ? undefined : this.active?.turn.id) };
|
|
91
109
|
// Observability callbacks must not terminate the agent or strand work.
|
|
92
110
|
try {
|
|
93
111
|
this.options.onEvent?.(correlated);
|
|
94
112
|
}
|
|
95
113
|
catch { /* host observer */ }
|
|
96
|
-
if (event.type === '
|
|
114
|
+
if (event.type === 'permission_request') {
|
|
115
|
+
this.observe({ type: 'permission_waiting', turnId: correlated.turnId, requestId: event.requestId,
|
|
116
|
+
toolUseId: event.toolUseId, toolName: event.toolName });
|
|
117
|
+
}
|
|
118
|
+
else if (event.type === 'permission_resolved') {
|
|
119
|
+
this.observe({ type: 'permission_resolved', turnId: correlated.turnId, requestId: event.requestId,
|
|
120
|
+
toolUseId: event.toolUseId, behavior: event.behavior });
|
|
121
|
+
}
|
|
122
|
+
if (event.type === 'result' && event.turnId !== undefined && this.active) {
|
|
97
123
|
if (event.turnId !== this.active.turn.id)
|
|
98
124
|
throw new Error('ccl-sdk: mismatched turn result');
|
|
99
|
-
this.active
|
|
125
|
+
const entry = this.active;
|
|
100
126
|
this.active = undefined;
|
|
127
|
+
this.observe({ type: 'turn_finished', turnId: entry.turn.id,
|
|
128
|
+
outcome: event.isError ? entry.cancelRequested ? 'interrupted' : 'failed' : 'succeeded',
|
|
129
|
+
...(event.isError ? { reason: event.subtype } : {}) });
|
|
130
|
+
entry.resolve(event);
|
|
101
131
|
this.wake?.();
|
|
102
132
|
this.notifyDrained();
|
|
103
133
|
}
|
|
@@ -105,7 +135,8 @@ export class Session {
|
|
|
105
135
|
throw new Error(event.message);
|
|
106
136
|
}
|
|
107
137
|
else if (event.type === 'suspended') {
|
|
108
|
-
|
|
138
|
+
this.fail(new Error(`ccl-sdk: session suspended; resumeWithDecision(${event.sessionId}) is required`), event.origin === 'background' ? 'interrupted' : 'suspended');
|
|
139
|
+
break;
|
|
109
140
|
}
|
|
110
141
|
}
|
|
111
142
|
if (!this.closed)
|
|
@@ -118,14 +149,31 @@ export class Session {
|
|
|
118
149
|
await this.controls.close();
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
|
-
|
|
152
|
+
observe(event) {
|
|
153
|
+
try {
|
|
154
|
+
this.options.onLifecycle?.({ ...event, at: Date.now(), pid: this.pid, sessionId: this.sessionId,
|
|
155
|
+
pendingTurns: this.queue.length + (this.active ? 1 : 0) });
|
|
156
|
+
}
|
|
157
|
+
catch { /* host observer */ }
|
|
158
|
+
}
|
|
159
|
+
fail(reason, outcome = 'interrupted') {
|
|
160
|
+
if (this.closed)
|
|
161
|
+
return;
|
|
122
162
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
123
163
|
this.closed = true;
|
|
124
164
|
this.accepting = false;
|
|
125
|
-
this.active
|
|
165
|
+
const active = this.active;
|
|
126
166
|
this.active = undefined;
|
|
127
|
-
|
|
167
|
+
const queued = this.queue.splice(0);
|
|
168
|
+
if (active) {
|
|
169
|
+
this.observe({ type: 'turn_finished', turnId: active.turn.id, outcome, reason: error.message });
|
|
170
|
+
active.reject(error);
|
|
171
|
+
}
|
|
172
|
+
for (const entry of queued) {
|
|
173
|
+
this.observe({ type: 'turn_finished', turnId: entry.turn.id, outcome: 'cancelled', reason: error.message });
|
|
128
174
|
entry.reject(error);
|
|
175
|
+
}
|
|
176
|
+
this.observe({ type: 'session_closed', reason: error.message });
|
|
129
177
|
this.wake?.();
|
|
130
178
|
this.notifyDrained();
|
|
131
179
|
void this.controls.close().catch(() => { });
|
|
@@ -137,8 +185,13 @@ export class Session {
|
|
|
137
185
|
resolve();
|
|
138
186
|
}
|
|
139
187
|
async interrupt() {
|
|
140
|
-
if (this.active)
|
|
188
|
+
if (this.active) {
|
|
189
|
+
if (!this.active.cancelRequested) {
|
|
190
|
+
this.active.cancelRequested = true;
|
|
191
|
+
this.observe({ type: 'turn_cancel_requested', turnId: this.active.turn.id });
|
|
192
|
+
}
|
|
141
193
|
await this.controls.interrupt();
|
|
194
|
+
}
|
|
142
195
|
}
|
|
143
196
|
async cancel(id) {
|
|
144
197
|
if (this.active?.turn.id === id)
|
|
@@ -146,7 +199,9 @@ export class Session {
|
|
|
146
199
|
const index = this.queue.findIndex(entry => entry.turn.id === id);
|
|
147
200
|
if (index < 0)
|
|
148
201
|
return;
|
|
149
|
-
this.queue.splice(index, 1)[0]
|
|
202
|
+
const entry = this.queue.splice(index, 1)[0];
|
|
203
|
+
this.observe({ type: 'turn_finished', turnId: id, outcome: 'cancelled', reason: 'Cancelled before dispatch' });
|
|
204
|
+
entry.reject(new Error('ccl-sdk: queued turn cancelled'));
|
|
150
205
|
this.notifyDrained();
|
|
151
206
|
}
|
|
152
207
|
/** Stop accepting inputs and drain accepted turns, keeping controls alive. */
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -314,7 +314,40 @@ export type SDKUserMessage = {
|
|
|
314
314
|
};
|
|
315
315
|
export type SDKEvent = SDKEventPayload & {
|
|
316
316
|
turnId?: string;
|
|
317
|
+
origin?: 'user' | 'background';
|
|
317
318
|
};
|
|
319
|
+
/** Host observations, separate from model output and artifact verification.
|
|
320
|
+
* started means the input was written to the CLI, not that a model responded.
|
|
321
|
+
* Active interruptions can have external effects; they are not queued cancellations. */
|
|
322
|
+
export type SessionLifecycleEvent = {
|
|
323
|
+
at: number;
|
|
324
|
+
pid?: number;
|
|
325
|
+
sessionId?: string;
|
|
326
|
+
pendingTurns: number;
|
|
327
|
+
} & ({
|
|
328
|
+
type: 'turn_queued' | 'turn_started' | 'turn_cancel_requested';
|
|
329
|
+
turnId: string;
|
|
330
|
+
} | {
|
|
331
|
+
type: 'turn_finished';
|
|
332
|
+
turnId: string;
|
|
333
|
+
outcome: 'succeeded' | 'failed' | 'cancelled' | 'interrupted' | 'suspended';
|
|
334
|
+
reason?: string;
|
|
335
|
+
} | {
|
|
336
|
+
type: 'permission_waiting';
|
|
337
|
+
turnId?: string;
|
|
338
|
+
requestId: string;
|
|
339
|
+
toolUseId: string;
|
|
340
|
+
toolName: string;
|
|
341
|
+
} | {
|
|
342
|
+
type: 'permission_resolved';
|
|
343
|
+
turnId?: string;
|
|
344
|
+
requestId: string;
|
|
345
|
+
toolUseId: string;
|
|
346
|
+
behavior: 'allow' | 'deny';
|
|
347
|
+
} | {
|
|
348
|
+
type: 'session_closed';
|
|
349
|
+
reason: string;
|
|
350
|
+
});
|
|
318
351
|
type SDKEventPayload = {
|
|
319
352
|
type: 'assistant_text';
|
|
320
353
|
text: string;
|
|
@@ -52,3 +52,40 @@ retries saving an already successful result, without rerunning its tools.
|
|
|
52
52
|
`cancel` stops an occurrence; delete the durable cron definition as well when
|
|
53
53
|
future recurring occurrences should cease. A permission wait and an interrupted
|
|
54
54
|
run remain visible across restarts. Stop does not erase schedules or evidence.
|
|
55
|
+
|
|
56
|
+
When asking the assistant to schedule work, choose its execution scope:
|
|
57
|
+
|
|
58
|
+
- `CronCreate {execution: "session", afterMs: 60000, prompt: "…"}` keeps the
|
|
59
|
+
reminder in the current process. A one-shot print command exits after its turn,
|
|
60
|
+
so this scope does not promise execution after exit.
|
|
61
|
+
- `CronCreate {execution: "background", afterMs: 60000, prompt: "…"}` saves the
|
|
62
|
+
definition durably and starts or reuses the project's detached owner. Calendar
|
|
63
|
+
and anchored interval schedules use the same scope selection.
|
|
64
|
+
- Older calls with only `durable: true` still save the definition without starting
|
|
65
|
+
an owner. Explicit `execution: "session"` cannot be combined with durable storage.
|
|
66
|
+
|
|
67
|
+
The response separates `saved`, `status`, `owner.ready`, canonical `project`,
|
|
68
|
+
`nextRunAt`, `permissions`, and `resultDirectory`. `background_ready` requires a
|
|
69
|
+
live protocol response and ownership of the project scheduler lock. An interactive
|
|
70
|
+
scheduler yields that lock after its pending definition writes finish; private
|
|
71
|
+
session reminders continue in their own process.
|
|
72
|
+
|
|
73
|
+
If the response is `saved_not_running`, retain its task ID and use
|
|
74
|
+
`CronCreate {operation: "handoff", taskId: "…"}` to retry. This keeps the same
|
|
75
|
+
schedule and identity. Do not create a duplicate. A different CLI path or configuration
|
|
76
|
+
already running for the project is reported explicitly; inspect it and stop it
|
|
77
|
+
before handing off with the new configuration. Startup does not inherit the
|
|
78
|
+
creating terminal's permission bypass mode. Existing configured rules apply,
|
|
79
|
+
and unresolved actions remain `waiting_permission` until explicitly approved.
|
|
80
|
+
|
|
81
|
+
The owner uses the creating CLI path, model, configuration inputs and credential
|
|
82
|
+
environment. Protocol, multiplexer and test-session variables are removed. Launch
|
|
83
|
+
configuration is passed through a private pipe, without writing gateway credentials
|
|
84
|
+
into task or owner records. Restarting after a reboot requires starting an owner
|
|
85
|
+
again with valid configuration; no temporary credentials are promoted to permanent
|
|
86
|
+
configuration.
|
|
87
|
+
|
|
88
|
+
`CronList` includes background execution/delivery records and result locations,
|
|
89
|
+
even after a completed one-shot definition has been removed. `ccl daemon status`
|
|
90
|
+
provides the same records from a fresh CLI. Process liveness (`running`) and verified
|
|
91
|
+
scheduler readiness (`owner.ready`) are separate fields.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 1.5.0-alpha.0
|
|
2
|
+
|
|
3
|
+
本版基于 `1.4.6-beta.0`,交付 AI 指令综合升级。原方案 F1–F14 的修复、真实端到端验证和六原则复核已完成,原协议 Lisa 在 R10 总复核通过。此版为 alpha 预览,不将办公生成的失败样本或既有类型诊断宣称为通过。
|
|
4
|
+
|
|
5
|
+
## 主要变化
|
|
6
|
+
|
|
7
|
+
- 主助手、子助手的能力说明与实际工具解析共用来源。bare 无 Agent 时可直接工作;零工具角色不再被描述成全工具。内置技能和常规提示不强制调用不存在的工具。
|
|
8
|
+
- 技能目录按完整条目分页,保留中文用途与近邻排除;宿主维护真实加载状态,文件里的标签不再被视为授权凭据。
|
|
9
|
+
- 执行型子助手保留项目约束;审查允许必要的隔离测试产物。配置使用真实路径 owner;Git 写操作按依赖顺序执行;旧 auto/smart 不再用短词覆盖上下文分类。
|
|
10
|
+
- 定时任务支持一次性、固定间隔和带时区的日历计划,包含未来起点与显式时间容差。新建精确任务默认不主动提前或错峰。
|
|
11
|
+
- 明确后台任务接通 daemon 的启动、调度锁和真实就绪检查;保存、执行、权限等待和投递状态分别呈现。关闭创建终端后,结果可从新入口取回。
|
|
12
|
+
- SDK 增加生命周期事件,修复用户输入与自主后台结果并发时的挂起和归属。包内 `examples/assistant/` 提供连续会话、进度、权限恢复和 Office 独立验证的参考宿主。
|
|
13
|
+
- 统一记忆选择 JSON 合同,避免整理 worker 重复检索。视觉输出预算受实际模型能力约束,预算耗尽或截断明确失败;主动取消不再误报缺少正常答案。
|
|
14
|
+
- Office 渲染只清理自己登记且未被修改的过期页面,避免短版文档混入旧尾页。
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install --global @margay/ccl-core@1.5.0-alpha.0
|
|
20
|
+
ccl --version
|
|
21
|
+
margay --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
两个入口均应输出 `1.5.0-alpha.0 (Margay)`。Node 要求仍为 `>=22.19.0`。预览标签为 `alpha`;`latest`、`next`、`stable`、`rc` 保持原值。
|
|
25
|
+
|
|
26
|
+
此包同时提供 CLI 与 SDK,不会更新 Margay Desktop 内置依赖,也不会自动重启已有进程或安装系统开机服务。若 PATH 前面存在旧包装脚本,用 `type -a ccl margay` 与 `"$(npm prefix -g)/bin/ccl" --version` 核对实际入口。
|
|
27
|
+
|
|
28
|
+
Office 参考流程的制作/渲染依赖及宿主与助手分工见包内 `examples/assistant/README.md` 和 `office/workflow.md`;发布包不自动安装 LibreOffice 或 Python 制作依赖。
|
|
29
|
+
|
|
30
|
+
## 调度兼容与回退
|
|
31
|
+
|
|
32
|
+
旧 cron 定义保持原有读取语义,不在读取时静默迁移。首次向已有旧格式文件写入版本 1 计划前,保存原字节到 `.ccl/scheduled_tasks.json.pre-v1.backup`。
|
|
33
|
+
|
|
34
|
+
旧版本不能完整理解新建的 once/interval 计划。回退前先停止本项目的新调度 owner,导出新任务与执行结果,再恢复旧格式备份;没有旧文件时保留导出并移开新格式文件。之后才安装旧版本。不要同时运行新旧调度 owner;回退程序不会撤销已经执行的业务写入。详见 [调度与 daemon 说明](../context-checkpoints-and-daemon.md)。
|
|
35
|
+
|
|
36
|
+
## 验证与结果
|
|
37
|
+
|
|
38
|
+
实现过程通过分片回归、原版反例、公开 CLI/SDK 合同、真实 WezTerm 模型行为、安装包验证及 R1–R10 协议 Lisa 审核。真实后台测试确认创建终端退出后读取库存,未提前访问,新入口取回正确结果。公开参考宿主验证十轮同 PID、排队/steer/取消、断线不重放、权限 pending 后恢复以及错误制品拒绝。
|
|
39
|
+
|
|
40
|
+
180 次短答和 30 个 Office 长任务的固定版本比较均保留失败及修复成本。Office 最终 29/30 合格,8/30 零修复,后者全部来自常驻会话的后续复用;候选 CLI/SDK 中位耗时增加,不能声称全面提速。这些性能包在比较开始时冻结,早于后续修复,不是本 alpha 最终包的性能成绩。
|
|
41
|
+
|
|
42
|
+
已知边界:全仓类型检查仍有与基线相同的 11 条诊断;有头十图测试包含记录在案的人工介入;Windows 实机未在本次验证。按需 schema 子树的实验没有改变产品默认配置。AI 生成内容仍需事实和全页审阅,文件存在或模型自报不是交付通过。
|
|
43
|
+
|
|
44
|
+
仓库内 [完整报告](../ai-instructions-upgrade-final-review-2026-10-20.md)、[网页信息图](../ai-upgrade-explained-2026-10-20.html) 保留修复、证据和审核关系。原始运行证据保留于执行机 `.rll/records/ai-upgrade/`;本版打包、安装与发布回执保留于 `.rll/records/release-1.5.0-alpha.0/`,本地原始证据不随 npm 包分发。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Terminal assistant reference host
|
|
2
|
+
|
|
3
|
+
This runnable host consumes the public `@margay/ccl-core/sdk`. It is included in the npm package; it does not modify the separate Margay GUI.
|
|
4
|
+
|
|
5
|
+
From an installed package (Node 22.19+), run:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
node node_modules/@margay/ccl-core/examples/assistant/terminal.mjs --cwd /path/to/project
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
From this checkout, build first (`npm run build`), then run `node examples/assistant/terminal.mjs --cwd /path/to/project`. Normal CLI account/configuration discovery applies. `--model MODEL` selects a model; `--extra-args '["--tools","Read,Write"]'` forwards explicit CLI options. Keep credentials in the existing account configuration, not command arguments.
|
|
12
|
+
|
|
13
|
+
Type a task. `/status` shows turn IDs and separate execution, verification and artifact states. `/steer TEXT` interrupts the active turn and queues the correction before follow-ups. `/cancel` requests active interruption; `/cancel TURN_ID` also removes a queued turn before dispatch. Completed effects are not rolled back.
|
|
14
|
+
|
|
15
|
+
Permission requests show the exact tool and input. `/allow TOOL_ID` or `/deny TOOL_ID` resolves a live request. `/defer TOOL_ID` persists it and suspends execution. Exit with `/exit`, restart with `--resume`, then explicitly allow or deny the saved request. Resume never approves automatically. Existing configured allow/deny rules remain in effect.
|
|
16
|
+
|
|
17
|
+
`/artifact RELATIVE_PATH` reads a project file, records its size and SHA-256, and reports availability. A model result, a readable file, or a model-written `validation.json` does not establish content correctness. Independent verification is recorded separately.
|
|
18
|
+
|
|
19
|
+
State defaults to `PROJECT/.ccl/assistant-client/`; `--state-dir DIR` selects another directory. `state.json` stores session identity, recent turn outcomes and artifact records; `events.jsonl` retains the full journal. CLI transcripts and SDK approval state use their existing stores. These files can contain task content: share them only as appropriate for that project. One client holds the state directory lock; another cannot overwrite it. After a crash, restart with `--resume`. Pending queued work is cancelled and work that may already have executed is marked interrupted; inputs are never replayed automatically. Inspect recorded effects before retrying. A stale lock expires after ten seconds.
|
|
20
|
+
|
|
21
|
+
Queue and dispatch notifications are local SDK observations and make no model request. “已发送” means written to the CLI, not a model response. The actual text stream, tools and request progress are displayed as received. Background events carry `origin: 'background'` and are not assigned to the waiting user turn.
|
|
22
|
+
|
|
23
|
+
For the Office workflow, `/office ["inputs","artifacts"]` sends the packaged workflow location and the supplied directories to the current session. It reads the provided CSV/brief/meeting material and creates editable files using the documented toolchain. `/verify-office ["inputs","artifacts"]` runs the packaged independent Python checker from the host after execution ends. It reports numeric/structural differences and keeps semantic review separate. The checker requires Python 3; generation and full-page rendering dependencies are documented in [the workflow](office/workflow.md). This is an explicit reference-client command; it does not claim registration as a global CLI slash command.
|