@exreve/exk 1.0.61 → 1.0.63
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/agentBackend.js +12 -0
- package/dist/cli/agentSession.js +253 -685
- package/dist/cli/claudeBackend.js +535 -0
- package/dist/cli/moduleMcpServer.js +50 -311
- package/dist/cli/piBackend.js +384 -0
- package/dist/cli/sessionHandlers.js +3 -2
- package/dist/cli/sharedTools.js +259 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +2 -1
package/dist/cli/agentSession.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { execSync, spawn } from 'child_process';
|
|
1
|
+
import { execSync } from 'child_process';
|
|
3
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
4
3
|
import { symlink as fsSymlink } from 'fs';
|
|
5
4
|
import { getSkillContent } from './skills/index.js';
|
|
6
5
|
import { isLocalModel, unwrapModelName, startOpenAIAdapter, getAdapterConfig } from './openaiAdapter.js';
|
|
7
6
|
import { ensureProxy } from './proxyManager.js';
|
|
8
|
-
import {
|
|
7
|
+
import { claudeBackend } from './claudeBackend.js';
|
|
9
8
|
import path from 'path';
|
|
10
9
|
import os from 'os';
|
|
11
|
-
import { createRequire } from 'module';
|
|
12
10
|
import { promisify } from 'util';
|
|
13
11
|
// ============ Session State Persistence ============
|
|
14
12
|
// Persists claudeSessionId to disk so context survives CLI restarts.
|
|
@@ -53,135 +51,8 @@ function deleteSessionState(sessionId) {
|
|
|
53
51
|
// Ignore cleanup errors
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Resolve path to the SDK's bundled cli.js.
|
|
58
|
-
* We resolve this ourselves so it works reliably on Windows when running from
|
|
59
|
-
* the PS1 install (ttc.cmd -> node ttc.js); the SDK's internal resolution via
|
|
60
|
-
* import.meta.url can fail or produce wrong paths in that context.
|
|
61
|
-
* CACHED: Path is resolved once at module load time for performance.
|
|
62
|
-
*/
|
|
63
|
-
function resolveSdkCliPath() {
|
|
64
|
-
try {
|
|
65
|
-
const req = typeof globalThis.require === 'function'
|
|
66
|
-
? globalThis.require
|
|
67
|
-
: createRequire(import.meta.url);
|
|
68
|
-
const pkgPath = req.resolve('@anthropic-ai/claude-agent-sdk/package.json');
|
|
69
|
-
const cliPath = path.join(path.dirname(pkgPath), 'cli.js');
|
|
70
|
-
return existsSync(cliPath) ? cliPath : undefined;
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Cache the resolved Claude executable path at module load time
|
|
77
|
-
const CACHED_CLAUDE_PATH = (() => {
|
|
78
|
-
const envPath = process.env.TTC_CLAUDE_PATH;
|
|
79
|
-
if (envPath)
|
|
80
|
-
return envPath;
|
|
81
|
-
const sdkPath = resolveSdkCliPath();
|
|
82
|
-
if (sdkPath)
|
|
83
|
-
return sdkPath;
|
|
84
|
-
const localPath = path.join(os.homedir(), '.local', 'bin', 'claude');
|
|
85
|
-
if (existsSync(localPath))
|
|
86
|
-
return localPath;
|
|
87
|
-
return undefined;
|
|
88
|
-
})();
|
|
89
54
|
// Promisify symlink for async use
|
|
90
55
|
const symlinkAsync = promisify(fsSymlink);
|
|
91
|
-
// Helper function to extract tool name from result structure
|
|
92
|
-
/**
|
|
93
|
-
* Detect tool name from the shape of tool_use_result.
|
|
94
|
-
*
|
|
95
|
-
* Uses discriminating keys from the SDK's own type definitions
|
|
96
|
-
* (sdk-tools.d.ts: BashOutput, GrepOutput, GlobOutput, FileReadOutput,
|
|
97
|
-
* FileEditOutput, FileWriteOutput, TodoWriteOutput, etc.)
|
|
98
|
-
*/
|
|
99
|
-
function extractToolName(toolResult) {
|
|
100
|
-
if (!toolResult || typeof toolResult !== 'object')
|
|
101
|
-
return 'unknown';
|
|
102
|
-
const r = toolResult;
|
|
103
|
-
// ── Read (FileReadOutput): {type: 'text'|'image'|'notebook'|'pdf'|'parts'|'file_unchanged', file: {...}}
|
|
104
|
-
if (r.file && typeof r.file === 'object'
|
|
105
|
-
&& ['text', 'image', 'notebook', 'pdf', 'parts', 'file_unchanged'].includes(r.type)) {
|
|
106
|
-
return 'Read';
|
|
107
|
-
}
|
|
108
|
-
// ── Edit (FileEditOutput): {filePath, oldString, newString, structuredPatch}
|
|
109
|
-
if (r.filePath && r.oldString !== undefined && r.newString !== undefined
|
|
110
|
-
&& Array.isArray(r.structuredPatch)) {
|
|
111
|
-
return 'Edit';
|
|
112
|
-
}
|
|
113
|
-
// ── Write (FileWriteOutput): {type: 'create'|'update', filePath, content, structuredPatch}
|
|
114
|
-
if (r.filePath && (r.type === 'create' || r.type === 'update')
|
|
115
|
-
&& r.content !== undefined) {
|
|
116
|
-
return 'Write';
|
|
117
|
-
}
|
|
118
|
-
// ── Grep (GrepOutput): {mode, numFiles, filenames, content?, numLines?}
|
|
119
|
-
if (typeof r.numFiles === 'number' && Array.isArray(r.filenames)
|
|
120
|
-
&& r.mode !== undefined) {
|
|
121
|
-
return 'Grep';
|
|
122
|
-
}
|
|
123
|
-
// ── Glob (GlobOutput): {durationMs, numFiles, filenames, truncated}
|
|
124
|
-
if (typeof r.numFiles === 'number' && Array.isArray(r.filenames)
|
|
125
|
-
&& typeof r.durationMs === 'number' && r.truncated !== undefined) {
|
|
126
|
-
return 'Glob';
|
|
127
|
-
}
|
|
128
|
-
// ── TodoWrite (TodoWriteOutput): {oldTodos, newTodos}
|
|
129
|
-
if (Array.isArray(r.oldTodos) && Array.isArray(r.newTodos)) {
|
|
130
|
-
return 'TodoWrite';
|
|
131
|
-
}
|
|
132
|
-
// ── Bash (BashOutput): {stdout, stderr, interrupted, ...}
|
|
133
|
-
if (r.stdout !== undefined || r.stderr !== undefined) {
|
|
134
|
-
return 'Bash';
|
|
135
|
-
}
|
|
136
|
-
// ── WebSearch (WebSearchOutput): {query, results}
|
|
137
|
-
if (typeof r.query === 'string' && Array.isArray(r.results)) {
|
|
138
|
-
return 'WebSearch';
|
|
139
|
-
}
|
|
140
|
-
// ── WebFetch (WebFetchOutput): {url, result, code, bytes}
|
|
141
|
-
if (typeof r.url === 'string' && typeof r.result === 'string'
|
|
142
|
-
&& typeof r.code === 'number') {
|
|
143
|
-
return 'WebFetch';
|
|
144
|
-
}
|
|
145
|
-
// ── send_file (custom MCP tool): content is JSON with _type marker
|
|
146
|
-
if (r.content && typeof r.content === 'string' && r.type === 'text') {
|
|
147
|
-
try {
|
|
148
|
-
const parsed = JSON.parse(r.content);
|
|
149
|
-
if (parsed._type === 'send_file')
|
|
150
|
-
return 'send_file';
|
|
151
|
-
}
|
|
152
|
-
catch { /* not JSON, fall through */ }
|
|
153
|
-
// SDK 0.2.x: content-only results from nested tool calls (no stdout/stderr wrapper)
|
|
154
|
-
// Don't default to Bash — the history lookup is authoritative
|
|
155
|
-
return 'unknown';
|
|
156
|
-
}
|
|
157
|
-
// ── Agent/Task output: {agentId, content, status}
|
|
158
|
-
if (r.agentId && Array.isArray(r.content) && r.status) {
|
|
159
|
-
return 'Task';
|
|
160
|
-
}
|
|
161
|
-
return 'unknown';
|
|
162
|
-
}
|
|
163
|
-
// Look up tool name from the most recent assistant message's tool_use blocks by tool_use_id
|
|
164
|
-
function lookupToolNameFromHistory(messages, toolUseId) {
|
|
165
|
-
if (!toolUseId)
|
|
166
|
-
return null;
|
|
167
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
168
|
-
const msg = messages[i];
|
|
169
|
-
if (msg.role !== 'assistant')
|
|
170
|
-
continue;
|
|
171
|
-
// msg.content is the SDK message object: {role: 'assistant', content: [{type: 'text',...}, {type: 'tool_use',...}]}
|
|
172
|
-
let content = typeof msg.content === 'string' ? null : msg.content;
|
|
173
|
-
// Unwrap nested content: {content: [...]} → [...]
|
|
174
|
-
if (content && !Array.isArray(content) && Array.isArray(content.content)) {
|
|
175
|
-
content = content.content;
|
|
176
|
-
}
|
|
177
|
-
if (!Array.isArray(content))
|
|
178
|
-
continue;
|
|
179
|
-
const toolUse = content.find((c) => c.type === 'tool_use' && c.id === toolUseId);
|
|
180
|
-
if (toolUse?.name)
|
|
181
|
-
return toolUse.name;
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
56
|
// AI config - loaded from server after registration, stored in ~/.talk-to-code/ai-config.json
|
|
186
57
|
// (Do not read ANTHROPIC_* / CLAUDE_MODEL from the host environment — only this file + code default model.)
|
|
187
58
|
const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
|
|
@@ -394,6 +265,28 @@ export class AgentSessionManager {
|
|
|
394
265
|
emergencyStopInProgress = new Set(); // Track sessions being emergency stopped
|
|
395
266
|
sessionHandlers = new Map(); // Track handlers for each session
|
|
396
267
|
socketRef = null; // Socket.IO reference for fetching session history from backend
|
|
268
|
+
/**
|
|
269
|
+
* Select the appropriate agent backend based on model/provider configuration.
|
|
270
|
+
* Can be overridden per-session via handler.agentBackend or env var TTC_AGENT_BACKEND.
|
|
271
|
+
*/
|
|
272
|
+
selectBackend(agentBackend) {
|
|
273
|
+
// Priority: explicit parameter > env var
|
|
274
|
+
const requested = agentBackend || process.env.TTC_AGENT_BACKEND;
|
|
275
|
+
if (requested === 'pi') {
|
|
276
|
+
let piBackend;
|
|
277
|
+
try {
|
|
278
|
+
piBackend = require('./piBackend.js').piBackend;
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
throw new Error(`Pi backend requested but module failed to load: ${err.message}. Install with: cd $(npm root -g)/@exreve/exk && npm install @mariozechner/pi-coding-agent`);
|
|
282
|
+
}
|
|
283
|
+
if (!piBackend.isAvailable()) {
|
|
284
|
+
throw new Error('Pi backend requested but SDK (@mariozechner/pi-coding-agent) is not installed. Install with: cd $(npm root -g)/@exreve/exk && npm install @mariozechner/pi-coding-agent');
|
|
285
|
+
}
|
|
286
|
+
return piBackend;
|
|
287
|
+
}
|
|
288
|
+
return claudeBackend;
|
|
289
|
+
}
|
|
397
290
|
/** Set the socket reference for backend communication (called from app-child.ts) */
|
|
398
291
|
setSocketRef(socket) {
|
|
399
292
|
this.socketRef = socket;
|
|
@@ -500,26 +393,28 @@ export class AgentSessionManager {
|
|
|
500
393
|
// Store the handler for this session
|
|
501
394
|
this.sessionHandlers.set(sessionId, handler);
|
|
502
395
|
const abortController = new AbortController();
|
|
503
|
-
// Restore
|
|
396
|
+
// Restore sdkSessionId from disk (survives CLI restart)
|
|
504
397
|
const persistedState = loadSessionState(sessionId);
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
507
|
-
console.log(`[AgentSessionManager] Restored
|
|
398
|
+
const restoredSdkSessionId = persistedState?.claudeSessionId;
|
|
399
|
+
if (restoredSdkSessionId) {
|
|
400
|
+
console.log(`[AgentSessionManager] Restored sdkSessionId for session ${sessionId}: ${restoredSdkSessionId}`);
|
|
508
401
|
}
|
|
402
|
+
// Select backend for this session
|
|
403
|
+
const backend = this.selectBackend(handler.agentBackend);
|
|
509
404
|
this.sessions.set(sessionId, {
|
|
510
405
|
abortController,
|
|
511
406
|
messages: [],
|
|
512
|
-
toolNameMap: new Map(),
|
|
513
407
|
totalInputTokens: 0,
|
|
514
408
|
totalOutputTokens: 0,
|
|
515
409
|
totalCostUsd: 0,
|
|
516
410
|
promptQueue: [],
|
|
517
411
|
isProcessingQueue: false,
|
|
518
|
-
|
|
412
|
+
sdkSessionId: restoredSdkSessionId, // Restored from disk or undefined
|
|
519
413
|
childProcesses: new Set(),
|
|
520
414
|
claudeProcessGroupId: undefined,
|
|
521
415
|
currentPromptId: undefined,
|
|
522
416
|
model: sessionModel,
|
|
417
|
+
backend,
|
|
523
418
|
});
|
|
524
419
|
// Auto-regenerate CLAUDE.md for fresh project context
|
|
525
420
|
await this.regenerateClaudeMd(projectPath);
|
|
@@ -564,7 +459,7 @@ export class AgentSessionManager {
|
|
|
564
459
|
if (!session.isProcessingQueue) {
|
|
565
460
|
this.processPromptQueue(sessionId);
|
|
566
461
|
}
|
|
567
|
-
else if (session.isProcessingQueue && !session.
|
|
462
|
+
else if (session.isProcessingQueue && !session.activeBackendStream && !this.emergencyStopInProgress.has(sessionId)) {
|
|
568
463
|
// Safety: isProcessingQueue is true but there's no active stream and no emergency stop
|
|
569
464
|
// This means the queue got stuck (e.g. from a previous abort return that bypassed cleanup)
|
|
570
465
|
console.warn(`[agentSession] Queue stuck detected for session ${sessionId}, resetting isProcessingQueue`);
|
|
@@ -627,16 +522,16 @@ export class AgentSessionManager {
|
|
|
627
522
|
// This ensures real-time status updates before any async operations
|
|
628
523
|
onStatusUpdate?.('running');
|
|
629
524
|
// Wait for current query to finish before starting next prompt
|
|
630
|
-
if (session.
|
|
525
|
+
if (session.activeBackendStream !== undefined) {
|
|
631
526
|
try {
|
|
632
|
-
for await (const _ of session.
|
|
527
|
+
for await (const _ of session.activeBackendStream) { }
|
|
633
528
|
}
|
|
634
529
|
catch (err) {
|
|
635
|
-
console.error(`[AgentSession] Error draining active
|
|
530
|
+
console.error(`[AgentSession] Error draining active backend stream:`, err);
|
|
636
531
|
}
|
|
637
|
-
session.
|
|
532
|
+
session.activeBackendStream = undefined;
|
|
638
533
|
}
|
|
639
|
-
session.
|
|
534
|
+
session.activeBackendStream = undefined;
|
|
640
535
|
// Build final prompt with enhancers
|
|
641
536
|
let finalPrompt = effectivePrompt;
|
|
642
537
|
if (enhancers && enhancers.length > 0) {
|
|
@@ -691,155 +586,14 @@ export class AgentSessionManager {
|
|
|
691
586
|
totalTokens: session.totalInputTokens + session.totalOutputTokens
|
|
692
587
|
}
|
|
693
588
|
});
|
|
694
|
-
//
|
|
695
|
-
const pathToClaudeCodeExecutable = CACHED_CLAUDE_PATH;
|
|
696
|
-
// Build query options - include abort signal for cancellation
|
|
697
|
-
const queryOptions = {
|
|
698
|
-
signal: abortController.signal, // Pass abort signal to SDK for interruption
|
|
699
|
-
cwd: projectPath,
|
|
700
|
-
apiKey: CLAUDE_CONFIG.apiKey,
|
|
701
|
-
model: CLAUDE_CONFIG.model,
|
|
702
|
-
tools: { type: 'preset', preset: 'claude_code' },
|
|
703
|
-
disallowedTools: ['AskUserQuestion', 'analyze_image'], // Disable built-in analyze_image (we provide our own via MCP)
|
|
704
|
-
settingSources: ['project'], // Enable CLAUDE.md loading
|
|
705
|
-
permissionMode: 'bypassPermissions',
|
|
706
|
-
allowDangerouslySkipPermissions: true,
|
|
707
|
-
// Create a fresh MCP server for each query call (SDK connects transport internally, cannot reuse)
|
|
708
|
-
...(() => {
|
|
709
|
-
const mcpServer = this.buildMcpServer(sessionId, attachmentDir, promptId);
|
|
710
|
-
return { mcpServers: { 'claude-voice-modules': mcpServer } };
|
|
711
|
-
})(),
|
|
712
|
-
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
713
|
-
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
714
|
-
const { command, args, cwd: cwd2, env, signal } = spawnOptions;
|
|
715
|
-
// Debug: log what env/args are being passed to Claude process
|
|
716
|
-
console.log(`[agentSession] Spawn ANTHROPIC_BASE_URL:`, env?.ANTHROPIC_BASE_URL || '(not set)');
|
|
717
|
-
console.log(`[agentSession] Spawn ANTHROPIC_API_KEY:`, env?.ANTHROPIC_API_KEY ? '(set)' : '(not set)');
|
|
718
|
-
console.log(`[agentSession] Spawn args:`, args?.join(' '));
|
|
719
|
-
// Only check file existence when command is a path (not a bare name like "claude" from PATH)
|
|
720
|
-
const hasPathSep = command.includes(path.sep) || command.includes('/') || command.includes('\\');
|
|
721
|
-
if (hasPathSep && !existsSync(command)) {
|
|
722
|
-
throw new Error(`Executable not found at ${command}. Set path with: ttc config --claude-path "<path>" (or TTC_CLAUDE_PATH)`);
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
if (cwd2 && !existsSync(cwd2)) {
|
|
726
|
-
mkdirSync(cwd2, { recursive: true });
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
catch (err) {
|
|
730
|
-
console.error(`[AgentSession] Failed to create working directory ${cwd2}:`, err);
|
|
731
|
-
}
|
|
732
|
-
const isWin = process.platform === 'win32';
|
|
733
|
-
// Ensure PATH includes common node locations, especially in containers
|
|
734
|
-
const defaultPath = isWin
|
|
735
|
-
? (process.env.Path || process.env.PATH || '')
|
|
736
|
-
: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
|
737
|
-
const spawnEnv = {
|
|
738
|
-
...env,
|
|
739
|
-
IS_SANDBOX: '1', // Tell Claude Code it's in a sandbox — skip root-user checks
|
|
740
|
-
PATH: env.PATH || process.env.PATH || defaultPath,
|
|
741
|
-
...(isWin
|
|
742
|
-
? {
|
|
743
|
-
USERPROFILE: env.USERPROFILE || process.env.USERPROFILE || os.homedir(),
|
|
744
|
-
USERNAME: env.USERNAME || process.env.USERNAME || 'user',
|
|
745
|
-
HOME: env.USERPROFILE || process.env.USERPROFILE || os.homedir(), // Windows: use Windows home, not /home/user
|
|
746
|
-
}
|
|
747
|
-
: { HOME: env.HOME || process.env.HOME || os.homedir(), USER: env.USER || process.env.USER || 'user' }),
|
|
748
|
-
};
|
|
749
|
-
// If command is 'node' and not found, try to resolve it
|
|
750
|
-
if (command === 'node' && !hasPathSep) {
|
|
751
|
-
try {
|
|
752
|
-
const nodePath = execSync('which node', { encoding: 'utf-8', env: spawnEnv }).trim();
|
|
753
|
-
if (nodePath) {
|
|
754
|
-
const child = spawn(nodePath, args, {
|
|
755
|
-
cwd: cwd2 || process.cwd(),
|
|
756
|
-
stdio: ["pipe", "pipe", env.DEBUG_CLAUDE_AGENT_SDK ? "pipe" : "ignore"],
|
|
757
|
-
signal,
|
|
758
|
-
env: spawnEnv,
|
|
759
|
-
windowsHide: true,
|
|
760
|
-
detached: !isWin // Create process group on Unix for tree-killing
|
|
761
|
-
});
|
|
762
|
-
// Track child process for force-kill
|
|
763
|
-
if (!session.childProcesses)
|
|
764
|
-
session.childProcesses = new Set();
|
|
765
|
-
session.childProcesses.add(child);
|
|
766
|
-
// Store process group ID for Unix (negative PID kills entire group)
|
|
767
|
-
if (!isWin && child.pid) {
|
|
768
|
-
session.claudeProcessGroupId = child.pid;
|
|
769
|
-
}
|
|
770
|
-
// Clean up when process exits
|
|
771
|
-
child.on('exit', () => {
|
|
772
|
-
session.childProcesses.delete(child);
|
|
773
|
-
});
|
|
774
|
-
child.on('error', () => {
|
|
775
|
-
session.childProcesses.delete(child);
|
|
776
|
-
});
|
|
777
|
-
return child;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
// Fall through to original spawn
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
const child = spawn(command, args, {
|
|
785
|
-
cwd: cwd2 || process.cwd(),
|
|
786
|
-
stdio: ["pipe", "pipe", env.DEBUG_CLAUDE_AGENT_SDK ? "pipe" : "ignore"],
|
|
787
|
-
signal,
|
|
788
|
-
env: spawnEnv,
|
|
789
|
-
windowsHide: true,
|
|
790
|
-
detached: !isWin // Create process group on Unix for tree-killing
|
|
791
|
-
});
|
|
792
|
-
// Track child process for force-kill
|
|
793
|
-
if (!session.childProcesses)
|
|
794
|
-
session.childProcesses = new Set();
|
|
795
|
-
session.childProcesses.add(child);
|
|
796
|
-
// Store process group ID for Unix (negative PID kills entire group)
|
|
797
|
-
if (!isWin && child.pid) {
|
|
798
|
-
session.claudeProcessGroupId = child.pid;
|
|
799
|
-
}
|
|
800
|
-
// Clean up when process exits
|
|
801
|
-
child.on('exit', () => {
|
|
802
|
-
session.childProcesses.delete(child);
|
|
803
|
-
});
|
|
804
|
-
child.on('error', () => {
|
|
805
|
-
session.childProcesses.delete(child);
|
|
806
|
-
});
|
|
807
|
-
return child;
|
|
808
|
-
},
|
|
809
|
-
env: envForClaudeCodeChild(),
|
|
810
|
-
hooks: {
|
|
811
|
-
// HookCallbackMatcher format: each entry must be { hooks: [callback] }
|
|
812
|
-
// NOT a raw callback array — wrong format silently breaks MCP server registration.
|
|
813
|
-
PostToolUse: [{
|
|
814
|
-
hooks: [(_toolResult) => {
|
|
815
|
-
// Tool result is handled by the user message handler below
|
|
816
|
-
return { continue: true };
|
|
817
|
-
}]
|
|
818
|
-
}],
|
|
819
|
-
Notification: [{
|
|
820
|
-
hooks: [(notification) => {
|
|
821
|
-
onOutput({
|
|
822
|
-
type: 'progress',
|
|
823
|
-
data: notification,
|
|
824
|
-
timestamp: Date.now(),
|
|
825
|
-
metadata: {
|
|
826
|
-
progress: {
|
|
827
|
-
message: typeof notification === 'string' ? notification : JSON.stringify(notification)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
});
|
|
831
|
-
return { continue: true };
|
|
832
|
-
}]
|
|
833
|
-
}],
|
|
834
|
-
}
|
|
835
|
-
};
|
|
836
|
-
// Log model being used for debugging
|
|
589
|
+
// Resolve provider and model configuration
|
|
837
590
|
const sessionModel = session.model || CLAUDE_CONFIG.model;
|
|
838
|
-
// Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
|
|
839
591
|
let effectiveModel = sessionModel;
|
|
840
|
-
let effectiveApiKey =
|
|
841
|
-
let effectiveEnv =
|
|
592
|
+
let effectiveApiKey = CLAUDE_CONFIG.apiKey;
|
|
593
|
+
let effectiveEnv = envForClaudeCodeChild();
|
|
842
594
|
let effectiveSettings;
|
|
595
|
+
let effectiveProvider;
|
|
596
|
+
// Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
|
|
843
597
|
const localOverrides = await getLocalModelEnvOverrides(sessionModel);
|
|
844
598
|
if (localOverrides) {
|
|
845
599
|
effectiveModel = unwrapModelName(sessionModel);
|
|
@@ -849,12 +603,8 @@ export class AgentSessionManager {
|
|
|
849
603
|
ANTHROPIC_API_KEY: localOverrides.apiKey,
|
|
850
604
|
ANTHROPIC_BASE_URL: localOverrides.baseUrl,
|
|
851
605
|
};
|
|
852
|
-
// Override settings to prevent ~/.claude/settings.json env from overriding our proxy URL.
|
|
853
|
-
// Claude Code CLI reads settings.json → env section and applies those on top of spawn env,
|
|
854
|
-
// which would replace our ANTHROPIC_BASE_URL with the z.ai URL.
|
|
855
606
|
effectiveSettings = { env: { ANTHROPIC_API_KEY: localOverrides.apiKey, ANTHROPIC_BASE_URL: localOverrides.baseUrl } };
|
|
856
607
|
console.log(`[agentSession] Using local model adapter: ${sessionModel} -> ${localOverrides.baseUrl}`);
|
|
857
|
-
console.log(`[agentSession] effectiveSettings for local model:`, JSON.stringify(effectiveSettings));
|
|
858
608
|
}
|
|
859
609
|
else {
|
|
860
610
|
// Resolve provider for multi-provider switching (Z.ai / MiniMax)
|
|
@@ -862,6 +612,7 @@ export class AgentSessionManager {
|
|
|
862
612
|
console.log(`[agentSession] Resolved provider: ${resolved.provider} for model: ${sessionModel}`);
|
|
863
613
|
effectiveApiKey = resolved.apiKey;
|
|
864
614
|
effectiveEnv = envForClaudeCodeChild(undefined, resolved);
|
|
615
|
+
effectiveProvider = resolved.provider;
|
|
865
616
|
// Build settings env to prevent ~/.claude/settings.json from overriding our credentials
|
|
866
617
|
const settingsEnv = {
|
|
867
618
|
ANTHROPIC_API_KEY: resolved.apiKey,
|
|
@@ -890,7 +641,7 @@ export class AgentSessionManager {
|
|
|
890
641
|
apiKey: resolved.apiKey,
|
|
891
642
|
model: resolved.model,
|
|
892
643
|
});
|
|
893
|
-
effectiveApiKey = 'cerebras-via-proxy';
|
|
644
|
+
effectiveApiKey = 'cerebras-via-proxy';
|
|
894
645
|
effectiveEnv = envForClaudeCodeChild(undefined, { ...resolved, baseUrl: proxyUrl });
|
|
895
646
|
settingsEnv.ANTHROPIC_API_KEY = effectiveApiKey;
|
|
896
647
|
settingsEnv.ANTHROPIC_BASE_URL = proxyUrl;
|
|
@@ -903,39 +654,44 @@ export class AgentSessionManager {
|
|
|
903
654
|
effectiveSettings = { env: settingsEnv };
|
|
904
655
|
console.log(`[agentSession] Provider: ${resolved.provider}, baseUrl: ${resolved.baseUrl}, model: ${resolved.model}`);
|
|
905
656
|
}
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
model: effectiveModel,
|
|
914
|
-
env: effectiveEnv,
|
|
915
|
-
...(effectiveSettings ? { settings: effectiveSettings } : {}),
|
|
916
|
-
...(session.claudeSessionId && !localOverrides && (() => {
|
|
917
|
-
// Don't resume if provider changed since last session (context format may differ)
|
|
918
|
-
const persisted = loadSessionState(sessionId);
|
|
919
|
-
const currentProvider = resolveProvider(sessionModel).provider;
|
|
920
|
-
return persisted?.provider === currentProvider;
|
|
921
|
-
})() ? { resume: session.claudeSessionId } : {})
|
|
922
|
-
// Note: don't resume session for local models - context format differs
|
|
923
|
-
// Note: also don't resume if provider differs from persisted session provider (context format may differ)
|
|
657
|
+
// Determine if we should resume (only for Claude backend, matching provider)
|
|
658
|
+
let resumeSessionId;
|
|
659
|
+
if (session.sdkSessionId && !localOverrides) {
|
|
660
|
+
const persisted = loadSessionState(sessionId);
|
|
661
|
+
const currentProvider = resolveProvider(sessionModel).provider;
|
|
662
|
+
if (persisted?.provider === currentProvider) {
|
|
663
|
+
resumeSessionId = session.sdkSessionId;
|
|
924
664
|
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
665
|
+
}
|
|
666
|
+
// Build backend config
|
|
667
|
+
const backendConfig = {
|
|
668
|
+
cwd: projectPath,
|
|
669
|
+
apiKey: effectiveApiKey,
|
|
670
|
+
model: effectiveModel,
|
|
671
|
+
baseUrl: effectiveEnv.ANTHROPIC_BASE_URL,
|
|
672
|
+
provider: effectiveProvider,
|
|
673
|
+
resumeSessionId,
|
|
674
|
+
env: effectiveEnv,
|
|
675
|
+
settings: effectiveSettings,
|
|
676
|
+
attachmentDir,
|
|
677
|
+
routingSessionId: sessionId,
|
|
678
|
+
routingPromptId: promptId,
|
|
679
|
+
signal: abortController.signal,
|
|
680
|
+
};
|
|
681
|
+
// Execute prompt via the selected backend
|
|
682
|
+
const backendStream = session.backend.executePrompt(finalPrompt, backendConfig);
|
|
683
|
+
session.activeBackendStream = backendStream;
|
|
684
|
+
// Process backend events and map to SessionOutput
|
|
685
|
+
const abortCheckInterval = 200;
|
|
930
686
|
let lastAbortCheck = Date.now();
|
|
931
687
|
try {
|
|
932
|
-
for await (const
|
|
933
|
-
// Check abort on each
|
|
688
|
+
for await (const event of backendStream) {
|
|
689
|
+
// Check abort on each event
|
|
934
690
|
if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
|
|
935
691
|
console.log(`[agentSession] Aborting prompt ${effectivePromptId} - abort signal received`);
|
|
936
692
|
break;
|
|
937
693
|
}
|
|
938
|
-
// Periodic check
|
|
694
|
+
// Periodic abort check
|
|
939
695
|
const now = Date.now();
|
|
940
696
|
if (now - lastAbortCheck > abortCheckInterval) {
|
|
941
697
|
lastAbortCheck = now;
|
|
@@ -944,376 +700,218 @@ export class AgentSessionManager {
|
|
|
944
700
|
break;
|
|
945
701
|
}
|
|
946
702
|
}
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
if (session.
|
|
703
|
+
// Map BackendEvent → SessionOutput
|
|
704
|
+
switch (event.type) {
|
|
705
|
+
case 'init': {
|
|
706
|
+
// SDK session ID received
|
|
707
|
+
const newSessionId = event.sessionId;
|
|
708
|
+
if (session.sdkSessionId && session.sdkSessionId !== newSessionId) {
|
|
953
709
|
session.contextLost = true;
|
|
954
|
-
console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.
|
|
710
|
+
console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.sdkSessionId} → ${newSessionId}`);
|
|
955
711
|
}
|
|
956
|
-
session.
|
|
957
|
-
saveSessionState(sessionId, {
|
|
712
|
+
session.sdkSessionId = newSessionId;
|
|
713
|
+
saveSessionState(sessionId, {
|
|
714
|
+
claudeSessionId: newSessionId,
|
|
715
|
+
model: session.model,
|
|
716
|
+
provider: resolveProvider(session.model).provider,
|
|
717
|
+
updatedAt: Date.now(),
|
|
718
|
+
});
|
|
719
|
+
break;
|
|
958
720
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
721
|
+
case 'assistant_message': {
|
|
722
|
+
// Update SDK session ID if provided
|
|
723
|
+
if (event.sdkSessionId) {
|
|
724
|
+
if (session.sdkSessionId && session.sdkSessionId !== event.sdkSessionId) {
|
|
725
|
+
session.contextLost = true;
|
|
726
|
+
console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.sdkSessionId} → ${event.sdkSessionId}`);
|
|
727
|
+
}
|
|
728
|
+
session.sdkSessionId = event.sdkSessionId;
|
|
729
|
+
saveSessionState(sessionId, {
|
|
730
|
+
claudeSessionId: event.sdkSessionId,
|
|
731
|
+
model: session.model,
|
|
732
|
+
provider: resolveProvider(session.model).provider,
|
|
733
|
+
updatedAt: Date.now(),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
// Handle context window error
|
|
737
|
+
if (event.isContextWindowError) {
|
|
738
|
+
console.warn(`[AgentSessionManager] Context window limit reached for session ${sessionId}. Clearing context.`);
|
|
739
|
+
session.sdkSessionId = undefined;
|
|
965
740
|
session.contextLost = true;
|
|
966
|
-
|
|
741
|
+
deleteSessionState(sessionId);
|
|
742
|
+
onOutput({
|
|
743
|
+
type: 'system',
|
|
744
|
+
data: {
|
|
745
|
+
message: 'Context window limit reached. Session context has been cleared. The next prompt will start fresh with a summary of previous conversation.',
|
|
746
|
+
subtype: 'context_window_reset',
|
|
747
|
+
},
|
|
748
|
+
timestamp: Date.now(),
|
|
749
|
+
metadata: {
|
|
750
|
+
subtype: 'context_window_reset',
|
|
751
|
+
contextInfo: {
|
|
752
|
+
messageCount: session.messages.length,
|
|
753
|
+
totalInputTokens: session.totalInputTokens,
|
|
754
|
+
totalOutputTokens: session.totalOutputTokens,
|
|
755
|
+
totalTokens: session.totalInputTokens + session.totalOutputTokens,
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
967
759
|
}
|
|
968
|
-
session.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
// when the provider returns 'model_context_window_exceeded'
|
|
974
|
-
const isContextWindowError = msg.error === 'max_output_tokens' && ((typeof msg.message === 'string' && msg.message.includes('context window limit')) ||
|
|
975
|
-
(Array.isArray(msg.message?.content) && msg.message.content.some((c) => typeof c?.text === 'string' && c.text.includes('context window limit'))));
|
|
976
|
-
if (isContextWindowError) {
|
|
977
|
-
console.warn(`[AgentSessionManager] Context window limit reached for session ${sessionId}. Clearing context for fresh start.`);
|
|
978
|
-
// Clear the session ID so next prompt starts fresh (no resume)
|
|
979
|
-
session.claudeSessionId = undefined;
|
|
980
|
-
session.contextLost = true;
|
|
981
|
-
deleteSessionState(sessionId);
|
|
982
|
-
// Emit a user-friendly system message
|
|
760
|
+
session.messages.push({
|
|
761
|
+
role: 'assistant',
|
|
762
|
+
content: event.raw,
|
|
763
|
+
timestamp: Date.now()
|
|
764
|
+
});
|
|
983
765
|
onOutput({
|
|
984
|
-
type: '
|
|
985
|
-
data:
|
|
986
|
-
message: 'Context window limit reached. Session context has been cleared. The next prompt will start fresh with a summary of previous conversation.',
|
|
987
|
-
subtype: 'context_window_reset',
|
|
988
|
-
},
|
|
766
|
+
type: 'assistant',
|
|
767
|
+
data: event.raw,
|
|
989
768
|
timestamp: Date.now(),
|
|
990
769
|
metadata: {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
messageCount: session.messages.length,
|
|
994
|
-
totalInputTokens: session.totalInputTokens,
|
|
995
|
-
totalOutputTokens: session.totalOutputTokens,
|
|
996
|
-
totalTokens: session.totalInputTokens + session.totalOutputTokens,
|
|
997
|
-
}
|
|
770
|
+
error: event.error,
|
|
771
|
+
contextSize: session.messages.length,
|
|
998
772
|
}
|
|
999
773
|
});
|
|
774
|
+
break;
|
|
1000
775
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
776
|
+
case 'tool_result': {
|
|
777
|
+
onOutput({
|
|
778
|
+
type: 'tool_result',
|
|
779
|
+
data: event.result,
|
|
780
|
+
timestamp: Date.now(),
|
|
781
|
+
metadata: {
|
|
782
|
+
toolName: event.toolName ?? undefined,
|
|
783
|
+
toolResult: event.result,
|
|
784
|
+
toolUseId: event.toolUseId || undefined,
|
|
785
|
+
parentToolUseId: event.parentToolUseId ?? undefined,
|
|
786
|
+
isSynthetic: event.isSynthetic,
|
|
1011
787
|
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
onOutput({
|
|
1015
|
-
type: 'assistant',
|
|
1016
|
-
data: msg.message,
|
|
1017
|
-
timestamp: Date.now(),
|
|
1018
|
-
metadata: {
|
|
1019
|
-
parentToolUseId: msg.parent_tool_use_id,
|
|
1020
|
-
uuid: msg.uuid,
|
|
1021
|
-
sessionId: msg.session_id,
|
|
1022
|
-
error: msg.error,
|
|
1023
|
-
contextSize: session.messages.length
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
else if (message.type === 'result') {
|
|
1028
|
-
const msg = message;
|
|
1029
|
-
// Update usage tracking
|
|
1030
|
-
if (msg.usage) {
|
|
1031
|
-
const usage = msg.usage;
|
|
1032
|
-
const inputTokens = usage.input_tokens || usage.inputTokens || 0;
|
|
1033
|
-
const outputTokens = usage.output_tokens || usage.outputTokens || 0;
|
|
1034
|
-
session.totalInputTokens += inputTokens;
|
|
1035
|
-
session.totalOutputTokens += outputTokens;
|
|
1036
|
-
session.totalCostUsd += msg.total_cost_usd || 0;
|
|
1037
|
-
session.lastUsage = {
|
|
1038
|
-
inputTokens,
|
|
1039
|
-
outputTokens,
|
|
1040
|
-
totalTokens: inputTokens + outputTokens
|
|
1041
|
-
};
|
|
788
|
+
});
|
|
789
|
+
break;
|
|
1042
790
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
totalCostUsd: msg.total_cost_usd,
|
|
1054
|
-
usage: msg.usage,
|
|
1055
|
-
modelUsage: msg.modelUsage,
|
|
1056
|
-
structuredOutput: msg.structured_output,
|
|
1057
|
-
numTurns: msg.num_turns,
|
|
1058
|
-
contextInfo: {
|
|
1059
|
-
messageCount: session.messages.length,
|
|
1060
|
-
totalInputTokens: session.totalInputTokens,
|
|
1061
|
-
totalOutputTokens: session.totalOutputTokens,
|
|
1062
|
-
totalTokens: session.totalInputTokens + session.totalOutputTokens,
|
|
1063
|
-
totalCostUsd: session.totalCostUsd,
|
|
1064
|
-
lastUsage: session.lastUsage
|
|
791
|
+
case 'tool_progress': {
|
|
792
|
+
onOutput({
|
|
793
|
+
type: 'tool_progress',
|
|
794
|
+
data: event.raw,
|
|
795
|
+
timestamp: Date.now(),
|
|
796
|
+
metadata: {
|
|
797
|
+
toolName: event.toolName,
|
|
798
|
+
toolUseId: event.toolUseId,
|
|
799
|
+
elapsedTimeSeconds: event.elapsedTimeSeconds,
|
|
800
|
+
parentToolUseId: event.parentToolUseId,
|
|
1065
801
|
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// Emit status update: CLI finished processing (real-time)
|
|
1069
|
-
const exitCode = msg.is_error ? 1 : 0;
|
|
1070
|
-
// Clean up abort controller
|
|
1071
|
-
this.promptAbortControllers.delete(promptId);
|
|
1072
|
-
// Update status immediately based on exit code
|
|
1073
|
-
onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
|
|
1074
|
-
onComplete(exitCode);
|
|
1075
|
-
session.activeQueryStream = undefined;
|
|
1076
|
-
break; // Prompt complete, continue to next in queue
|
|
1077
|
-
}
|
|
1078
|
-
else if (message.type === 'user') {
|
|
1079
|
-
const msg = message;
|
|
1080
|
-
// SDK sends tool results with TWO data sources:
|
|
1081
|
-
// message.content[0] — {type:'tool_result', tool_use_id:'...', content: <raw text>}
|
|
1082
|
-
// msg.tool_use_result — structured object with {stdout,stderr} for Bash,
|
|
1083
|
-
// {type,file} for Read, or [{type:'text',text:'...'}] for MCP tools
|
|
1084
|
-
//
|
|
1085
|
-
// The tool_use_id lives ONLY in message.content[].tool_use_id.
|
|
1086
|
-
// The structured result lives in tool_use_result for built-in tools.
|
|
1087
|
-
// For MCP tools, tool_use_result is a content-block array we need to parse.
|
|
1088
|
-
let toolResult = null;
|
|
1089
|
-
let toolUseId = null;
|
|
1090
|
-
// STEP 1: Extract tool_use_id from message.content (authoritative source)
|
|
1091
|
-
if (Array.isArray(msg.message?.content)) {
|
|
1092
|
-
const contentBlocks = msg.message.content;
|
|
1093
|
-
const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
|
|
1094
|
-
if (toolResultBlock?.tool_use_id) {
|
|
1095
|
-
toolUseId = toolResultBlock.tool_use_id;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
// STEP 1b: Fallback — use parent_tool_use_id if STEP 1 didn't find it
|
|
1099
|
-
// (subagent results where the message.content may not contain tool_result block)
|
|
1100
|
-
if (!toolUseId && msg.parent_tool_use_id) {
|
|
1101
|
-
toolUseId = msg.parent_tool_use_id;
|
|
802
|
+
});
|
|
803
|
+
break;
|
|
1102
804
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const textParts = raw
|
|
1112
|
-
.filter((c) => c.type === 'text')
|
|
1113
|
-
.map((c) => c.text);
|
|
1114
|
-
const rawContent = textParts.join('\n');
|
|
1115
|
-
try {
|
|
1116
|
-
toolResult = JSON.parse(rawContent);
|
|
1117
|
-
}
|
|
1118
|
-
catch {
|
|
1119
|
-
toolResult = { content: rawContent, type: 'text' };
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
else if (typeof raw === 'object' && raw !== null) {
|
|
1123
|
-
// Built-in tool result: {stdout, stderr, ...} or {type, file, ...} — use directly
|
|
1124
|
-
toolResult = raw;
|
|
1125
|
-
}
|
|
1126
|
-
else if (typeof raw === 'string') {
|
|
1127
|
-
try {
|
|
1128
|
-
toolResult = JSON.parse(raw);
|
|
1129
|
-
}
|
|
1130
|
-
catch {
|
|
1131
|
-
toolResult = { content: raw, type: 'text' };
|
|
805
|
+
case 'system': {
|
|
806
|
+
onOutput({
|
|
807
|
+
type: 'system',
|
|
808
|
+
data: event.raw,
|
|
809
|
+
timestamp: Date.now(),
|
|
810
|
+
metadata: {
|
|
811
|
+
subtype: event.subtype,
|
|
812
|
+
messageType: event.subtype || 'system',
|
|
1132
813
|
}
|
|
1133
|
-
}
|
|
814
|
+
});
|
|
815
|
+
break;
|
|
1134
816
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
817
|
+
case 'result': {
|
|
818
|
+
// Update usage tracking
|
|
819
|
+
if (event.usage) {
|
|
820
|
+
session.totalInputTokens += event.usage.inputTokens;
|
|
821
|
+
session.totalOutputTokens += event.usage.outputTokens;
|
|
822
|
+
session.totalCostUsd += event.usage.totalCostUsd || 0;
|
|
823
|
+
session.lastUsage = {
|
|
824
|
+
inputTokens: event.usage.inputTokens,
|
|
825
|
+
outputTokens: event.usage.outputTokens,
|
|
826
|
+
totalTokens: event.usage.inputTokens + event.usage.outputTokens,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
const exitCode = event.isError ? 1 : 0;
|
|
830
|
+
onOutput({
|
|
831
|
+
type: 'result',
|
|
832
|
+
data: event.raw,
|
|
833
|
+
timestamp: Date.now(),
|
|
834
|
+
metadata: {
|
|
835
|
+
isError: event.isError,
|
|
836
|
+
exitCode,
|
|
837
|
+
durationMs: event.usage?.durationMs,
|
|
838
|
+
totalCostUsd: event.usage?.totalCostUsd,
|
|
839
|
+
numTurns: event.usage?.numTurns,
|
|
840
|
+
contextInfo: {
|
|
841
|
+
messageCount: session.messages.length,
|
|
842
|
+
totalInputTokens: session.totalInputTokens,
|
|
843
|
+
totalOutputTokens: session.totalOutputTokens,
|
|
844
|
+
totalTokens: session.totalInputTokens + session.totalOutputTokens,
|
|
845
|
+
totalCostUsd: session.totalCostUsd,
|
|
846
|
+
lastUsage: session.lastUsage,
|
|
1159
847
|
}
|
|
1160
848
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
849
|
+
});
|
|
850
|
+
this.promptAbortControllers.delete(promptId);
|
|
851
|
+
onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
|
|
852
|
+
onComplete(exitCode);
|
|
853
|
+
session.activeBackendStream = undefined;
|
|
854
|
+
break; // Prompt complete, continue to next in queue
|
|
1165
855
|
}
|
|
1166
|
-
|
|
1167
|
-
// Map lookup is O(1), falls back to O(n) history scan, then heuristic
|
|
1168
|
-
const mappedName = toolUseId ? session.toolNameMap.get(toolUseId) ?? null : null;
|
|
1169
|
-
const historyName = mappedName || lookupToolNameFromHistory(session.messages, toolUseId);
|
|
1170
|
-
const detectedName = historyName || extractToolName(toolResult);
|
|
1171
|
-
const resolvedName = detectedName;
|
|
856
|
+
case 'progress': {
|
|
1172
857
|
onOutput({
|
|
1173
|
-
type: '
|
|
1174
|
-
data:
|
|
858
|
+
type: 'progress',
|
|
859
|
+
data: event.raw || { message: event.message },
|
|
1175
860
|
timestamp: Date.now(),
|
|
1176
861
|
metadata: {
|
|
1177
|
-
|
|
1178
|
-
toolResult: toolResult,
|
|
1179
|
-
toolUseId: toolUseId || undefined,
|
|
1180
|
-
parentToolUseId: msg.parent_tool_use_id,
|
|
1181
|
-
isSynthetic: msg.isSynthetic
|
|
862
|
+
progress: { message: event.message },
|
|
1182
863
|
}
|
|
1183
864
|
});
|
|
865
|
+
break;
|
|
1184
866
|
}
|
|
1185
|
-
|
|
867
|
+
case 'stream_event': {
|
|
1186
868
|
onOutput({
|
|
1187
|
-
type: '
|
|
1188
|
-
data:
|
|
869
|
+
type: 'stream_event',
|
|
870
|
+
data: event.raw,
|
|
1189
871
|
timestamp: Date.now(),
|
|
1190
872
|
metadata: {
|
|
1191
|
-
parentToolUseId:
|
|
1192
|
-
isSynthetic: msg.isSynthetic
|
|
873
|
+
parentToolUseId: event.parentToolUseId,
|
|
1193
874
|
}
|
|
1194
875
|
});
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
case 'rate_limit': {
|
|
879
|
+
onOutput({
|
|
880
|
+
type: 'rate_limit_event',
|
|
881
|
+
data: event.raw,
|
|
882
|
+
timestamp: Date.now(),
|
|
883
|
+
});
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
case 'prompt_suggestion': {
|
|
887
|
+
onOutput({
|
|
888
|
+
type: 'prompt_suggestion',
|
|
889
|
+
data: event.suggestion,
|
|
890
|
+
timestamp: Date.now(),
|
|
891
|
+
});
|
|
892
|
+
break;
|
|
1195
893
|
}
|
|
1196
|
-
}
|
|
1197
|
-
else if (message.type === 'system') {
|
|
1198
|
-
const sysMsg = message;
|
|
1199
|
-
onOutput({
|
|
1200
|
-
type: 'system',
|
|
1201
|
-
data: sysMsg,
|
|
1202
|
-
timestamp: Date.now(),
|
|
1203
|
-
metadata: {
|
|
1204
|
-
subtype: sysMsg.subtype,
|
|
1205
|
-
messageType: sysMsg.subtype || 'system'
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
else if (message.type === 'tool_progress') {
|
|
1210
|
-
const msg = message;
|
|
1211
|
-
onOutput({
|
|
1212
|
-
type: 'tool_progress',
|
|
1213
|
-
data: msg,
|
|
1214
|
-
timestamp: Date.now(),
|
|
1215
|
-
metadata: {
|
|
1216
|
-
toolName: msg.tool_name,
|
|
1217
|
-
toolUseId: msg.tool_use_id,
|
|
1218
|
-
elapsedTimeSeconds: msg.elapsed_time_seconds,
|
|
1219
|
-
parentToolUseId: msg.parent_tool_use_id
|
|
1220
|
-
}
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
else if (message.type === 'auth_status') {
|
|
1224
|
-
onOutput({
|
|
1225
|
-
type: 'auth_status',
|
|
1226
|
-
data: message,
|
|
1227
|
-
timestamp: Date.now(),
|
|
1228
|
-
metadata: {
|
|
1229
|
-
isAuthenticating: message.isAuthenticating,
|
|
1230
|
-
error: message.error
|
|
1231
|
-
}
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
else if (message.type === 'stream_event') {
|
|
1235
|
-
onOutput({
|
|
1236
|
-
type: 'stream_event',
|
|
1237
|
-
data: message.event || message,
|
|
1238
|
-
timestamp: Date.now(),
|
|
1239
|
-
metadata: {
|
|
1240
|
-
parentToolUseId: message.parent_tool_use_id
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
else if (message.type === 'tool_use_summary') {
|
|
1245
|
-
const msg = message;
|
|
1246
|
-
onOutput({
|
|
1247
|
-
type: 'tool_use_summary',
|
|
1248
|
-
data: msg.summary || '',
|
|
1249
|
-
timestamp: Date.now(),
|
|
1250
|
-
metadata: {
|
|
1251
|
-
precedingToolUseIds: msg.preceding_tool_use_ids,
|
|
1252
|
-
uuid: msg.uuid,
|
|
1253
|
-
sessionId: msg.session_id
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
else if (message.type === 'rate_limit_event') {
|
|
1258
|
-
const msg = message;
|
|
1259
|
-
onOutput({
|
|
1260
|
-
type: 'rate_limit_event',
|
|
1261
|
-
data: msg,
|
|
1262
|
-
timestamp: Date.now(),
|
|
1263
|
-
metadata: {
|
|
1264
|
-
rateLimitInfo: msg.rate_limit_info,
|
|
1265
|
-
uuid: msg.uuid,
|
|
1266
|
-
sessionId: msg.session_id
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
else if (message.type === 'prompt_suggestion') {
|
|
1271
|
-
const msg = message;
|
|
1272
|
-
onOutput({
|
|
1273
|
-
type: 'prompt_suggestion',
|
|
1274
|
-
data: msg.suggestion || '',
|
|
1275
|
-
timestamp: Date.now(),
|
|
1276
|
-
metadata: {
|
|
1277
|
-
uuid: msg.uuid,
|
|
1278
|
-
sessionId: msg.session_id
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
else if (message.type === 'keep_alive') {
|
|
1283
|
-
// Internal keepalive - silently ignore
|
|
1284
|
-
}
|
|
1285
|
-
else {
|
|
1286
|
-
onOutput({
|
|
1287
|
-
type: 'stdout',
|
|
1288
|
-
data: JSON.stringify(message, null, 2),
|
|
1289
|
-
timestamp: Date.now()
|
|
1290
|
-
});
|
|
1291
894
|
}
|
|
1292
895
|
}
|
|
1293
896
|
}
|
|
1294
897
|
catch (streamError) {
|
|
1295
|
-
// Check if this was an abort-related error
|
|
1296
898
|
if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
|
|
1297
899
|
console.log(`[agentSession] Stream aborted for prompt ${effectivePromptId}`);
|
|
1298
|
-
// Handle abort gracefully
|
|
1299
900
|
onStatusUpdate?.('cancelled');
|
|
1300
901
|
onComplete(null);
|
|
1301
|
-
session.
|
|
902
|
+
session.activeBackendStream = undefined;
|
|
1302
903
|
session.currentPromptId = undefined;
|
|
1303
|
-
// Use break instead of return to ensure isProcessingQueue gets reset
|
|
1304
|
-
// after the while loop at the end of processPromptQueue
|
|
1305
904
|
break;
|
|
1306
905
|
}
|
|
1307
|
-
// Re-throw non-abort errors
|
|
1308
906
|
throw streamError;
|
|
1309
907
|
}
|
|
1310
|
-
session.
|
|
1311
|
-
session.currentPromptId = undefined;
|
|
908
|
+
session.activeBackendStream = undefined;
|
|
909
|
+
session.currentPromptId = undefined;
|
|
1312
910
|
}
|
|
1313
911
|
catch (error) {
|
|
1314
912
|
const currentSession = this.sessions.get(sessionId);
|
|
1315
913
|
if (currentSession) {
|
|
1316
|
-
currentSession.
|
|
914
|
+
currentSession.activeBackendStream = undefined;
|
|
1317
915
|
}
|
|
1318
916
|
// Clean up abort controller (promptId is guaranteed to exist here due to check at line 204)
|
|
1319
917
|
if (promptId) {
|
|
@@ -1335,7 +933,7 @@ export class AgentSessionManager {
|
|
|
1335
933
|
const session = this.sessions.get(sessionId);
|
|
1336
934
|
if (session) {
|
|
1337
935
|
session.abortController.abort();
|
|
1338
|
-
session.
|
|
936
|
+
session.activeBackendStream = undefined;
|
|
1339
937
|
this.sessions.delete(sessionId);
|
|
1340
938
|
}
|
|
1341
939
|
// Clean up persisted state
|
|
@@ -1386,59 +984,42 @@ export class AgentSessionManager {
|
|
|
1386
984
|
const session = this.sessions.get(sessionId);
|
|
1387
985
|
if (!session)
|
|
1388
986
|
return;
|
|
987
|
+
// Delegate to backend if it supports process killing (ClaudeBackend)
|
|
988
|
+
if (session.backend instanceof claudeBackend.constructor) {
|
|
989
|
+
;
|
|
990
|
+
session.backend.killProcesses();
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
// Fallback: generic process cleanup
|
|
1389
994
|
const isWin = process.platform === 'win32';
|
|
1390
|
-
// 1. Kill all tracked child processes
|
|
1391
995
|
if (session.childProcesses && session.childProcesses.size > 0) {
|
|
1392
996
|
console.log(`[agentSession] Killing ${session.childProcesses.size} tracked child processes`);
|
|
1393
997
|
for (const child of session.childProcesses) {
|
|
1394
998
|
if (!child.killed) {
|
|
1395
999
|
try {
|
|
1396
|
-
if (isWin)
|
|
1397
|
-
// Windows: use taskkill to force kill
|
|
1398
|
-
if (child.pid) {
|
|
1399
|
-
spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t'], {
|
|
1400
|
-
stdio: 'ignore',
|
|
1401
|
-
windowsHide: true
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
else {
|
|
1406
|
-
// Unix: try graceful SIGTERM first, then SIGKILL
|
|
1000
|
+
if (!isWin)
|
|
1407
1001
|
child.kill('SIGTERM');
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
catch (e) {
|
|
1411
|
-
// Process may already be dead
|
|
1412
1002
|
}
|
|
1003
|
+
catch { /* already dead */ }
|
|
1413
1004
|
}
|
|
1414
1005
|
}
|
|
1415
|
-
// Wait a bit for graceful shutdown, then force kill
|
|
1416
1006
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1417
1007
|
for (const child of session.childProcesses) {
|
|
1418
1008
|
if (!child.killed) {
|
|
1419
1009
|
try {
|
|
1420
|
-
if (!isWin && child.pid)
|
|
1421
|
-
// Unix: force kill with SIGKILL
|
|
1010
|
+
if (!isWin && child.pid)
|
|
1422
1011
|
child.kill('SIGKILL');
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
catch (e) {
|
|
1426
|
-
// Already dead
|
|
1427
1012
|
}
|
|
1013
|
+
catch { /* already dead */ }
|
|
1428
1014
|
}
|
|
1429
1015
|
}
|
|
1430
1016
|
session.childProcesses.clear();
|
|
1431
1017
|
}
|
|
1432
|
-
// 2. Kill the entire process group on Unix-like systems
|
|
1433
1018
|
if (!isWin && session.claudeProcessGroupId) {
|
|
1434
1019
|
try {
|
|
1435
|
-
console.log(`[agentSession] Killing process group ${session.claudeProcessGroupId}`);
|
|
1436
|
-
// Kill entire process group using negative PID
|
|
1437
1020
|
process.kill(-session.claudeProcessGroupId, 'SIGKILL');
|
|
1438
1021
|
}
|
|
1439
|
-
catch
|
|
1440
|
-
// Process group may already be dead
|
|
1441
|
-
}
|
|
1022
|
+
catch { /* dead */ }
|
|
1442
1023
|
session.claudeProcessGroupId = undefined;
|
|
1443
1024
|
}
|
|
1444
1025
|
}
|
|
@@ -1487,7 +1068,7 @@ export class AgentSessionManager {
|
|
|
1487
1068
|
// 4. Clear the prompt queue
|
|
1488
1069
|
session.promptQueue = [];
|
|
1489
1070
|
// 5. Clear active stream
|
|
1490
|
-
session.
|
|
1071
|
+
session.activeBackendStream = undefined;
|
|
1491
1072
|
// 6. Reset processing state
|
|
1492
1073
|
session.isProcessingQueue = false;
|
|
1493
1074
|
// 7. Clean up abort controllers map (only for this session's prompts, not ALL sessions)
|
|
@@ -1512,18 +1093,5 @@ export class AgentSessionManager {
|
|
|
1512
1093
|
return { success: false, message: error.message || 'Emergency stop failed' };
|
|
1513
1094
|
}
|
|
1514
1095
|
}
|
|
1515
|
-
/**
|
|
1516
|
-
* Build a fresh MCP server for a query call.
|
|
1517
|
-
* The SDK's query() connects the MCP server's internal transport, so we cannot
|
|
1518
|
-
* reuse a single instance across multiple queries. This must be called fresh each time.
|
|
1519
|
-
*/
|
|
1520
|
-
buildMcpServer(sessionId, attachmentDir, promptId) {
|
|
1521
|
-
console.log(`[buildMcpServer] Session ${sessionId}: attachmentDir=${attachmentDir || 'none'}, promptId=${promptId || 'none'}`);
|
|
1522
|
-
return createModuleMcpServer({
|
|
1523
|
-
attachmentDir,
|
|
1524
|
-
sessionId,
|
|
1525
|
-
promptId,
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
1096
|
}
|
|
1529
1097
|
export const agentSessionManager = new AgentSessionManager();
|