@a3s-lab/code 0.3.1 → 0.4.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.
@@ -0,0 +1,1092 @@
1
+ /**
2
+ * Session — The core abstraction for A3S Code SDK.
3
+ *
4
+ * A Session binds a workspace and model at creation time (immutable).
5
+ * All agentic, generation, streaming, and context management calls are methods on the session.
6
+ *
7
+ * Every `session.send()` can trigger the AgenticLoop — when the model calls tools,
8
+ * the session automatically enters the loop (generate → tool call → execute → reflect → repeat).
9
+ *
10
+ * Supports `using` syntax for automatic cleanup via Symbol.asyncDispose.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { A3sClient, createProvider } from '@a3s-lab/code';
15
+ *
16
+ * const client = new A3sClient();
17
+ * const openai = createProvider({ name: 'openai', apiKey: 'sk-xxx' });
18
+ *
19
+ * await using session = await client.createSession({
20
+ * model: openai('gpt-4o'),
21
+ * workspace: '/project',
22
+ * system: 'You are a senior software engineer.',
23
+ * });
24
+ *
25
+ * // Simple question — model answers directly
26
+ * const { text } = await session.send('What is TypeScript?');
27
+ *
28
+ * // Complex task — auto-enters AgenticLoop
29
+ * const { text, steps, toolCalls } = await session.send(
30
+ * 'Refactor the auth module to use JWT',
31
+ * );
32
+ *
33
+ * // Streaming with real-time events
34
+ * const { eventStream } = session.sendStream('Fix all TODOs in src/');
35
+ * for await (const event of eventStream) {
36
+ * if (event.type === 'text') process.stdout.write(event.content);
37
+ * }
38
+ * ```
39
+ */
40
+ // ============================================================================
41
+ // Internal Helpers
42
+ // ============================================================================
43
+ function resolveMessages(prompt, messages) {
44
+ if (messages && messages.length > 0)
45
+ return messages;
46
+ if (prompt)
47
+ return [{ role: 'user', content: prompt }];
48
+ throw new Error('Either "prompt" or "messages" must be provided');
49
+ }
50
+ async function executeClientTool(toolDef, toolCall, onToolCall) {
51
+ const args = toolCall.arguments ? JSON.parse(toolCall.arguments) : {};
52
+ const event = {
53
+ toolCallId: toolCall.id,
54
+ toolName: toolCall.name,
55
+ args,
56
+ };
57
+ if (onToolCall) {
58
+ const callbackResult = await onToolCall(event);
59
+ if (callbackResult !== undefined && !toolDef.execute) {
60
+ return {
61
+ success: true,
62
+ output: typeof callbackResult === 'string'
63
+ ? callbackResult
64
+ : JSON.stringify(callbackResult),
65
+ error: '',
66
+ metadata: {},
67
+ };
68
+ }
69
+ }
70
+ if (toolDef.execute) {
71
+ try {
72
+ const result = await toolDef.execute(args, { toolCallId: toolCall.id });
73
+ return {
74
+ success: true,
75
+ output: typeof result === 'string' ? result : JSON.stringify(result),
76
+ error: '',
77
+ metadata: {},
78
+ };
79
+ }
80
+ catch (err) {
81
+ return {
82
+ success: false,
83
+ output: '',
84
+ error: err instanceof Error ? err.message : String(err),
85
+ metadata: {},
86
+ };
87
+ }
88
+ }
89
+ return {
90
+ success: false,
91
+ output: '',
92
+ error: `Tool "${toolCall.name}" has no execute function and onToolCall did not return a result`,
93
+ metadata: {},
94
+ };
95
+ }
96
+ /** Map friendly lane name to proto enum */
97
+ function laneNameToProto(lane) {
98
+ const map = {
99
+ control: 'SESSION_LANE_CONTROL',
100
+ query: 'SESSION_LANE_QUERY',
101
+ execute: 'SESSION_LANE_EXECUTE',
102
+ generate: 'SESSION_LANE_GENERATE',
103
+ };
104
+ return map[lane];
105
+ }
106
+ /** Map friendly handler mode to proto enum */
107
+ function handlerModeToProto(mode) {
108
+ const map = {
109
+ internal: 'TASK_HANDLER_MODE_INTERNAL',
110
+ external: 'TASK_HANDLER_MODE_EXTERNAL',
111
+ hybrid: 'TASK_HANDLER_MODE_HYBRID',
112
+ };
113
+ return map[mode];
114
+ }
115
+ /** Map friendly timeout action to proto enum */
116
+ function timeoutActionToProto(action) {
117
+ if (action === 'auto-approve')
118
+ return 'TIMEOUT_ACTION_AUTO_APPROVE';
119
+ return 'TIMEOUT_ACTION_REJECT';
120
+ }
121
+ /** Map friendly permission action to proto enum */
122
+ function permissionActionToProto(action) {
123
+ const map = {
124
+ allow: 'PERMISSION_DECISION_ALLOW',
125
+ deny: 'PERMISSION_DECISION_DENY',
126
+ ask: 'PERMISSION_DECISION_ASK',
127
+ };
128
+ return map[action] || 'PERMISSION_DECISION_ALLOW';
129
+ }
130
+ /** Map a proto AgenticGenerateEvent to an SDK AgentLoopEvent */
131
+ function mapProtoAgenticEvent(event) {
132
+ switch (event.type) {
133
+ case 'text':
134
+ return event.content ? { type: 'text', content: event.content } : null;
135
+ case 'tool_call':
136
+ if (!event.toolCall)
137
+ return null;
138
+ return {
139
+ type: 'tool_call',
140
+ toolName: event.toolCall.name,
141
+ args: event.toolCall.arguments ? JSON.parse(event.toolCall.arguments) : {},
142
+ toolCallId: event.toolCall.id,
143
+ };
144
+ case 'tool_result':
145
+ return event.toolResult
146
+ ? {
147
+ type: 'tool_result',
148
+ toolCallId: event.toolCallId || '',
149
+ output: event.toolResult.output,
150
+ success: event.toolResult.success,
151
+ }
152
+ : null;
153
+ case 'step_finish':
154
+ return {
155
+ type: 'step_finish',
156
+ stepIndex: event.stepIndex ?? 0,
157
+ text: event.stepText || '',
158
+ toolCalls: [],
159
+ };
160
+ case 'error':
161
+ return {
162
+ type: 'error',
163
+ message: event.errorMessage || 'Unknown error',
164
+ recoverable: event.recoverable ?? false,
165
+ };
166
+ case 'done':
167
+ return { type: 'done', finishReason: event.finishReason || 'stop' };
168
+ case 'confirmation_required':
169
+ return {
170
+ type: 'confirmation_required',
171
+ confirmationId: event.confirmationId || '',
172
+ toolName: event.toolName || '',
173
+ args: event.toolArgs ? JSON.parse(event.toolArgs) : {},
174
+ timeout: event.timeoutMs ?? 30000,
175
+ };
176
+ case 'confirmation_received':
177
+ return { type: 'confirmation_received', approved: event.approved ?? false };
178
+ case 'subagent_start':
179
+ return {
180
+ type: 'subagent_start',
181
+ agentName: event.agentName || '',
182
+ task: event.agentTask || '',
183
+ sessionId: event.agentSessionId || '',
184
+ };
185
+ case 'subagent_end':
186
+ return {
187
+ type: 'subagent_end',
188
+ agentName: event.agentName || '',
189
+ result: event.agentResult || '',
190
+ };
191
+ case 'context_compact':
192
+ return {
193
+ type: 'context_compact',
194
+ beforeTokens: event.beforeTokens ?? 0,
195
+ afterTokens: event.afterTokens ?? 0,
196
+ };
197
+ default:
198
+ return null;
199
+ }
200
+ }
201
+ // ============================================================================
202
+ // Session Class
203
+ // ============================================================================
204
+ /**
205
+ * Session — The core object for interacting with A3S Code.
206
+ *
207
+ * Created via `client.createSession()`. Workspace and model are immutable
208
+ * after creation. Supports `await using` for automatic cleanup.
209
+ */
210
+ export class Session {
211
+ /** The underlying A3S client */
212
+ _client;
213
+ /** Session ID on the server */
214
+ id;
215
+ /** Whether this session has been closed */
216
+ _closed = false;
217
+ /** Custom registered agents */
218
+ _customAgents = [];
219
+ /** @internal — Use client.createSession() instead */
220
+ constructor(client, sessionId) {
221
+ this._client = client;
222
+ this.id = sessionId;
223
+ }
224
+ // --------------------------------------------------------------------------
225
+ // Text Generation
226
+ // --------------------------------------------------------------------------
227
+ /**
228
+ * Generate text from the language model.
229
+ *
230
+ * Supports multi-step tool calling via `tools` and `maxSteps`.
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * const { text } = await session.generateText({ prompt: 'Hello' });
235
+ *
236
+ * // With tools
237
+ * const { text, steps } = await session.generateText({
238
+ * prompt: 'What is the weather?',
239
+ * tools: { weather: weatherTool },
240
+ * maxSteps: 5,
241
+ * });
242
+ * ```
243
+ */
244
+ async generateText(options) {
245
+ this._ensureOpen();
246
+ const messages = resolveMessages(options.prompt, options.messages);
247
+ const maxSteps = options.maxSteps ?? 1;
248
+ const allSteps = [];
249
+ let fullText = '';
250
+ let lastFinishReason = 'stop';
251
+ const allToolCalls = [];
252
+ for (let step = 0; step < maxSteps; step++) {
253
+ const stepMessages = step === 0 ? messages : [];
254
+ const response = await this._client.generate(this.id, stepMessages);
255
+ const stepText = response.message?.content || '';
256
+ fullText += stepText;
257
+ lastFinishReason = response.finishReason;
258
+ const stepToolCalls = response.toolCalls || [];
259
+ const stepToolResults = [];
260
+ allToolCalls.push(...stepToolCalls);
261
+ // Execute client-side tools
262
+ const clientToolCalls = stepToolCalls.filter((tc) => options.tools && tc.name in options.tools);
263
+ for (const tc of clientToolCalls) {
264
+ const toolDef = options.tools[tc.name];
265
+ const result = await executeClientTool(toolDef, tc, options.onToolCall);
266
+ stepToolResults.push(result);
267
+ tc.result = result;
268
+ }
269
+ const stepResult = {
270
+ stepIndex: step,
271
+ text: stepText,
272
+ toolCalls: stepToolCalls,
273
+ toolResults: stepToolResults,
274
+ usage: response.usage,
275
+ finishReason: response.finishReason,
276
+ };
277
+ allSteps.push(stepResult);
278
+ if (options.onStepFinish) {
279
+ await options.onStepFinish(stepResult);
280
+ }
281
+ if (stepToolCalls.length === 0 || response.finishReason !== 'tool_calls') {
282
+ break;
283
+ }
284
+ }
285
+ return {
286
+ text: fullText,
287
+ usage: allSteps.length > 0 ? allSteps[allSteps.length - 1].usage : undefined,
288
+ finishReason: lastFinishReason,
289
+ toolCalls: allToolCalls,
290
+ steps: allSteps,
291
+ };
292
+ }
293
+ // --------------------------------------------------------------------------
294
+ // Text Streaming
295
+ // --------------------------------------------------------------------------
296
+ /**
297
+ * Stream text from the language model.
298
+ *
299
+ * Returns immediately with stream handles. Supports multi-step tool calling.
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * const { textStream } = session.streamText({ prompt: 'Explain this' });
304
+ * for await (const chunk of textStream) {
305
+ * process.stdout.write(chunk);
306
+ * }
307
+ * ```
308
+ */
309
+ streamText(options) {
310
+ this._ensureOpen();
311
+ const messages = resolveMessages(options.prompt, options.messages);
312
+ const maxSteps = options.maxSteps ?? 1;
313
+ let fullText = '';
314
+ let finalUsage;
315
+ let finalFinishReason;
316
+ const allSteps = [];
317
+ let resolveText;
318
+ let resolveUsage;
319
+ let resolveFinishReason;
320
+ let resolveSteps;
321
+ let rejectText;
322
+ const textPromise = new Promise((res, rej) => {
323
+ resolveText = res;
324
+ rejectText = rej;
325
+ });
326
+ const usagePromise = new Promise((res) => {
327
+ resolveUsage = res;
328
+ });
329
+ const finishReasonPromise = new Promise((res) => {
330
+ resolveFinishReason = res;
331
+ });
332
+ const stepsPromise = new Promise((res) => {
333
+ resolveSteps = res;
334
+ });
335
+ const chunks = [];
336
+ let streamDone = false;
337
+ const waiters = [];
338
+ function notifyWaiters() {
339
+ for (const w of waiters.splice(0))
340
+ w();
341
+ }
342
+ const sessionId = this.id;
343
+ const client = this._client;
344
+ const produce = (async () => {
345
+ try {
346
+ for (let step = 0; step < maxSteps; step++) {
347
+ const stepMessages = step === 0 ? messages : [];
348
+ const stream = client.streamGenerate(sessionId, stepMessages);
349
+ let stepText = '';
350
+ const stepToolCalls = [];
351
+ const stepToolResults = [];
352
+ let stepFinishReason;
353
+ for await (const chunk of stream) {
354
+ if (chunk.content) {
355
+ fullText += chunk.content;
356
+ stepText += chunk.content;
357
+ }
358
+ if (chunk.toolCall)
359
+ stepToolCalls.push(chunk.toolCall);
360
+ if (chunk.finishReason) {
361
+ stepFinishReason = chunk.finishReason;
362
+ finalFinishReason = chunk.finishReason;
363
+ }
364
+ chunks.push(chunk);
365
+ notifyWaiters();
366
+ }
367
+ // Execute client-side tools
368
+ if (options.tools) {
369
+ for (const tc of stepToolCalls) {
370
+ if (tc.name in options.tools) {
371
+ const toolDef = options.tools[tc.name];
372
+ const result = await executeClientTool(toolDef, tc, options.onToolCall);
373
+ stepToolResults.push(result);
374
+ tc.result = result;
375
+ chunks.push({
376
+ type: 'tool_result',
377
+ sessionId,
378
+ content: '',
379
+ toolCall: tc,
380
+ toolResult: result,
381
+ metadata: {},
382
+ });
383
+ notifyWaiters();
384
+ }
385
+ }
386
+ }
387
+ const stepResult = {
388
+ stepIndex: step,
389
+ text: stepText,
390
+ toolCalls: stepToolCalls,
391
+ toolResults: stepToolResults,
392
+ usage: undefined,
393
+ finishReason: stepFinishReason,
394
+ };
395
+ allSteps.push(stepResult);
396
+ if (options.onStepFinish) {
397
+ await options.onStepFinish(stepResult);
398
+ }
399
+ if (stepToolCalls.length === 0 || stepFinishReason !== 'tool_calls') {
400
+ break;
401
+ }
402
+ }
403
+ resolveText(fullText);
404
+ resolveUsage(finalUsage);
405
+ resolveFinishReason(finalFinishReason);
406
+ resolveSteps(allSteps);
407
+ }
408
+ catch (err) {
409
+ rejectText(err);
410
+ throw err;
411
+ }
412
+ finally {
413
+ streamDone = true;
414
+ notifyWaiters();
415
+ }
416
+ })();
417
+ produce.catch(() => { });
418
+ function createIterator(transform) {
419
+ return {
420
+ [Symbol.asyncIterator]() {
421
+ let index = 0;
422
+ return {
423
+ async next() {
424
+ while (true) {
425
+ if (index < chunks.length) {
426
+ const val = transform(chunks[index++]);
427
+ if (val !== null)
428
+ return { value: val, done: false };
429
+ continue;
430
+ }
431
+ if (streamDone) {
432
+ return { value: undefined, done: true };
433
+ }
434
+ await new Promise((r) => waiters.push(r));
435
+ }
436
+ },
437
+ };
438
+ },
439
+ };
440
+ }
441
+ return {
442
+ textStream: createIterator((c) => (c.content ? c.content : null)),
443
+ fullStream: createIterator((c) => c),
444
+ toolStream: createIterator((c) => (c.toolCall ? c.toolCall : null)),
445
+ text: textPromise,
446
+ usage: usagePromise,
447
+ finishReason: finishReasonPromise,
448
+ steps: stepsPromise,
449
+ };
450
+ }
451
+ // --------------------------------------------------------------------------
452
+ // Structured Output
453
+ // --------------------------------------------------------------------------
454
+ /**
455
+ * Generate a structured object from the language model.
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * const { object } = await session.generateObject({
460
+ * schema: JSON.stringify({ type: 'object', properties: { name: { type: 'string' } } }),
461
+ * prompt: 'Extract the name',
462
+ * });
463
+ * ```
464
+ */
465
+ async generateObject(options) {
466
+ this._ensureOpen();
467
+ const messages = resolveMessages(options.prompt, options.messages);
468
+ const response = await this._client.generateStructured(this.id, messages, options.schema);
469
+ let parsed;
470
+ try {
471
+ parsed = JSON.parse(response.data);
472
+ }
473
+ catch {
474
+ parsed = response.data;
475
+ }
476
+ return { object: parsed, data: response.data, usage: response.usage };
477
+ }
478
+ /**
479
+ * Stream a structured object from the language model.
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * const { partialStream, object } = session.streamObject({
484
+ * schema: '{"type":"object","properties":{"items":{"type":"array"}}}',
485
+ * prompt: 'List project files',
486
+ * });
487
+ * for await (const partial of partialStream) {
488
+ * console.log('partial:', partial);
489
+ * }
490
+ * const result = await object;
491
+ * ```
492
+ */
493
+ streamObject(options) {
494
+ this._ensureOpen();
495
+ const messages = resolveMessages(options.prompt, options.messages);
496
+ let fullData = '';
497
+ let resolveObject;
498
+ let resolveData;
499
+ let rejectAll;
500
+ const objectPromise = new Promise((res, rej) => {
501
+ resolveObject = res;
502
+ rejectAll = rej;
503
+ });
504
+ const dataPromise = new Promise((res) => {
505
+ resolveData = res;
506
+ });
507
+ const sessionId = this.id;
508
+ const client = this._client;
509
+ const partialStream = {
510
+ [Symbol.asyncIterator]() {
511
+ let started = false;
512
+ let iter;
513
+ return {
514
+ async next() {
515
+ if (!started) {
516
+ started = true;
517
+ const stream = client.streamGenerateStructured(sessionId, messages, options.schema);
518
+ iter = stream[Symbol.asyncIterator]();
519
+ }
520
+ try {
521
+ const result = await iter.next();
522
+ if (result.done) {
523
+ resolveData(fullData);
524
+ try {
525
+ resolveObject(JSON.parse(fullData));
526
+ }
527
+ catch {
528
+ resolveObject(fullData);
529
+ }
530
+ return { value: undefined, done: true };
531
+ }
532
+ fullData += result.value.data;
533
+ return { value: result.value.data, done: false };
534
+ }
535
+ catch (err) {
536
+ rejectAll(err);
537
+ throw err;
538
+ }
539
+ },
540
+ };
541
+ },
542
+ };
543
+ return { partialStream, object: objectPromise, data: dataPromise };
544
+ }
545
+ // --------------------------------------------------------------------------
546
+ // Context Management
547
+ // --------------------------------------------------------------------------
548
+ /** Get context usage (token counts, message count) */
549
+ async getContextUsage() {
550
+ this._ensureOpen();
551
+ const resp = await this._client.getContextUsage(this.id);
552
+ return resp.usage;
553
+ }
554
+ /** Compact the conversation context to save tokens */
555
+ async compactContext() {
556
+ this._ensureOpen();
557
+ await this._client.compactContext(this.id);
558
+ }
559
+ /** Clear conversation history */
560
+ async clearContext() {
561
+ this._ensureOpen();
562
+ await this._client.clearContext(this.id);
563
+ }
564
+ /** Get conversation messages */
565
+ async getMessages(limit, offset) {
566
+ this._ensureOpen();
567
+ return this._client.getMessages(this.id, limit, offset);
568
+ }
569
+ // --------------------------------------------------------------------------
570
+ // AgenticLoop: send() / sendStream()
571
+ // --------------------------------------------------------------------------
572
+ /**
573
+ * Send a message to the agent. Automatically enters the server-side
574
+ * AgenticLoop when the model calls tools (plan → execute → reflect → repeat).
575
+ *
576
+ * The entire loop runs on the server — built-in tools, planning, reflection,
577
+ * and HITL are all handled server-side. Client-side tools (if provided) are
578
+ * used as a fallback for tools not available on the server.
579
+ *
580
+ * @example
581
+ * ```typescript
582
+ * // Simple question — no tools needed
583
+ * const { text } = await session.send('What is TypeScript?');
584
+ *
585
+ * // Complex task — server runs full AgenticLoop
586
+ * const { text, steps, toolCalls } = await session.send(
587
+ * 'Refactor the auth module to use JWT',
588
+ * { maxSteps: 50 },
589
+ * );
590
+ * ```
591
+ */
592
+ async send(prompt, options = {}) {
593
+ this._ensureOpen();
594
+ const strategyMap = {
595
+ auto: 'AGENTIC_STRATEGY_AUTO',
596
+ direct: 'AGENTIC_STRATEGY_DIRECT',
597
+ planned: 'AGENTIC_STRATEGY_PLANNED',
598
+ iterative: 'AGENTIC_STRATEGY_ITERATIVE',
599
+ parallel: 'AGENTIC_STRATEGY_PARALLEL',
600
+ };
601
+ const resp = await this._client.agenticGenerate(this.id, prompt, {
602
+ strategy: strategyMap[options.strategy || 'auto'],
603
+ maxSteps: options.maxSteps ?? 50,
604
+ reflection: options.reflection ?? true,
605
+ planning: options.planning === true || options.planning === 'auto',
606
+ });
607
+ // Map server response to SDK types
608
+ const steps = (resp.steps || []).map((s) => ({
609
+ stepIndex: s.stepIndex,
610
+ text: s.text,
611
+ toolCalls: s.toolCalls || [],
612
+ toolResults: s.toolResults || [],
613
+ usage: s.usage,
614
+ finishReason: s.finishReason,
615
+ }));
616
+ // Emit events for callbacks
617
+ if (options.onEvent) {
618
+ await options.onEvent({ type: 'done', finishReason: resp.finishReason });
619
+ }
620
+ return {
621
+ text: resp.text,
622
+ steps,
623
+ toolCalls: resp.toolCalls || [],
624
+ usage: resp.usage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
625
+ finishReason: resp.finishReason || 'stop',
626
+ };
627
+ }
628
+ /**
629
+ * Stream a message to the agent. Returns an event stream for real-time UI
630
+ * and a promise for the final result.
631
+ *
632
+ * @example
633
+ * ```typescript
634
+ * const { eventStream, result } = session.sendStream('Fix all TODOs in src/');
635
+ * for await (const event of eventStream) {
636
+ * if (event.type === 'text') process.stdout.write(event.content);
637
+ * if (event.type === 'tool_call') console.log(`🔧 ${event.toolName}`);
638
+ * }
639
+ * const final = await result;
640
+ * ```
641
+ */
642
+ sendStream(prompt, options = {}) {
643
+ this._ensureOpen();
644
+ const maxSteps = options.maxSteps ?? 50;
645
+ const events = [];
646
+ let streamDone = false;
647
+ const waiters = [];
648
+ function pushEvent(event) {
649
+ events.push(event);
650
+ for (const w of waiters.splice(0))
651
+ w();
652
+ }
653
+ const resultPromise = (async () => {
654
+ const allSteps = [];
655
+ const allToolCalls = [];
656
+ let fullText = '';
657
+ let totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
658
+ try {
659
+ for (let step = 0; step < maxSteps; step++) {
660
+ if (options.signal?.aborted) {
661
+ pushEvent({ type: 'done', finishReason: 'cancelled' });
662
+ return { text: fullText, steps: allSteps, toolCalls: allToolCalls, usage: totalUsage, finishReason: 'cancelled' };
663
+ }
664
+ const messages = step === 0 ? [{ role: 'user', content: prompt }] : [];
665
+ const stream = this._client.streamGenerate(this.id, messages);
666
+ let stepText = '';
667
+ const stepToolCalls = [];
668
+ let stepFinishReason;
669
+ for await (const chunk of stream) {
670
+ if (chunk.content) {
671
+ fullText += chunk.content;
672
+ stepText += chunk.content;
673
+ pushEvent({ type: 'text', content: chunk.content });
674
+ }
675
+ if (chunk.toolCall) {
676
+ stepToolCalls.push(chunk.toolCall);
677
+ const args = chunk.toolCall.arguments ? JSON.parse(chunk.toolCall.arguments) : {};
678
+ pushEvent({ type: 'tool_call', toolName: chunk.toolCall.name, args, toolCallId: chunk.toolCall.id });
679
+ }
680
+ if (chunk.finishReason) {
681
+ stepFinishReason = chunk.finishReason;
682
+ }
683
+ }
684
+ allToolCalls.push(...stepToolCalls);
685
+ // Execute client-side tools
686
+ const stepToolResults = [];
687
+ if (options.tools) {
688
+ for (const tc of stepToolCalls) {
689
+ if (tc.name in options.tools) {
690
+ const toolDef = options.tools[tc.name];
691
+ const result = await executeClientTool(toolDef, tc, options.onToolCall);
692
+ stepToolResults.push(result);
693
+ tc.result = result;
694
+ pushEvent({ type: 'tool_result', toolCallId: tc.id, output: result.output, success: result.success });
695
+ }
696
+ }
697
+ }
698
+ const stepResult = {
699
+ stepIndex: step,
700
+ text: stepText,
701
+ toolCalls: stepToolCalls,
702
+ toolResults: stepToolResults,
703
+ usage: undefined,
704
+ finishReason: stepFinishReason,
705
+ };
706
+ allSteps.push(stepResult);
707
+ pushEvent({ type: 'step_finish', stepIndex: step, text: stepText, toolCalls: stepToolCalls });
708
+ if (options.onStepFinish) {
709
+ await options.onStepFinish(stepResult);
710
+ }
711
+ if (stepToolCalls.length === 0 || stepFinishReason !== 'tool_calls') {
712
+ break;
713
+ }
714
+ }
715
+ const finishReason = allSteps.length >= maxSteps ? 'max_steps' : 'stop';
716
+ pushEvent({ type: 'done', finishReason });
717
+ return { text: fullText, steps: allSteps, toolCalls: allToolCalls, usage: totalUsage, finishReason };
718
+ }
719
+ finally {
720
+ streamDone = true;
721
+ for (const w of waiters.splice(0))
722
+ w();
723
+ }
724
+ })();
725
+ // Suppress unhandled rejection on the background promise
726
+ resultPromise.catch(() => { });
727
+ const eventStream = {
728
+ [Symbol.asyncIterator]() {
729
+ let index = 0;
730
+ return {
731
+ async next() {
732
+ while (true) {
733
+ if (index < events.length) {
734
+ return { value: events[index++], done: false };
735
+ }
736
+ if (streamDone) {
737
+ return { value: undefined, done: true };
738
+ }
739
+ await new Promise((r) => waiters.push(r));
740
+ }
741
+ },
742
+ };
743
+ },
744
+ };
745
+ return { eventStream, result: resultPromise };
746
+ }
747
+ // --------------------------------------------------------------------------
748
+ // Delegation (Subagents)
749
+ // --------------------------------------------------------------------------
750
+ /**
751
+ * Delegate a task to a built-in or custom agent.
752
+ *
753
+ * Uses server-side delegation — the server creates a child session with
754
+ * the agent's restricted permissions and executes the task.
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * const result = await session.delegate('explore', 'Find all API endpoints');
759
+ * console.log(result.text);
760
+ * ```
761
+ */
762
+ async delegate(agent, task, options) {
763
+ this._ensureOpen();
764
+ const resp = await this._client.delegateToAgent(this.id, agent, task, {
765
+ maxSteps: options?.maxSteps ?? 50,
766
+ allowedTools: options?.allowedTools,
767
+ });
768
+ const steps = (resp.steps || []).map((s) => ({
769
+ stepIndex: s.stepIndex,
770
+ text: s.text,
771
+ toolCalls: s.toolCalls || [],
772
+ toolResults: s.toolResults || [],
773
+ usage: s.usage,
774
+ finishReason: s.finishReason,
775
+ }));
776
+ return {
777
+ text: resp.text,
778
+ steps,
779
+ toolCalls: resp.toolCalls || [],
780
+ usage: resp.usage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
781
+ finishReason: resp.finishReason || 'stop',
782
+ };
783
+ }
784
+ /**
785
+ * Delegate a task to a subagent with streaming.
786
+ *
787
+ * Uses server-side streaming delegation for real-time event updates.
788
+ */
789
+ delegateStream(agent, task, options) {
790
+ this._ensureOpen();
791
+ const events = [];
792
+ let streamDone = false;
793
+ const waiters = [];
794
+ function pushEvent(event) {
795
+ events.push(event);
796
+ for (const w of waiters.splice(0))
797
+ w();
798
+ }
799
+ const resultPromise = (async () => {
800
+ const allSteps = [];
801
+ const allToolCalls = [];
802
+ let fullText = '';
803
+ let totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
804
+ try {
805
+ const stream = this._client.streamDelegateToAgent(this.id, agent, task, {
806
+ maxSteps: options?.maxSteps ?? 50,
807
+ allowedTools: options?.allowedTools,
808
+ });
809
+ for await (const event of stream) {
810
+ const mapped = mapProtoAgenticEvent(event);
811
+ if (mapped)
812
+ pushEvent(mapped);
813
+ if (event.content)
814
+ fullText += event.content;
815
+ if (event.toolCall)
816
+ allToolCalls.push(event.toolCall);
817
+ if (event.usage)
818
+ totalUsage = event.usage;
819
+ }
820
+ const finishReason = 'stop';
821
+ pushEvent({ type: 'done', finishReason });
822
+ return { text: fullText, steps: allSteps, toolCalls: allToolCalls, usage: totalUsage, finishReason };
823
+ }
824
+ finally {
825
+ streamDone = true;
826
+ for (const w of waiters.splice(0))
827
+ w();
828
+ }
829
+ })();
830
+ resultPromise.catch(() => { });
831
+ const eventStream = {
832
+ [Symbol.asyncIterator]() {
833
+ let index = 0;
834
+ return {
835
+ async next() {
836
+ while (true) {
837
+ if (index < events.length) {
838
+ return { value: events[index++], done: false };
839
+ }
840
+ if (streamDone) {
841
+ return { value: undefined, done: true };
842
+ }
843
+ await new Promise((r) => waiters.push(r));
844
+ }
845
+ },
846
+ };
847
+ },
848
+ };
849
+ return { eventStream, result: resultPromise };
850
+ }
851
+ // --------------------------------------------------------------------------
852
+ // Lane Queue Management
853
+ // --------------------------------------------------------------------------
854
+ /** Set lane execution mode (internal/external/hybrid) */
855
+ async setLaneHandler(lane, config) {
856
+ this._ensureOpen();
857
+ const protoLane = laneNameToProto(lane);
858
+ const protoConfig = {
859
+ mode: handlerModeToProto(config.mode),
860
+ timeoutMs: config.timeout ?? 60_000,
861
+ };
862
+ await this._client.setLaneHandler(this.id, protoLane, protoConfig);
863
+ }
864
+ /** Get lane handler configuration */
865
+ async getLaneHandler(lane) {
866
+ this._ensureOpen();
867
+ const resp = await this._client.getLaneHandler(this.id, laneNameToProto(lane));
868
+ if (!resp.config)
869
+ return undefined;
870
+ const modeMap = {
871
+ TASK_HANDLER_MODE_INTERNAL: 'internal',
872
+ TASK_HANDLER_MODE_EXTERNAL: 'external',
873
+ TASK_HANDLER_MODE_HYBRID: 'hybrid',
874
+ };
875
+ return {
876
+ mode: modeMap[resp.config.mode] || 'internal',
877
+ timeout: resp.config.timeoutMs,
878
+ };
879
+ }
880
+ /** List tasks waiting for external processing */
881
+ async listPendingTasks() {
882
+ this._ensureOpen();
883
+ const resp = await this._client.listPendingExternalTasks(this.id);
884
+ return resp.tasks;
885
+ }
886
+ /** Complete an external task */
887
+ async completeTask(taskId, result) {
888
+ this._ensureOpen();
889
+ await this._client.completeExternalTask(this.id, taskId, result.success, result.output, result.error);
890
+ }
891
+ // --------------------------------------------------------------------------
892
+ // Skills Management
893
+ // --------------------------------------------------------------------------
894
+ /** Load skills from a directory (uses server-side batch loading) */
895
+ async loadSkills(dir, recursive = true) {
896
+ this._ensureOpen();
897
+ const resp = await this._client.loadSkillsFromDir(this.id, dir, recursive);
898
+ return resp.loadedSkills || [];
899
+ }
900
+ /** Load a single skill by name */
901
+ async loadSkill(name, content) {
902
+ this._ensureOpen();
903
+ await this._client.loadSkill(this.id, name, content);
904
+ }
905
+ /** Add an inline skill definition */
906
+ async addSkill(skill) {
907
+ this._ensureOpen();
908
+ await this._client.loadSkill(this.id, skill.name, skill.content);
909
+ }
910
+ /** Unload a skill */
911
+ async unloadSkill(name) {
912
+ this._ensureOpen();
913
+ await this._client.unloadSkill(this.id, name);
914
+ }
915
+ /** List available skills */
916
+ async listSkills() {
917
+ this._ensureOpen();
918
+ const resp = await this._client.listSkills(this.id);
919
+ return resp.skills.map((s) => ({
920
+ name: s.name,
921
+ description: s.description,
922
+ loaded: true,
923
+ source: 'builtin',
924
+ }));
925
+ }
926
+ // --------------------------------------------------------------------------
927
+ // Built-in Agents
928
+ // --------------------------------------------------------------------------
929
+ /** List available agents (built-in + custom) */
930
+ async listAgents() {
931
+ this._ensureOpen();
932
+ return [
933
+ { name: 'explore', description: 'Read-only codebase exploration', mode: 'subagent' },
934
+ { name: 'plan', description: 'Read-only planning and analysis', mode: 'subagent' },
935
+ { name: 'general', description: 'Multi-step task execution', mode: 'subagent' },
936
+ ...this._customAgents.map((a) => ({
937
+ name: a.name,
938
+ description: a.description,
939
+ mode: 'subagent',
940
+ })),
941
+ ];
942
+ }
943
+ /** Register a custom agent */
944
+ registerAgent(def) {
945
+ this._ensureOpen();
946
+ this._customAgents.push(def);
947
+ }
948
+ // --------------------------------------------------------------------------
949
+ // HITL & Permissions
950
+ // --------------------------------------------------------------------------
951
+ /** Set HITL confirmation policy */
952
+ async setConfirmation(config) {
953
+ this._ensureOpen();
954
+ const policy = {
955
+ enabled: true,
956
+ requireConfirmTools: config.requireConfirmation || [],
957
+ autoApproveTools: config.autoApprove || [],
958
+ defaultTimeoutMs: config.timeout ?? 30_000,
959
+ timeoutAction: timeoutActionToProto(config.timeoutAction),
960
+ yoloLanes: [],
961
+ };
962
+ await this._client.setConfirmationPolicy(this.id, policy);
963
+ }
964
+ /** Set tool permission policy */
965
+ async setPermissions(config) {
966
+ this._ensureOpen();
967
+ const allow = [];
968
+ const deny = [];
969
+ const ask = [];
970
+ for (const rule of config.rules || []) {
971
+ const ruleStr = rule.pattern ? `${rule.tool}:${rule.pattern}` : rule.tool;
972
+ if (rule.action === 'allow')
973
+ allow.push({ rule: ruleStr });
974
+ else if (rule.action === 'deny')
975
+ deny.push({ rule: ruleStr });
976
+ else
977
+ ask.push({ rule: ruleStr });
978
+ }
979
+ const policy = {
980
+ enabled: true,
981
+ allow,
982
+ deny,
983
+ ask,
984
+ defaultDecision: permissionActionToProto(config.defaultAction || 'allow'),
985
+ };
986
+ await this._client.setPermissionPolicy(this.id, policy);
987
+ }
988
+ /** Respond to a confirmation request */
989
+ async confirm(confirmationId, approved, reason) {
990
+ this._ensureOpen();
991
+ await this._client.confirmToolExecution(this.id, confirmationId, approved, reason);
992
+ }
993
+ // --------------------------------------------------------------------------
994
+ // Observability & Stats
995
+ // --------------------------------------------------------------------------
996
+ /** Get session statistics (tokens, cost, message count, tool calls) */
997
+ async getStats() {
998
+ this._ensureOpen();
999
+ const [ctx, cost] = await Promise.all([
1000
+ this._client.getContextUsage(this.id),
1001
+ this._client.getCostSummary({ sessionId: this.id }),
1002
+ ]);
1003
+ return {
1004
+ totalTokens: cost.totalTokens,
1005
+ promptTokens: cost.totalPromptTokens,
1006
+ completionTokens: cost.totalCompletionTokens,
1007
+ totalCost: cost.totalCostUsd,
1008
+ messageCount: ctx.usage?.messageCount ?? 0,
1009
+ toolCallCount: cost.callCount,
1010
+ };
1011
+ }
1012
+ /** Get per-tool execution metrics */
1013
+ async getToolMetrics() {
1014
+ this._ensureOpen();
1015
+ const resp = await this._client.getToolMetrics(this.id);
1016
+ const result = {};
1017
+ for (const t of resp.tools) {
1018
+ result[t.toolName] = t;
1019
+ }
1020
+ return result;
1021
+ }
1022
+ /** Get cost breakdown by model and day */
1023
+ async getCostSummary() {
1024
+ this._ensureOpen();
1025
+ return this._client.getCostSummary({ sessionId: this.id });
1026
+ }
1027
+ /** Get per-lane queue statistics */
1028
+ async getQueueStats() {
1029
+ this._ensureOpen();
1030
+ const resp = await this._client.getQueueStats(this.id);
1031
+ const mapLane = (lane) => ({
1032
+ pending: lane?.pending ?? 0,
1033
+ active: lane?.active ?? 0,
1034
+ external: lane?.external ?? 0,
1035
+ completed: lane?.completed ?? 0,
1036
+ failed: lane?.failed ?? 0,
1037
+ });
1038
+ return {
1039
+ control: mapLane(resp.control),
1040
+ query: mapLane(resp.query),
1041
+ execute: mapLane(resp.execute),
1042
+ generate: mapLane(resp.generate),
1043
+ deadLetters: resp.deadLetters ?? 0,
1044
+ };
1045
+ }
1046
+ /** Update session configuration (cannot change model or workspace) */
1047
+ async configure(options) {
1048
+ this._ensureOpen();
1049
+ const resp = await this._client.getSession(this.id);
1050
+ const existing = resp.session?.config;
1051
+ if (existing) {
1052
+ await this._client.configureSession(this.id, {
1053
+ ...existing,
1054
+ autoCompact: options.autoCompact ?? existing.autoCompact,
1055
+ });
1056
+ }
1057
+ }
1058
+ /** Subscribe to real-time agent events for this session */
1059
+ subscribeEvents(eventTypes) {
1060
+ this._ensureOpen();
1061
+ return this._client.subscribeEvents(this.id, eventTypes);
1062
+ }
1063
+ // --------------------------------------------------------------------------
1064
+ // Lifecycle
1065
+ // --------------------------------------------------------------------------
1066
+ /** Close the session and release server resources */
1067
+ async close() {
1068
+ if (this._closed)
1069
+ return;
1070
+ this._closed = true;
1071
+ try {
1072
+ await this._client.destroySession(this.id);
1073
+ }
1074
+ catch {
1075
+ // Ignore cleanup errors
1076
+ }
1077
+ }
1078
+ /** Support `await using session = ...` for automatic cleanup */
1079
+ async [Symbol.asyncDispose]() {
1080
+ await this.close();
1081
+ }
1082
+ /** Whether this session has been closed */
1083
+ get closed() {
1084
+ return this._closed;
1085
+ }
1086
+ _ensureOpen() {
1087
+ if (this._closed) {
1088
+ throw new Error(`Session ${this.id} has been closed`);
1089
+ }
1090
+ }
1091
+ }
1092
+ //# sourceMappingURL=session.js.map