@realtimex/sdk 1.4.4 → 1.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realtimex/sdk",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "SDK for building Local Apps that integrate with RealtimeX",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -2,12 +2,12 @@
2
2
  name: realtimex-moderator-sdk
3
3
  description: Control and interact with the RealTimeX application through its Node.js SDK. This skill should be used when users want to manage workspaces, threads, agents, activities, LLM chat, vector store, MCP tools, ACP agent sessions, TTS/STT, or any other RealTimeX platform feature via the API. All method signatures are verified against the SDK source code.
4
4
  generated: 2026-03-27
5
- sdk_version: 1.4.4
5
+ sdk_version: 1.4.5
6
6
  ---
7
7
 
8
8
  # RealTimeX Moderator (SDK Source-Verified)
9
9
 
10
- Interact with the RealTimeX desktop app (`http://localhost:3001`) using `@realtimex/sdk` **v1.4.4** in Developer Mode (API Key).
10
+ Interact with the RealTimeX desktop app (`http://localhost:3001`) using `@realtimex/sdk` **v1.4.5** in Developer Mode (API Key).
11
11
 
12
12
  > Auto-generated from the `@realtimex/sdk` TypeScript source.
13
13
  > Refresh: `node scripts/generate-skill.mjs --force` from the SDK repo root.
@@ -41,7 +41,7 @@ node "$SKILL" agents $ENV
41
41
  node "$SKILL" workspaces $ENV
42
42
  node "$SKILL" threads <workspace-slug> $ENV
43
43
  node "$SKILL" trigger-agent <agent> <workspace> <msg> $ENV
44
- node "$SKILL" acp-chat qwen "question" --cwd=<path> $ENV
44
+ node "$SKILL" acp-chat qwen-cli "question" --cwd=<path> $ENV
45
45
  node "$SKILL" llm-chat "message" $ENV
46
46
  node "$SKILL" activities --status=pending $ENV
47
47
  node "$SKILL" mcp-servers $ENV
@@ -58,6 +58,125 @@ const { sdk, apiKey } = await initSDK({ envDir: process.cwd() });
58
58
 
59
59
  ---
60
60
 
61
+ ## ACP Session Management
62
+
63
+ ACP sessions are persistent agent processes. **Always reuse sessions** across turns instead of spawning a new process for every message — it preserves context and is far more efficient.
64
+
65
+ ### Smart `acp-chat` (recommended)
66
+
67
+ `acp-chat` automatically finds or creates a session using this priority:
68
+
69
+ 1. `--session=<key>` → validate and reuse that exact session
70
+ 2. `listSessions()` → find a compatible active session (same `agent_id`, optional `cwd` match)
71
+ 3. `createSession()` → spawn a new agent process only if none available
72
+
73
+ ```bash
74
+ # First call — creates a new session, prints session key at end
75
+ node "$SKILL" acp-chat qwen-cli "build a website" --cwd=~/projects/myapp $ENV
76
+
77
+ # Subsequent calls — reuses the existing session automatically
78
+ node "$SKILL" acp-chat qwen-cli "add a login page" $ENV
79
+
80
+ # Pin to a specific session
81
+ node "$SKILL" acp-chat qwen-cli "fix the bug" --session=<key> $ENV
82
+
83
+ # Force a fresh session
84
+ node "$SKILL" acp-chat qwen-cli "start over" --new $ENV
85
+
86
+ # Close session after this turn
87
+ node "$SKILL" acp-chat qwen-cli "done for now" --close $ENV
88
+ ```
89
+
90
+ ### Manual Session Lifecycle
91
+
92
+ ```bash
93
+ # Spawn a session explicitly — save the session_key
94
+ node "$SKILL" acp-session-create qwen-cli --cwd=~/projects/myapp $ENV
95
+
96
+ # Inspect session state
97
+ node "$SKILL" acp-session-get <session-key> $ENV
98
+
99
+ # List all active sessions
100
+ node "$SKILL" acp-sessions $ENV
101
+
102
+ # Patch runtime options (applied on next turn)
103
+ node "$SKILL" acp-session-patch <session-key> --cwd=~/projects/other $ENV
104
+
105
+ # Send a turn on an existing session (sync, no streaming)
106
+ node "$SKILL" acp-send <session-key> "what files did you create?" $ENV
107
+
108
+ # Stream a turn on an existing session (with permission handling)
109
+ node "$SKILL" acp-stream <session-key> "run the tests" $ENV
110
+
111
+ # Cancel the active turn
112
+ node "$SKILL" acp-cancel <session-key> $ENV
113
+
114
+ # Manually resolve a permission request (while stream is active in another process)
115
+ node "$SKILL" acp-resolve <session-key> <request-id> <option-id> $ENV
116
+
117
+ # Close/terminate the session
118
+ node "$SKILL" acp-session-close <session-key> $ENV
119
+ ```
120
+
121
+ ### Permission Handling
122
+
123
+ `acp-chat` and `acp-stream` handle `permission_request` SSE events inline via `resolvePermission()`.
124
+
125
+ Control with `--policy-override`:
126
+ - `approve-all` *(default)* — auto-approve (picks the approve/allow/yes option)
127
+ - `deny-all` — auto-deny (picks the deny/cancel/no option)
128
+ - `prompt` — pause and ask you interactively via stdin
129
+
130
+ ```bash
131
+ # Auto-approve all tool permissions (default)
132
+ node "$SKILL" acp-chat qwen-cli "delete temp files" --policy-override=approve-all $ENV
133
+
134
+ # Ask before approving each permission
135
+ node "$SKILL" acp-chat qwen-cli "run npm install" --policy-override=prompt $ENV
136
+ ```
137
+
138
+ ### Custom Script Pattern
139
+
140
+ ```js
141
+ const { initSDK } = require('<SKILL_DIR>/scripts/lib/sdk-init');
142
+ const { sdk } = await initSDK({ envDir: process.cwd() });
143
+
144
+ // Find or reuse a session
145
+ let sessionKey;
146
+ const sessions = await sdk.acpAgent.listSessions();
147
+ const match = sessions.find(s => s.agent_id === 'qwen-cli' && s.state !== 'closed');
148
+ if (match) {
149
+ sessionKey = match.session_key;
150
+ } else {
151
+ const session = await sdk.acpAgent.createSession({
152
+ agent_id: 'qwen-cli',
153
+ cwd: '/path/to/project',
154
+ approvalPolicy: 'approve-all',
155
+ });
156
+ sessionKey = session.session_key;
157
+ }
158
+
159
+ // Stream a turn, resolving permissions inline
160
+ for await (const event of sdk.acpAgent.streamChat(sessionKey, 'build a website')) {
161
+ if (event.type === 'text_delta' && event.data.type !== 'thinking') {
162
+ process.stdout.write(event.data.text ?? '');
163
+ }
164
+ if (event.type === 'permission_request') {
165
+ const req = event.data;
166
+ const opt = req.options?.[0];
167
+ if (opt) {
168
+ await sdk.acpAgent.resolvePermission(sessionKey, {
169
+ requestId: req.requestId,
170
+ optionId: opt.id || opt.optionId,
171
+ outcome: 'approved',
172
+ });
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ ---
179
+
61
180
  ## Critical Rules (source-detected)
62
181
 
63
182
  | # | Issue |
@@ -84,7 +203,8 @@ Full fixes in `references/known-issues.md`.
84
203
  - **Metadata methods** (`getAgents`, `getWorkspaces`, etc.) live on `sdk.api.*`, not `sdk.*`
85
204
  - **`sdk.webhook.triggerAgent()`** sends wrong event type — always use raw fetch with `event: "trigger-agent"`
86
205
  - **`sdk.task`** methods: `start(uuid)`, `complete(uuid, result)`, `fail(uuid, "error")` — positional args
87
- - **ACP sessions** need `approvalPolicy: 'approve-all'` for autonomous scripts
206
+ - **ACP sessions** are persistent — reuse them across turns via `listSessions()` + `streamChat()`
207
+ - **`resolvePermission()`** must be called while the `streamChat` SSE stream is still active
88
208
  - **SDK env vars:** `RTX_API_KEY` (dev), `RTX_APP_ID` (prod), `RTX_APP_NAME`
89
209
 
90
210
  ## References
@@ -1,6 +1,6 @@
1
1
  # RealTimeX SDK — API Reference
2
2
 
3
- > Auto-generated from `@realtimex/sdk` source · v**1.4.4** · 2026-03-27
3
+ > Auto-generated from `@realtimex/sdk` source · v**1.4.5** · 2026-03-27
4
4
 
5
5
  **Package:** `@realtimex/sdk` (CJS) · **Server:** `http://localhost:3001`
6
6
  **Developer Mode auth:** `Authorization: Bearer <apiKey>`
@@ -1,6 +1,6 @@
1
1
  # Known Issues — Source-Detected
2
2
 
3
- > Auto-generated by `scripts/generate-skill.mjs` · SDK **1.4.4** · 2026-03-27
3
+ > Auto-generated by `scripts/generate-skill.mjs` · SDK **1.4.5** · 2026-03-27
4
4
 
5
5
  Run `node scripts/generate-skill.mjs --force` after SDK source changes to refresh.
6
6
 
@@ -311,42 +311,386 @@ CMD['acp-agents'] = async () => {
311
311
  const { sdk } = await getSDK();
312
312
  printTable(await sdk.acpAgent.listAgents({ includeModels: flags.models === 'true' }), ['id', 'label', 'status', 'authReady', 'installed']);
313
313
  };
314
+
315
+ // -- acp-sessions -----------------------------------------------------------
316
+ // Source: AcpAgentModule.listSessions() → AcpSessionStatus[]
314
317
  CMD['acp-sessions'] = async () => {
315
318
  const { sdk } = await getSDK();
316
319
  print(await sdk.acpAgent.listSessions());
317
320
  };
318
321
 
322
+ // -- acp-session-create -----------------------------------------------------
323
+ // Source: AcpAgentModule.createSession(options) → AcpSession { session_key, agent_id, state, ... }
324
+ // Prints session_key so callers can pass --session=<key> on subsequent calls.
325
+ CMD['acp-session-create'] = async () => {
326
+ const [agentId] = cmdArgs;
327
+ if (!agentId) {
328
+ console.error('Usage: rtx.js acp-session-create <agent-id> [--cwd=<path>] [--model=<m>] [--policy=approve-all|approve-reads|deny-all]');
329
+ process.exit(1);
330
+ }
331
+ const { sdk } = await getSDK();
332
+ const opts = {
333
+ agent_id: agentId,
334
+ cwd: flags.cwd || process.cwd(),
335
+ approvalPolicy: flags.policy || 'approve-all',
336
+ };
337
+ if (flags.model) opts.model = flags.model;
338
+ const session = await sdk.acpAgent.createSession(opts);
339
+ process.stderr.write('Session created.\n');
340
+ print(session);
341
+ };
342
+
343
+ // -- acp-session-get --------------------------------------------------------
344
+ // Source: AcpAgentModule.getSession(sessionKey) → AcpSessionStatus
345
+ CMD['acp-session-get'] = async () => {
346
+ const [sessionKey] = cmdArgs;
347
+ if (!sessionKey) { console.error('Usage: rtx.js acp-session-get <session-key>'); process.exit(1); }
348
+ const { sdk } = await getSDK();
349
+ print(await sdk.acpAgent.getSession(sessionKey));
350
+ };
351
+
352
+ // -- acp-session-patch ------------------------------------------------------
353
+ // Source: AcpAgentModule.patchSession(sessionKey, patch) → void
354
+ // patch fields: model?, cwd?, timeoutSeconds?, runtimeMode?, approvalPolicy?
355
+ CMD['acp-session-patch'] = async () => {
356
+ const [sessionKey] = cmdArgs;
357
+ if (!sessionKey) {
358
+ console.error('Usage: rtx.js acp-session-patch <session-key> [--cwd=<path>] [--model=<m>] [--policy=<p>] [--timeout=<s>]');
359
+ process.exit(1);
360
+ }
361
+ const { sdk } = await getSDK();
362
+ const patch = {};
363
+ if (flags.cwd) patch.cwd = flags.cwd;
364
+ if (flags.model) patch.model = flags.model;
365
+ if (flags.policy) patch.approvalPolicy = flags.policy;
366
+ if (flags.timeout) patch.timeoutSeconds = Number(flags.timeout);
367
+ await sdk.acpAgent.patchSession(sessionKey, patch);
368
+ console.log('Session patched.');
369
+ };
370
+
371
+ // -- acp-session-close ------------------------------------------------------
372
+ // Source: AcpAgentModule.closeSession(sessionKey, reason?) → void
373
+ CMD['acp-session-close'] = async () => {
374
+ const [sessionKey, ...reasonParts] = cmdArgs;
375
+ if (!sessionKey) { console.error('Usage: rtx.js acp-session-close <session-key> [<reason>]'); process.exit(1); }
376
+ const { sdk } = await getSDK();
377
+ await sdk.acpAgent.closeSession(sessionKey, reasonParts.join(' ') || undefined);
378
+ console.log('Session closed.');
379
+ };
380
+
381
+ // -- acp-cancel -------------------------------------------------------------
382
+ // Source: AcpAgentModule.cancelTurn(sessionKey, reason?) → void
383
+ CMD['acp-cancel'] = async () => {
384
+ const [sessionKey, ...reasonParts] = cmdArgs;
385
+ if (!sessionKey) { console.error('Usage: rtx.js acp-cancel <session-key> [<reason>]'); process.exit(1); }
386
+ const { sdk } = await getSDK();
387
+ await sdk.acpAgent.cancelTurn(sessionKey, reasonParts.join(' ') || undefined);
388
+ console.log('Turn cancelled.');
389
+ };
390
+
391
+ // -- acp-resolve ------------------------------------------------------------
392
+ // Source: AcpAgentModule.resolvePermission(sessionKey, { requestId, optionId, outcome? }) → void
393
+ // Must be called while streamChat SSE is still active for that session.
394
+ CMD['acp-resolve'] = async () => {
395
+ const [sessionKey, requestId, optionId] = cmdArgs;
396
+ if (!sessionKey || !requestId || !optionId) {
397
+ console.error('Usage: rtx.js acp-resolve <session-key> <request-id> <option-id> [--outcome=approved]');
398
+ process.exit(1);
399
+ }
400
+ const { sdk } = await getSDK();
401
+ await sdk.acpAgent.resolvePermission(sessionKey, {
402
+ requestId,
403
+ optionId,
404
+ outcome: flags.outcome || 'approved',
405
+ });
406
+ console.log('Permission resolved.');
407
+ };
408
+
409
+ // -- acp-send ---------------------------------------------------------------
410
+ // Source: AcpAgentModule.chat(sessionKey, message) → AcpChatResponse (sync, waits for full reply)
411
+ // Reuses existing session; never creates a new one.
412
+ CMD['acp-send'] = async () => {
413
+ const [sessionKey, ...msgParts] = cmdArgs;
414
+ const message = msgParts.join(' ');
415
+ if (!sessionKey || !message) {
416
+ console.error('Usage: rtx.js acp-send <session-key> <message>');
417
+ process.exit(1);
418
+ }
419
+ const { sdk } = await getSDK();
420
+ const res = await sdk.acpAgent.chat(sessionKey, message);
421
+ console.log(res.text ?? JSON.stringify(res));
422
+ };
423
+
424
+ // -- acp-stream -------------------------------------------------------------
425
+ // Source: AcpAgentModule.streamChat(sessionKey, message) — SSE stream on existing session.
426
+ // Handles permission_request events automatically via resolvePermission().
427
+ // Reuses existing session; never creates a new one.
428
+ CMD['acp-stream'] = async () => {
429
+ const [sessionKey, ...msgParts] = cmdArgs;
430
+ const message = msgParts.join(' ');
431
+ if (!sessionKey || !message) {
432
+ console.error('Usage: rtx.js acp-stream <session-key> <message> [--thoughts] [--quiet]');
433
+ process.exit(1);
434
+ }
435
+ const { sdk } = await getSDK();
436
+ await acpStreamWithPermissions(sdk, sessionKey, message);
437
+ };
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // ACP helpers
441
+ // ---------------------------------------------------------------------------
442
+
443
+ /**
444
+ * Extract a stable string id from a permission option entry.
445
+ * Options can be raw numbers, strings, or objects with numeric/string id fields.
446
+ * IMPORTANT: must use != null (not ||) to handle id === 0 (falsy but valid).
447
+ */
448
+ function optionId(opt, index) {
449
+ if (opt == null) return String(index);
450
+ if (typeof opt === 'number') return String(opt);
451
+ if (typeof opt === 'string') return opt;
452
+ if (opt.id != null) return String(opt.id);
453
+ if (opt.optionId != null) return String(opt.optionId);
454
+ if (opt.value != null) return String(opt.value);
455
+ return String(index);
456
+ }
457
+
458
+ /** Extract a human-readable label from a permission option entry. */
459
+ function optionLabel(opt, index) {
460
+ if (opt == null) return String(index);
461
+ if (typeof opt === 'number') return String(opt);
462
+ if (typeof opt === 'string') return opt;
463
+ return opt.label || opt.name || opt.description || optionId(opt, index);
464
+ }
465
+
466
+ /**
467
+ * Stream a chat turn on an existing session, auto-resolving permission_request
468
+ * events via sdk.acpAgent.resolvePermission().
469
+ *
470
+ * Permission resolution strategy (--policy-override flag):
471
+ * approve-all (default) — approve: label-match → last option (typically "Yes/Always")
472
+ * deny-all — deny: label-match → first option (typically "No/Cancel")
473
+ * prompt — pause and ask the user interactively via stdin
474
+ *
475
+ * Why last-option for approve-all fallback:
476
+ * Qwen/Claude-style dialogs are ordered [Deny=0, Approve-once=1, Approve-always=2].
477
+ * Picking options[0] = Deny. We want the last approve variant when no label matches.
478
+ */
479
+ async function acpStreamWithPermissions(sdk, sessionKey, message) {
480
+ const policyOverride = flags['policy-override'] || 'approve-all';
481
+
482
+ for await (const event of sdk.acpAgent.streamChat(sessionKey, message)) {
483
+ switch (event.type) {
484
+
485
+ case 'text_delta': {
486
+ if (event.data && event.data.type === 'thinking') {
487
+ if (flags.thoughts) process.stderr.write('[thought] ' + String(event.data.text ?? '') + '\n');
488
+ } else {
489
+ process.stdout.write(String(event.data?.text ?? event.data ?? ''));
490
+ }
491
+ break;
492
+ }
493
+
494
+ case 'permission_request': {
495
+ const req = event.data || {};
496
+ const requestId = req.requestId || req.id || req.request_id;
497
+ const options = Array.isArray(req.options) ? req.options : [];
498
+
499
+ // Always show the full raw payload — critical for diagnosing option structure
500
+ if (!flags.quiet) {
501
+ process.stderr.write('\n[Permission Request] id=' + requestId + '\n');
502
+ if (req.title) process.stderr.write(' title: ' + req.title + '\n');
503
+ if (req.description) process.stderr.write(' details: ' + req.description + '\n');
504
+ options.forEach((o, i) => {
505
+ process.stderr.write(' [' + optionId(o, i) + '] ' + optionLabel(o, i) + '\n');
506
+ });
507
+ if (flags.debug) {
508
+ process.stderr.write(' raw: ' + JSON.stringify(req) + '\n');
509
+ }
510
+ }
511
+
512
+ let chosenId = null;
513
+
514
+ if (policyOverride === 'prompt') {
515
+ const readline = require('readline');
516
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
517
+ const answer = await new Promise(resolve => {
518
+ rl.question(' Choose option id (Enter = last/approve): ', ans => { rl.close(); resolve(ans.trim()); });
519
+ });
520
+ // Use typed answer, or fall back to last option (approve-always)
521
+ chosenId = answer || (options.length ? optionId(options[options.length - 1], options.length - 1) : null);
522
+
523
+ } else if (policyOverride === 'deny-all') {
524
+ // Label-match for deny keywords first, then fall back to first option (index 0 = No/Cancel)
525
+ const idx = options.findIndex(o => /deny|cancel|no|reject/i.test(optionLabel(o, 0)));
526
+ const pick = idx >= 0 ? options[idx] : options[0];
527
+ const pickIdx = idx >= 0 ? idx : 0;
528
+ if (pick != null) chosenId = optionId(pick, pickIdx);
529
+
530
+ } else {
531
+ // approve-all: label-match first, then LAST option (not first — first is typically Deny)
532
+ const idx = options.findIndex(o => /approve|allow|yes|confirm|ok|always/i.test(optionLabel(o, 0)));
533
+ if (idx >= 0) {
534
+ chosenId = optionId(options[idx], idx);
535
+ } else if (options.length > 0) {
536
+ // Last option is safest approve fallback (e.g. "Yes, don't ask again" / "Approve always")
537
+ const lastIdx = options.length - 1;
538
+ chosenId = optionId(options[lastIdx], lastIdx);
539
+ }
540
+ }
541
+
542
+ if (chosenId !== null && requestId) {
543
+ if (!flags.quiet) process.stderr.write(' → resolving with option: ' + chosenId + '\n');
544
+ await sdk.acpAgent.resolvePermission(sessionKey, {
545
+ requestId,
546
+ optionId: chosenId,
547
+ outcome: policyOverride === 'deny-all' ? 'denied' : 'approved',
548
+ });
549
+ } else {
550
+ if (!flags.quiet) process.stderr.write(' → no options to resolve\n');
551
+ }
552
+ break;
553
+ }
554
+
555
+ case 'tool_call': {
556
+ if (!flags.quiet) {
557
+ const tool = event.data?.tool || event.data?.name || JSON.stringify(event.data);
558
+ process.stderr.write('\n[tool: ' + tool + ']\n');
559
+ }
560
+ break;
561
+ }
562
+
563
+ case 'status': {
564
+ if (!flags.quiet && event.data?.message) {
565
+ process.stderr.write('[status] ' + event.data.message + '\n');
566
+ }
567
+ break;
568
+ }
569
+
570
+ case 'error': {
571
+ process.stderr.write('\n[Error] ' + (event.data?.message || JSON.stringify(event.data)) + '\n');
572
+ break;
573
+ }
574
+
575
+ case 'done':
576
+ case 'close':
577
+ process.stdout.write('\n');
578
+ break;
579
+
580
+ default:
581
+ if (flags.debug) process.stderr.write('[event:' + event.type + '] ' + JSON.stringify(event.data) + '\n');
582
+ break;
583
+ }
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Find a reusable session for agentId+cwd, or create a fresh one.
589
+ *
590
+ * Resolution order:
591
+ * 1. --session=<key> → validate it exists and is not closed/stale, use it
592
+ * 2. listSessions() → find first session matching agent_id (and cwd if --cwd given)
593
+ * whose state is not 'closed' or 'stale'
594
+ * 3. createSession() → spawn a new process
595
+ *
596
+ * Pass --new to always create a fresh session regardless.
597
+ */
598
+ async function findOrCreateSession(sdk, agentId) {
599
+ const cwd = flags.cwd || process.cwd();
600
+ const policy = flags.policy || 'approve-all';
601
+
602
+ // Force-new
603
+ if (flags.new) {
604
+ process.stderr.write('Creating new ACP session for "' + agentId + '"...\n');
605
+ const s = await sdk.acpAgent.createSession({ agent_id: agentId, cwd, approvalPolicy: policy, ...(flags.model ? { model: flags.model } : {}) });
606
+ process.stderr.write('Session: ' + s.session_key + '\n');
607
+ return s.session_key;
608
+ }
609
+
610
+ // Explicit key
611
+ if (flags.session) {
612
+ try {
613
+ const status = await sdk.acpAgent.getSession(flags.session);
614
+ const state = status?.state || status?.runtime_options?.state;
615
+ if (state !== 'closed' && state !== 'stale') {
616
+ process.stderr.write('Reusing session (--session): ' + flags.session + '\n');
617
+ return flags.session;
618
+ }
619
+ process.stderr.write('Session ' + flags.session + ' is ' + state + ', will create new.\n');
620
+ } catch (e) {
621
+ process.stderr.write('Session lookup failed (' + (e.message || e) + '), will create new.\n');
622
+ }
623
+ }
624
+
625
+ // Search existing sessions
626
+ try {
627
+ const sessions = await sdk.acpAgent.listSessions();
628
+ if (Array.isArray(sessions)) {
629
+ for (const s of sessions) {
630
+ const key = s.session_key || s.key;
631
+ const sid = s.agent_id || s.agentId;
632
+ const state = s.state || s.runtime_options?.state;
633
+ const sCwd = s.cwd || s.runtime_options?.cwd;
634
+ if (state === 'closed' || state === 'stale') continue;
635
+ if (sid && sid !== agentId) continue;
636
+ if (flags.cwd && sCwd && sCwd !== cwd) continue;
637
+ if (key) {
638
+ process.stderr.write('Reusing existing session: ' + key + (sid ? ' (' + sid + ')' : '') + '\n');
639
+ return key;
640
+ }
641
+ }
642
+ }
643
+ } catch (_) { /* listSessions not supported or empty — fall through */ }
644
+
645
+ // Create new
646
+ process.stderr.write('Creating new ACP session for "' + agentId + '"...\n');
647
+ const session = await sdk.acpAgent.createSession({
648
+ agent_id: agentId,
649
+ cwd,
650
+ approvalPolicy: policy,
651
+ ...(flags.model ? { model: flags.model } : {}),
652
+ });
653
+ process.stderr.write('Session: ' + session.session_key + '\n');
654
+ return session.session_key;
655
+ }
656
+
319
657
  // -- acp-chat ---------------------------------------------------------------
320
- // Source: AcpAgentModule.createSession + streamChat
321
- // approvalPolicy: 'approve-all' | 'approve-reads' | 'deny-all'
322
- // StreamEvent types: text_delta | status | tool_call | permission_request | done | error | close
323
- // text_delta.data.type === 'thinking' → internal reasoning (not final output)
658
+ // Smart version: reuses existing sessions, handles permission_request via resolvePermission().
659
+ //
660
+ // Flags:
661
+ // --session=<key> Reuse this specific session key
662
+ // --new Always create a fresh session
663
+ // --cwd=<path> Working directory (used for new session + matching)
664
+ // --model=<m> Model override
665
+ // --policy=approve-all approvalPolicy for new sessions
666
+ // --policy-override=<p> Runtime permission decision: approve-all|deny-all|prompt
667
+ // --close Close the session after this turn
668
+ // --thoughts Print reasoning/thinking tokens to stderr
669
+ // --quiet Suppress tool/status/permission stderr output
670
+ // --debug Print all unhandled SSE event types
324
671
  CMD['acp-chat'] = async () => {
325
672
  const [agentId, ...msgParts] = cmdArgs;
326
673
  const message = msgParts.join(' ');
327
674
  if (!agentId || !message) {
328
- console.error('Usage: rtx.js acp-chat <agent-id> <message> [--cwd=<path>] [--model=<m>] [--policy=approve-all]');
675
+ console.error('Usage: rtx.js acp-chat <agent-id> <message>\n' +
676
+ ' [--session=<key>] [--new] [--cwd=<path>] [--model=<m>]\n' +
677
+ ' [--policy=approve-all] [--policy-override=approve-all|deny-all|prompt]\n' +
678
+ ' [--close] [--thoughts] [--quiet] [--debug]');
329
679
  process.exit(1);
330
680
  }
331
681
  const { sdk } = await getSDK();
332
- const sessionOpts = { agent_id: agentId, cwd: flags.cwd || process.cwd(), approvalPolicy: flags.policy || 'approve-all' };
333
- if (flags.model) sessionOpts.model = flags.model;
334
- process.stderr.write('Creating ACP session for "' + agentId + '"...\n');
335
- const session = await sdk.acpAgent.createSession(sessionOpts);
682
+ const sessionKey = await findOrCreateSession(sdk, agentId);
683
+
336
684
  try {
337
- for await (const event of sdk.acpAgent.streamChat(session.session_key, message)) {
338
- if (event.type === 'text_delta') {
339
- if (event.data.type === 'thinking') { if (flags.thoughts) process.stderr.write('[thought] ' + event.data.text + '\n'); }
340
- else process.stdout.write(String(event.data.text ?? ''));
341
- } else if (event.type === 'tool_call') {
342
- if (!flags.quiet) process.stderr.write('\n[tool: ' + event.data.tool + ']\n');
343
- } else if (event.type === 'error') {
344
- process.stderr.write('\nError: ' + event.data.message + '\n');
345
- } else if (event.type === 'done' || event.type === 'close') {
346
- process.stdout.write('\n');
347
- }
685
+ await acpStreamWithPermissions(sdk, sessionKey, message);
686
+ } finally {
687
+ if (flags.close) {
688
+ await sdk.acpAgent.closeSession(sessionKey, 'chat complete').catch(() => {});
689
+ process.stderr.write('Session closed.\n');
690
+ } else {
691
+ process.stderr.write('Session key (reuse with --session=' + sessionKey + ')\n');
348
692
  }
349
- } finally { await sdk.acpAgent.closeSession(session.session_key).catch(() => {}); }
693
+ }
350
694
  };
351
695
 
352
696
  // -- tts-providers / stt-providers ------------------------------------------
@@ -453,11 +797,55 @@ sdk.mcp.*:
453
797
  mcp-tools <server> [--provider]
454
798
  mcp-exec <server> <tool> [<args-json>] [--provider]
455
799
 
456
- sdk.acpAgent.*:
800
+ sdk.acpAgent.* — Session Management:
457
801
  acp-agents [--models=true]
802
+ List available ACP CLI agents.
803
+
458
804
  acp-sessions
805
+ List all active sessions owned by this app.
806
+
807
+ acp-session-create <agent-id>
808
+ [--cwd=<path>] [--model=<m>] [--policy=approve-all|approve-reads|deny-all]
809
+ Spawn a new agent process. Prints session_key for reuse.
810
+
811
+ acp-session-get <session-key>
812
+ Get session status and runtime options.
813
+
814
+ acp-session-patch <session-key>
815
+ [--cwd=<path>] [--model=<m>] [--policy=<p>] [--timeout=<s>]
816
+ Update runtime options (applied on next turn).
817
+
818
+ acp-session-close <session-key> [<reason>]
819
+ Stop the agent process and close the session.
820
+
821
+ acp-cancel <session-key> [<reason>]
822
+ Cancel the currently active turn on a session.
823
+
824
+ acp-resolve <session-key> <request-id> <option-id> [--outcome=approved]
825
+ Manually resolve a pending permission request (while stream is active).
826
+
827
+ acp-send <session-key> <message>
828
+ Synchronous chat on an existing session (waits for full reply).
829
+
830
+ acp-stream <session-key> <message>
831
+ [--policy-override=approve-all|deny-all|prompt] [--thoughts] [--quiet] [--debug]
832
+ Streaming chat on an existing session. Auto-resolves permission_request events.
833
+
459
834
  acp-chat <agent-id> <message>
460
- [--cwd] [--model] [--policy=approve-all] [--thoughts] [--quiet]
835
+ [--session=<key>] Reuse a specific session key
836
+ [--new] Force-create a new session
837
+ [--cwd=<path>] Working directory (for new session and session matching)
838
+ [--model=<m>] Model override
839
+ [--policy=approve-all] approvalPolicy for newly created sessions
840
+ [--policy-override=approve-all|deny-all|prompt] Runtime permission handling
841
+ [--close] Close session after this turn
842
+ [--thoughts] Print reasoning tokens to stderr
843
+ [--quiet] Suppress tool/status/permission stderr output
844
+ [--debug] Print all unhandled SSE events
845
+
846
+ Smart session reuse: checks --session flag → searches listSessions() for a
847
+ compatible active session → creates new only if none found.
848
+ Handles permission_request events inline via resolvePermission().
461
849
 
462
850
  sdk.tts.* / sdk.stt.*:
463
851
  tts-providers / stt-providers