@bahulam/code 0.1.6 → 0.1.8
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/LICENSE +1 -1
- package/NOTICE +6 -6
- package/README.md +11 -18
- package/package.json +11 -11
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/config/settings.mjs +2 -2
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/attachments.mjs +66 -4
- package/src/core/backend-url.mjs +11 -12
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +18 -4
- package/src/core/jsonl-writer.mjs +18 -18
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +40 -8
- package/src/core/local-store.mjs +5 -1
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +22 -26
- package/src/core/pricing.mjs +10 -3
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +13 -8
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +178 -84
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +2 -2
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +43 -17
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +37 -7
- package/src/terminal/repl-resume.mjs +2 -2
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +248 -70
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/project-overview.mjs +12 -12
- package/src/tools/registry.mjs +4 -0
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/icons.mjs +2 -0
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +81 -6
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- package/pulse/types/claude.ts +0 -294
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-local agent relay.
|
|
3
|
+
*
|
|
4
|
+
* Bridges a local workspace browser session to the same CLI-owned remote
|
|
5
|
+
* agent path used by the terminal: BahulamStreamClient + local ToolExecutor.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
11
|
+
import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
|
|
12
|
+
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
13
|
+
import {
|
|
14
|
+
buildResumeHistory,
|
|
15
|
+
getRecentSessions,
|
|
16
|
+
getSessionDetail,
|
|
17
|
+
} from '../core/local-store.mjs';
|
|
18
|
+
import { BahulamStreamClient } from '../core/stream-client.mjs';
|
|
19
|
+
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
20
|
+
import { buildWorkScope } from '../core/work-scope.mjs';
|
|
21
|
+
import { BrowserApprovalManager } from './approval-bridge.mjs';
|
|
22
|
+
|
|
23
|
+
const __require = createRequire(import.meta.url);
|
|
24
|
+
const VERSION = __require('../../package.json').version;
|
|
25
|
+
|
|
26
|
+
export class LocalAgentRelay {
|
|
27
|
+
constructor({ session, emit } = {}) {
|
|
28
|
+
if (!session?.id) throw new Error('LocalAgentRelay requires a session');
|
|
29
|
+
if (typeof emit !== 'function') throw new Error('LocalAgentRelay requires an emit function');
|
|
30
|
+
this.session = session;
|
|
31
|
+
this.emit = emit;
|
|
32
|
+
this.ready = null;
|
|
33
|
+
this.running = false;
|
|
34
|
+
this.turnCount = 0;
|
|
35
|
+
this.client = null;
|
|
36
|
+
this.toolExecutor = null;
|
|
37
|
+
this.creds = null;
|
|
38
|
+
this.approvalManager = null;
|
|
39
|
+
this.resumeLoaded = false;
|
|
40
|
+
this.resumeSessionId = null;
|
|
41
|
+
this.historyMode = 'full';
|
|
42
|
+
this.displayHistory = [];
|
|
43
|
+
this.agentHistory = [];
|
|
44
|
+
this.jsonlWriter = null;
|
|
45
|
+
this.approvalAutoMode = false;
|
|
46
|
+
this.pendingFollowups = [];
|
|
47
|
+
this.flushingFollowups = false;
|
|
48
|
+
this.cancellationRequested = false;
|
|
49
|
+
this.cancellationEventEmitted = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async listHistorySessions() {
|
|
53
|
+
const sessions = await transcriptsForRoot(this.session.root_path);
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
root_path: this.session.root_path,
|
|
57
|
+
selected_session_id: this.resumeSessionId || null,
|
|
58
|
+
sessions: sessions.map(serializeTranscriptSession),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
currentHistory() {
|
|
63
|
+
return this._historySnapshot();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async startNewHistory() {
|
|
67
|
+
if (this.running) {
|
|
68
|
+
const err = new Error('A local agent turn is already running for this workspace');
|
|
69
|
+
err.code = 'CONFLICT';
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
if (this.turnCount > 0) {
|
|
73
|
+
const err = new Error('Start a new local workspace session to switch history after a turn has run');
|
|
74
|
+
err.code = 'CONFLICT';
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
this.resumeLoaded = true;
|
|
78
|
+
this.resumeSessionId = null;
|
|
79
|
+
this.displayHistory = [];
|
|
80
|
+
this.agentHistory = [];
|
|
81
|
+
this.jsonlWriter = null;
|
|
82
|
+
if (this.client) this.client.sessionId = null;
|
|
83
|
+
this.emit('agent_history_new', {
|
|
84
|
+
root_path: this.session.root_path,
|
|
85
|
+
});
|
|
86
|
+
return this._historySnapshot();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async loadHistory(options = {}) {
|
|
90
|
+
const sessionId = typeof options === 'string' ? options : options.sessionId;
|
|
91
|
+
if (!sessionId) return this._historySnapshot();
|
|
92
|
+
return this.resumeHistory(sessionId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async resumeHistory(sessionId) {
|
|
96
|
+
const requestedSessionId = String(sessionId || '').trim();
|
|
97
|
+
if (!requestedSessionId) {
|
|
98
|
+
const err = new Error('session_id is required');
|
|
99
|
+
err.code = 'BAD_REQUEST';
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
if (this.running) {
|
|
103
|
+
const err = new Error('A local agent turn is already running for this workspace');
|
|
104
|
+
err.code = 'CONFLICT';
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
if (this.turnCount > 0 && this.resumeSessionId !== requestedSessionId) {
|
|
108
|
+
const err = new Error('Start a new local workspace session to switch history after a turn has run');
|
|
109
|
+
err.code = 'CONFLICT';
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
if (this.resumeLoaded && this.resumeSessionId === requestedSessionId) return this._historySnapshot();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const match = await transcriptForRootBySessionId(this.session.root_path, requestedSessionId);
|
|
116
|
+
if (!match) {
|
|
117
|
+
const err = new Error(`No local transcript for this workspace session: ${requestedSessionId}`);
|
|
118
|
+
err.code = 'NOT_FOUND';
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const detail = await getSessionDetail(match.sessionId, { filePath: match.filePath });
|
|
123
|
+
if (!detail) {
|
|
124
|
+
const err = new Error(`Local transcript could not be read: ${requestedSessionId}`);
|
|
125
|
+
err.code = 'NOT_FOUND';
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const history = buildResumeHistory({ ...detail, recapTailTurns: 8 }, this.historyMode);
|
|
130
|
+
this.resumeSessionId = match.sessionId;
|
|
131
|
+
this.displayHistory = history.displayHistory || [];
|
|
132
|
+
this.agentHistory = history.agentHistory || [];
|
|
133
|
+
this.resumeLoaded = true;
|
|
134
|
+
if (this.client) this.client.sessionId = this.resumeSessionId;
|
|
135
|
+
if (this.jsonlWriter?.sessionId && this.jsonlWriter.sessionId !== this.resumeSessionId) {
|
|
136
|
+
this.jsonlWriter = null;
|
|
137
|
+
}
|
|
138
|
+
this._ensureJsonlWriter();
|
|
139
|
+
this.emit('agent_history_loaded', {
|
|
140
|
+
backend_session_id: this.resumeSessionId,
|
|
141
|
+
messages: this.displayHistory.length,
|
|
142
|
+
agent_messages: this.agentHistory.length,
|
|
143
|
+
root_path: this.session.root_path,
|
|
144
|
+
transcript_path: match.filePath,
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.emit('agent_history_error', {
|
|
148
|
+
message: err.message || String(err),
|
|
149
|
+
root_path: this.session.root_path,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this._historySnapshot();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async runTurn({ prompt, path = '.', attachments = [] } = {}) {
|
|
157
|
+
const instruction = String(prompt || '').trim();
|
|
158
|
+
const turnAttachments = normalizeTurnAttachments(attachments);
|
|
159
|
+
if (!instruction && !turnAttachments.length) {
|
|
160
|
+
const err = new Error('prompt is required');
|
|
161
|
+
err.code = 'BAD_REQUEST';
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
if (this.running) {
|
|
165
|
+
const err = new Error('A local agent turn is already running for this workspace');
|
|
166
|
+
err.code = 'CONFLICT';
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this.resumeLoaded) await this.startNewHistory();
|
|
171
|
+
await this._ensureReady();
|
|
172
|
+
|
|
173
|
+
this.running = true;
|
|
174
|
+
this.cancellationRequested = false;
|
|
175
|
+
this.cancellationEventEmitted = false;
|
|
176
|
+
this.turnCount += 1;
|
|
177
|
+
const turnId = `turn_${Date.now().toString(36)}_${this.turnCount}`;
|
|
178
|
+
const userInstruction = instruction || 'Review the attached files.';
|
|
179
|
+
const relayInstruction = this._buildInstruction(userInstruction, path, turnAttachments);
|
|
180
|
+
const writer = this._ensureJsonlWriter();
|
|
181
|
+
const turnHistory = new AgentHistoryTurnBuilder();
|
|
182
|
+
let content = '';
|
|
183
|
+
let assistantContent = '';
|
|
184
|
+
let eventCount = 0;
|
|
185
|
+
let userTurnWritten = false;
|
|
186
|
+
const writeUserTurn = () => {
|
|
187
|
+
if (userTurnWritten) return;
|
|
188
|
+
writer.writeUserTurn(userInstruction);
|
|
189
|
+
writer.writeHistory(userInstruction);
|
|
190
|
+
userTurnWritten = true;
|
|
191
|
+
};
|
|
192
|
+
if (this.client.sessionId) writeUserTurn();
|
|
193
|
+
|
|
194
|
+
this.emit('agent_turn_started', {
|
|
195
|
+
turn_id: turnId,
|
|
196
|
+
prompt: userInstruction.slice(0, 500),
|
|
197
|
+
cwd: this.session.root_path,
|
|
198
|
+
path,
|
|
199
|
+
attachments: turnAttachments.length,
|
|
200
|
+
backend_url: this.creds.backendUrl,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const execContext = await this._buildExecContext(relayInstruction);
|
|
205
|
+
for await (const event of this.client.execute(
|
|
206
|
+
relayInstruction,
|
|
207
|
+
execContext,
|
|
208
|
+
this.agentHistory.length ? this.agentHistory : null,
|
|
209
|
+
)) {
|
|
210
|
+
eventCount += 1;
|
|
211
|
+
writer.writeBahulamEvent(event);
|
|
212
|
+
|
|
213
|
+
const contentUpdate = contentDeltaForEvent(event, assistantContent);
|
|
214
|
+
if (contentUpdate) {
|
|
215
|
+
assistantContent = contentUpdate.next;
|
|
216
|
+
if (contentUpdate.delta) {
|
|
217
|
+
content += contentUpdate.delta;
|
|
218
|
+
turnHistory.addAssistantText(contentUpdate.delta);
|
|
219
|
+
writer.accumulateContent(contentUpdate.delta);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (event.type === 'session_info' && event.data?.session_id) {
|
|
224
|
+
this.resumeSessionId = this.resumeSessionId || event.data.session_id;
|
|
225
|
+
if (!writer.sessionId) writer.setSessionId(this.resumeSessionId);
|
|
226
|
+
writeUserTurn();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (event.type === 'tool_call' || event.type === 'tool_request') {
|
|
230
|
+
const data = event.data || {};
|
|
231
|
+
turnHistory.addToolUse(data);
|
|
232
|
+
writer.accumulateToolCall(data.call_id || data.request_id, data.tool || data.name, data.args || data.input);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (event.type === 'tool_done' || event.type === 'tool_result') {
|
|
236
|
+
const data = event.data || {};
|
|
237
|
+
turnHistory.addToolResult(data);
|
|
238
|
+
writer.recordToolResult(
|
|
239
|
+
data.call_id || data._callId || data.request_id || data.id || data.tool_use_id,
|
|
240
|
+
data.output ?? data.result ?? data.message ?? '',
|
|
241
|
+
data.success === false || data.is_error,
|
|
242
|
+
data,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (event.type === 'complete') {
|
|
247
|
+
writer.setTurnUsage(event.data?.usage, event.data?.model || this.creds?.modelConfig?.reasoning);
|
|
248
|
+
writer.flushAssistantTurn();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this._emitAgentEvent(event, { turnId, contentText: contentUpdate?.delta });
|
|
252
|
+
await this._flushQueuedFollowups();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const wasCancelled = this.cancellationRequested || Boolean(this.client?._cancelled);
|
|
256
|
+
this._markUndeliveredFollowupsQueued(wasCancelled ? 'Task cancelled' : '');
|
|
257
|
+
if (!userTurnWritten && (this.client.sessionId || wasCancelled)) writeUserTurn();
|
|
258
|
+
if (wasCancelled) {
|
|
259
|
+
writer.writeBahulamEvent({
|
|
260
|
+
type: 'cancelled',
|
|
261
|
+
data: {
|
|
262
|
+
task_id: this.client?.currentTaskId || null,
|
|
263
|
+
reason: 'Cancelled by user',
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
writer.flushAssistantTurn();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.displayHistory.push({
|
|
270
|
+
role: 'user',
|
|
271
|
+
content: userInstruction,
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
order: this.displayHistory.length,
|
|
274
|
+
});
|
|
275
|
+
if (assistantContent) {
|
|
276
|
+
this.displayHistory.push({
|
|
277
|
+
role: 'assistant',
|
|
278
|
+
content: assistantContent,
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
order: this.displayHistory.length,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const structuredTurn = turnHistory.finish();
|
|
284
|
+
if (structuredTurn.length) {
|
|
285
|
+
this.agentHistory.push({ role: 'user', content: userInstruction }, ...structuredTurn);
|
|
286
|
+
} else if (assistantContent) {
|
|
287
|
+
this.agentHistory.push({ role: 'user', content: userInstruction }, { role: 'assistant', content: assistantContent });
|
|
288
|
+
} else {
|
|
289
|
+
this.agentHistory.push({ role: 'user', content: userInstruction });
|
|
290
|
+
}
|
|
291
|
+
await writer.flush();
|
|
292
|
+
|
|
293
|
+
if (wasCancelled) {
|
|
294
|
+
if (!this.cancellationEventEmitted) {
|
|
295
|
+
this.cancellationEventEmitted = true;
|
|
296
|
+
this.emit('agent_turn_cancelled', {
|
|
297
|
+
turn_id: turnId,
|
|
298
|
+
event_count: eventCount,
|
|
299
|
+
content_chars: content.length,
|
|
300
|
+
backend_session_id: this.client.sessionId || null,
|
|
301
|
+
transcript_session_id: writer.sessionId || null,
|
|
302
|
+
reason: 'Cancelled by user',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
status: 'cancelled',
|
|
308
|
+
turn_id: turnId,
|
|
309
|
+
event_count: eventCount,
|
|
310
|
+
content,
|
|
311
|
+
backend_session_id: this.client.sessionId || null,
|
|
312
|
+
transcript_session_id: writer.sessionId || null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.emit('agent_turn_complete', {
|
|
317
|
+
turn_id: turnId,
|
|
318
|
+
event_count: eventCount,
|
|
319
|
+
content_chars: content.length,
|
|
320
|
+
backend_session_id: this.client.sessionId || null,
|
|
321
|
+
transcript_session_id: writer.sessionId || null,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
ok: true,
|
|
326
|
+
turn_id: turnId,
|
|
327
|
+
event_count: eventCount,
|
|
328
|
+
content,
|
|
329
|
+
backend_session_id: this.client.sessionId || null,
|
|
330
|
+
transcript_session_id: writer.sessionId || null,
|
|
331
|
+
};
|
|
332
|
+
} catch (err) {
|
|
333
|
+
this._markUndeliveredFollowupsQueued(err.message || String(err));
|
|
334
|
+
this.emit('agent_error', {
|
|
335
|
+
turn_id: turnId,
|
|
336
|
+
code: err.code || 'agent_relay_error',
|
|
337
|
+
message: err.message || String(err),
|
|
338
|
+
});
|
|
339
|
+
throw err;
|
|
340
|
+
} finally {
|
|
341
|
+
this.running = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async close() {
|
|
346
|
+
this.approvalManager?.rejectAll?.();
|
|
347
|
+
try {
|
|
348
|
+
await this.jsonlWriter?.close?.();
|
|
349
|
+
} catch {}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
decideApproval(approvalId, decision = {}) {
|
|
353
|
+
if (!this.approvalManager) {
|
|
354
|
+
const err = new Error('Approval bridge is not ready');
|
|
355
|
+
err.code = 'NOT_FOUND';
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
return this.approvalManager.decide(approvalId, decision);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async cancelTurn(reason = 'Cancelled by user') {
|
|
362
|
+
const taskId = this.client?.currentTaskId || null;
|
|
363
|
+
if (!this.running || !this.client) {
|
|
364
|
+
return { ok: true, status: 'idle', task_id: taskId };
|
|
365
|
+
}
|
|
366
|
+
this.cancellationRequested = true;
|
|
367
|
+
this._markUndeliveredFollowupsQueued(reason);
|
|
368
|
+
try {
|
|
369
|
+
this.approvalManager?.rejectAll?.(reason);
|
|
370
|
+
} catch {}
|
|
371
|
+
try {
|
|
372
|
+
await this.client.cancel();
|
|
373
|
+
} catch {}
|
|
374
|
+
const result = {
|
|
375
|
+
ok: true,
|
|
376
|
+
status: 'cancelled',
|
|
377
|
+
task_id: taskId,
|
|
378
|
+
reason,
|
|
379
|
+
};
|
|
380
|
+
this.emit('agent_cancel_requested', result);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
setApprovalAutoMode(enabled, { emit = true } = {}) {
|
|
385
|
+
this.approvalAutoMode = Boolean(enabled);
|
|
386
|
+
if (this.approvalManager) this.approvalManager.setAutoMode(this.approvalAutoMode);
|
|
387
|
+
const state = this.approvalMode();
|
|
388
|
+
if (emit) this.emit('agent_approval_mode', state);
|
|
389
|
+
return state;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
approvalMode() {
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
mode: this.approvalAutoMode ? 'auto' : 'ask',
|
|
396
|
+
auto: this.approvalAutoMode,
|
|
397
|
+
summary: this.approvalManager?.browserSummary?.() || null,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async sendFollowup({ instruction } = {}) {
|
|
402
|
+
const text = String(instruction || '').trim();
|
|
403
|
+
if (!text) {
|
|
404
|
+
const err = new Error('instruction is required');
|
|
405
|
+
err.code = 'BAD_REQUEST';
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
if (!this.running || !this.client) {
|
|
409
|
+
const err = new Error('No running agent turn to follow up');
|
|
410
|
+
err.code = 'CONFLICT';
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const item = {
|
|
415
|
+
instruction: text,
|
|
416
|
+
role: 'user',
|
|
417
|
+
messageType: 'user_intervention',
|
|
418
|
+
priority: 'high',
|
|
419
|
+
idempotencyKey: `local-followup-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
420
|
+
};
|
|
421
|
+
if (!this.client.currentTaskId) {
|
|
422
|
+
this.pendingFollowups.push(item);
|
|
423
|
+
const result = {
|
|
424
|
+
ok: true,
|
|
425
|
+
status: 'waiting_for_task',
|
|
426
|
+
intervention_id: item.idempotencyKey,
|
|
427
|
+
task_id: null,
|
|
428
|
+
};
|
|
429
|
+
this.emit('agent_followup_status', {
|
|
430
|
+
...result,
|
|
431
|
+
instruction: text.slice(0, 500),
|
|
432
|
+
});
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
return this._sendFollowupNow(item);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async _flushQueuedFollowups() {
|
|
439
|
+
if (this.flushingFollowups || !this.client?.currentTaskId || !this.pendingFollowups.length) return;
|
|
440
|
+
this.flushingFollowups = true;
|
|
441
|
+
try {
|
|
442
|
+
while (this.pendingFollowups.length && this.client?.currentTaskId) {
|
|
443
|
+
const item = this.pendingFollowups.shift();
|
|
444
|
+
await this._sendFollowupNow(item);
|
|
445
|
+
}
|
|
446
|
+
} finally {
|
|
447
|
+
this.flushingFollowups = false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
_markUndeliveredFollowupsQueued(error = '') {
|
|
452
|
+
while (this.pendingFollowups.length) {
|
|
453
|
+
const item = this.pendingFollowups.shift();
|
|
454
|
+
const failed = Boolean(error);
|
|
455
|
+
this.emit('agent_followup_status', {
|
|
456
|
+
ok: !failed,
|
|
457
|
+
status: failed ? 'error' : 'queued_next_turn',
|
|
458
|
+
intervention_id: item.idempotencyKey,
|
|
459
|
+
task_id: this.client?.currentTaskId || null,
|
|
460
|
+
instruction: item.instruction.slice(0, 500),
|
|
461
|
+
error: error || null,
|
|
462
|
+
role: item.role || 'user',
|
|
463
|
+
message_type: item.messageType || 'user_intervention',
|
|
464
|
+
priority: item.priority || 'high',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async _sendFollowupNow(item) {
|
|
470
|
+
const result = await this.client.sendIntervention(item.instruction, {
|
|
471
|
+
idempotencyKey: item.idempotencyKey,
|
|
472
|
+
priority: item.priority || 'high',
|
|
473
|
+
});
|
|
474
|
+
const payload = {
|
|
475
|
+
ok: result.status !== 'error',
|
|
476
|
+
status: result.status || 'unknown',
|
|
477
|
+
intervention_id: result.interventionId || item.idempotencyKey,
|
|
478
|
+
task_id: this.client.currentTaskId || null,
|
|
479
|
+
role: item.role || 'user',
|
|
480
|
+
message_type: item.messageType || 'user_intervention',
|
|
481
|
+
priority: item.priority || 'high',
|
|
482
|
+
error: result.error || null,
|
|
483
|
+
http_status: result.httpStatus || null,
|
|
484
|
+
};
|
|
485
|
+
this._ensureJsonlWriter().writeBahulamEvent({
|
|
486
|
+
type: 'user_intervention',
|
|
487
|
+
data: {
|
|
488
|
+
instruction: item.instruction,
|
|
489
|
+
task_id: payload.task_id,
|
|
490
|
+
intervention_id: payload.intervention_id,
|
|
491
|
+
status: payload.status,
|
|
492
|
+
role: payload.role,
|
|
493
|
+
message_type: payload.message_type,
|
|
494
|
+
priority: payload.priority,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
this.emit('agent_followup_status', {
|
|
498
|
+
...payload,
|
|
499
|
+
instruction: item.instruction.slice(0, 500),
|
|
500
|
+
});
|
|
501
|
+
return payload;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async _ensureReady() {
|
|
505
|
+
if (this.ready) return this.ready;
|
|
506
|
+
this.ready = this._initialize().catch((err) => {
|
|
507
|
+
this.ready = null;
|
|
508
|
+
throw err;
|
|
509
|
+
});
|
|
510
|
+
return this.ready;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async _initialize() {
|
|
514
|
+
const auth = new BahulamAuth();
|
|
515
|
+
const creds = auth.loadCredentials();
|
|
516
|
+
if (!creds.token) {
|
|
517
|
+
const err = new Error('Not logged in. Run `bahulam login` from the CLI, then retry.');
|
|
518
|
+
err.code = 'auth_required';
|
|
519
|
+
throw err;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// This service process owns one local workspace. Set cwd to the granted
|
|
523
|
+
// root so existing CLI tools, memory, hooks, and shell defaults behave the
|
|
524
|
+
// same as a terminal launched from that workspace.
|
|
525
|
+
if (this.session.root_path && process.cwd() !== this.session.root_path) {
|
|
526
|
+
process.chdir(this.session.root_path);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const toolExecutor = createToolExecutor();
|
|
530
|
+
await toolExecutor.waitForAutoRegister?.();
|
|
531
|
+
await toolExecutor.registerProjectRoots?.([this.session.root_path], { forceRefresh: false });
|
|
532
|
+
|
|
533
|
+
const approval = new BrowserApprovalManager({
|
|
534
|
+
emit: this.emit,
|
|
535
|
+
cwd: this.session.root_path,
|
|
536
|
+
});
|
|
537
|
+
approval.setAutoMode(this.approvalAutoMode);
|
|
538
|
+
|
|
539
|
+
this.creds = creds;
|
|
540
|
+
this.toolExecutor = toolExecutor;
|
|
541
|
+
this.approvalManager = approval;
|
|
542
|
+
this.client = new BahulamStreamClient({
|
|
543
|
+
baseUrl: creds.backendUrl,
|
|
544
|
+
token: creds.token,
|
|
545
|
+
toolExecutor,
|
|
546
|
+
approvalManager: approval,
|
|
547
|
+
mode: 'remote',
|
|
548
|
+
});
|
|
549
|
+
if (this.resumeSessionId) this.client.sessionId = this.resumeSessionId;
|
|
550
|
+
this._ensureJsonlWriter();
|
|
551
|
+
|
|
552
|
+
this.emit('agent_relay_ready', {
|
|
553
|
+
backend_url: creds.backendUrl,
|
|
554
|
+
root_path: this.session.root_path,
|
|
555
|
+
tools: toolExecutor.listTools?.().length || 0,
|
|
556
|
+
backend_session_id: this.client.sessionId || null,
|
|
557
|
+
approval_mode: this.approvalAutoMode ? 'auto' : 'ask',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
_ensureJsonlWriter() {
|
|
562
|
+
if (!this.jsonlWriter) {
|
|
563
|
+
this.jsonlWriter = new JsonlWriter(this.session.root_path, VERSION);
|
|
564
|
+
}
|
|
565
|
+
if (this.resumeSessionId && !this.jsonlWriter.sessionId) {
|
|
566
|
+
this.jsonlWriter.setSessionId(this.resumeSessionId);
|
|
567
|
+
}
|
|
568
|
+
return this.jsonlWriter;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async _buildExecContext(instruction) {
|
|
572
|
+
await this.toolExecutor.registerProjectRoots?.([this.session.root_path], { forceRefresh: false });
|
|
573
|
+
const projectResources = this.toolExecutor.getProjectResources();
|
|
574
|
+
const execContext = {
|
|
575
|
+
cwd: this.session.root_path,
|
|
576
|
+
project_resources: projectResources,
|
|
577
|
+
work_scope: buildWorkScope({
|
|
578
|
+
instruction,
|
|
579
|
+
cwd: this.session.root_path,
|
|
580
|
+
projectResources,
|
|
581
|
+
}),
|
|
582
|
+
agent_context: this.toolExecutor.getAgentContext(),
|
|
583
|
+
local_service: {
|
|
584
|
+
product: 'bahulam-local-service',
|
|
585
|
+
session_id: this.session.id,
|
|
586
|
+
machine_id: this.session.machine_id || null,
|
|
587
|
+
root_path: this.session.root_path,
|
|
588
|
+
focus_path: this.session.focus_path || '',
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const modelConfig = this.creds.modelConfig || {};
|
|
593
|
+
if (Object.keys(modelConfig).length > 0) {
|
|
594
|
+
execContext.model_overrides = modelConfig;
|
|
595
|
+
if (modelConfig.reasoning) execContext.model_override = modelConfig.reasoning;
|
|
596
|
+
}
|
|
597
|
+
if (this.creds.modelMode) execContext.model_mode = this.creds.modelMode;
|
|
598
|
+
if (this.creds.routePreference) execContext.model_route = this.creds.routePreference;
|
|
599
|
+
return execContext;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
_buildInstruction(prompt, currentPath, attachments = []) {
|
|
603
|
+
const lines = [
|
|
604
|
+
'You are running inside Bahulam Local IDE.',
|
|
605
|
+
`The user granted this local workspace root: ${this.session.root_path}`,
|
|
606
|
+
];
|
|
607
|
+
if (this.session.focus_path) lines.push(`The session focus file is: ${this.session.focus_path}`);
|
|
608
|
+
if (currentPath) lines.push(`The browser currently selected: ${currentPath}`);
|
|
609
|
+
if (attachments.length) {
|
|
610
|
+
lines.push('', 'Attached files for this turn:');
|
|
611
|
+
for (const file of attachments) {
|
|
612
|
+
const hint = attachmentToolHint(file);
|
|
613
|
+
const bits = [
|
|
614
|
+
file.kind || file.viewer || 'file',
|
|
615
|
+
file.mime_type || '',
|
|
616
|
+
file.size != null ? `${file.size} bytes` : '',
|
|
617
|
+
hint ? `suggested_tool=${hint}` : '',
|
|
618
|
+
].filter(Boolean).join(', ');
|
|
619
|
+
lines.push(`- ${file.name || file.path} (${bits}) path=${file.path}`);
|
|
620
|
+
}
|
|
621
|
+
lines.push(
|
|
622
|
+
'Use analyze_image(path=..., question=...) for images.',
|
|
623
|
+
'Use read_table(path=...) for CSV/TSV/Excel-style tables.',
|
|
624
|
+
'Use read_attachment(path=...) for text, PDFs, Markdown, JSON/YAML, and Jupyter notebooks.',
|
|
625
|
+
'For unsupported binary files, inspect metadata or ask before attempting lossy conversion.',
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
lines.push(
|
|
629
|
+
'Use CLI-local tools for file, shell, code, and document work.',
|
|
630
|
+
'Do not operate outside the granted workspace root unless the user explicitly asks and the local permission layer allows it.',
|
|
631
|
+
'',
|
|
632
|
+
'User request:',
|
|
633
|
+
prompt,
|
|
634
|
+
);
|
|
635
|
+
return lines.join('\n');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
_historySnapshot() {
|
|
639
|
+
return {
|
|
640
|
+
ok: true,
|
|
641
|
+
backend_session_id: this.resumeSessionId || null,
|
|
642
|
+
history_mode: this.resumeSessionId ? this.historyMode : 'new',
|
|
643
|
+
messages: browserMessages(this.displayHistory),
|
|
644
|
+
trace: browserTraceItems(this.displayHistory),
|
|
645
|
+
agent_messages: this.agentHistory.length,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
_emitAgentEvent(event, { turnId, contentText = null }) {
|
|
650
|
+
const type = event?.type || 'unknown';
|
|
651
|
+
const data = event?.data || {};
|
|
652
|
+
|
|
653
|
+
switch (type) {
|
|
654
|
+
case 'session_info':
|
|
655
|
+
this.emit('agent_session', { turn_id: turnId, ...pick(data, ['session_id', 'task_id', 'model', 'route']) });
|
|
656
|
+
break;
|
|
657
|
+
case 'status':
|
|
658
|
+
case 'phase_update':
|
|
659
|
+
case 'worker_update':
|
|
660
|
+
this.emit('agent_status', { turn_id: turnId, type, message: data.message || data.status || data.phase || '', data });
|
|
661
|
+
break;
|
|
662
|
+
case 'thinking':
|
|
663
|
+
this.emit('agent_reasoning', { turn_id: turnId, text: data.text || data.thinking || data.delta || '', data });
|
|
664
|
+
break;
|
|
665
|
+
case 'content_partial':
|
|
666
|
+
case 'content': {
|
|
667
|
+
const text = contentText ?? data.text ?? data.content ?? '';
|
|
668
|
+
this.emit('agent_content', { turn_id: turnId, text, partial: type === 'content_partial' });
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
case 'tool_call':
|
|
672
|
+
case 'tool_request':
|
|
673
|
+
this.emit('agent_tool_call', {
|
|
674
|
+
turn_id: turnId,
|
|
675
|
+
tool: data.tool || '',
|
|
676
|
+
call_id: data.call_id || data.request_id || '',
|
|
677
|
+
args: data.args || {},
|
|
678
|
+
internal: Boolean(data.internal || data.sub_agent),
|
|
679
|
+
sub_agent: data.sub_agent || null,
|
|
680
|
+
});
|
|
681
|
+
break;
|
|
682
|
+
case 'tool_result':
|
|
683
|
+
case 'tool_done':
|
|
684
|
+
this.emit('agent_tool_result', {
|
|
685
|
+
turn_id: turnId,
|
|
686
|
+
tool: data.tool || '',
|
|
687
|
+
call_id: data.call_id || data.request_id || '',
|
|
688
|
+
success: data.success !== false,
|
|
689
|
+
duration_ms: data.duration_ms || null,
|
|
690
|
+
output: truncateOutput(data.output || data.message || data.error || ''),
|
|
691
|
+
internal: Boolean(data.internal || data.sub_agent),
|
|
692
|
+
sub_agent: data.sub_agent || null,
|
|
693
|
+
});
|
|
694
|
+
break;
|
|
695
|
+
case 'approval_required':
|
|
696
|
+
this.emit('agent_approval_event', {
|
|
697
|
+
turn_id: turnId,
|
|
698
|
+
state: 'required',
|
|
699
|
+
approval_id: data.tool_id || data.approval_id || '',
|
|
700
|
+
tool: data.tool || '',
|
|
701
|
+
call_id: data.call_id || data.request_id || data.tool_id || '',
|
|
702
|
+
args: data.args || {},
|
|
703
|
+
tier: data.tier || data.risk || '',
|
|
704
|
+
reason: data.reason || '',
|
|
705
|
+
data,
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
case 'approval_granted':
|
|
709
|
+
case 'approval_denied':
|
|
710
|
+
this.emit('agent_approval_event', {
|
|
711
|
+
turn_id: turnId,
|
|
712
|
+
state: type === 'approval_granted' ? 'granted' : 'denied',
|
|
713
|
+
approval_id: data.tool_id || data.approval_id || '',
|
|
714
|
+
tool: data.tool || '',
|
|
715
|
+
call_id: data.call_id || data.request_id || data.tool_id || '',
|
|
716
|
+
args: data.args || {},
|
|
717
|
+
tier: data.tier || data.risk || '',
|
|
718
|
+
reason: data.reason || '',
|
|
719
|
+
data,
|
|
720
|
+
});
|
|
721
|
+
break;
|
|
722
|
+
case 'user_intervention_accepted':
|
|
723
|
+
case 'user_intervention_delivered':
|
|
724
|
+
case 'user_intervention_queued':
|
|
725
|
+
this.emit('agent_followup_event', { turn_id: turnId, type, data });
|
|
726
|
+
break;
|
|
727
|
+
case 'cancelled':
|
|
728
|
+
this.cancellationRequested = true;
|
|
729
|
+
if (!this.cancellationEventEmitted) {
|
|
730
|
+
this.cancellationEventEmitted = true;
|
|
731
|
+
this.emit('agent_turn_cancelled', {
|
|
732
|
+
turn_id: turnId,
|
|
733
|
+
task_id: data.task_id || this.client?.currentTaskId || null,
|
|
734
|
+
reason: data.reason || 'Cancelled by user',
|
|
735
|
+
data,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
739
|
+
case 'sub_agent_start':
|
|
740
|
+
case 'sub_agent_complete':
|
|
741
|
+
case 'delegation':
|
|
742
|
+
this.emit('agent_activity', { turn_id: turnId, type, data });
|
|
743
|
+
break;
|
|
744
|
+
case 'error':
|
|
745
|
+
this.emit('agent_error', { turn_id: turnId, ...data });
|
|
746
|
+
break;
|
|
747
|
+
case 'complete':
|
|
748
|
+
this.emit('agent_complete', { turn_id: turnId, data });
|
|
749
|
+
break;
|
|
750
|
+
default:
|
|
751
|
+
this.emit('agent_event', { turn_id: turnId, type, data });
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function normalizeTurnAttachments(attachments = []) {
|
|
758
|
+
if (!Array.isArray(attachments)) return [];
|
|
759
|
+
return attachments
|
|
760
|
+
.map((file) => ({
|
|
761
|
+
name: String(file?.name || '').slice(0, 240),
|
|
762
|
+
path: String(file?.path || file?.upload_path || '').trim(),
|
|
763
|
+
size: Number.isFinite(Number(file?.size)) ? Number(file.size) : null,
|
|
764
|
+
mime_type: String(file?.mime_type || '').slice(0, 120),
|
|
765
|
+
kind: String(file?.kind || file?.viewer || '').slice(0, 80),
|
|
766
|
+
viewer: String(file?.viewer || '').slice(0, 80),
|
|
767
|
+
}))
|
|
768
|
+
.filter((file) => file.path)
|
|
769
|
+
.slice(0, 20);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function attachmentToolHint(file) {
|
|
773
|
+
const kind = String(file?.kind || file?.viewer || '').toLowerCase();
|
|
774
|
+
const mime = String(file?.mime_type || '').toLowerCase();
|
|
775
|
+
const ext = String(file?.path || file?.name || '').split('.').pop().toLowerCase();
|
|
776
|
+
if (kind === 'image' || mime.startsWith('image/')) return 'analyze_image';
|
|
777
|
+
if (kind === 'spreadsheet' || kind === 'table' || ['csv', 'tsv', 'xlsx', 'xls', 'ods'].includes(ext)) return 'read_table';
|
|
778
|
+
if (['pdf', 'markdown', 'text', 'code', 'config', 'notebook'].includes(kind) || ['txt', 'md', 'mdx', 'pdf', 'json', 'yaml', 'yml', 'toml', 'html', 'xml', 'ipynb', 'log', 'rst', 'sql', 'sh'].includes(ext)) return 'read_attachment';
|
|
779
|
+
return '';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function transcriptsForRoot(rootPath) {
|
|
783
|
+
const target = realpathOrSelf(rootPath);
|
|
784
|
+
const sessions = await getRecentSessions(Infinity);
|
|
785
|
+
return sessions.filter((session) => {
|
|
786
|
+
const project = session.projectPath || session.project || '';
|
|
787
|
+
return project && realpathOrSelf(project) === target;
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function transcriptForRootBySessionId(rootPath, sessionId) {
|
|
792
|
+
const sessions = await transcriptsForRoot(rootPath);
|
|
793
|
+
return sessions.find((session) => session.sessionId === sessionId) || null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function serializeTranscriptSession(session) {
|
|
797
|
+
const tools = Array.isArray(session.toolCalls)
|
|
798
|
+
? session.toolCalls.map((tool) => `${tool.name} x${tool.count}`).slice(0, 4)
|
|
799
|
+
: [];
|
|
800
|
+
return {
|
|
801
|
+
session_id: session.sessionId,
|
|
802
|
+
first_prompt: session.firstPrompt || '',
|
|
803
|
+
project_path: session.projectPath || session.project || '',
|
|
804
|
+
transcript_path: session.filePath || '',
|
|
805
|
+
started_at: session.startTime || null,
|
|
806
|
+
last_activity_at: session.endTime || null,
|
|
807
|
+
status: session.endStatus || 'unknown',
|
|
808
|
+
user_messages: session.userMessages || 0,
|
|
809
|
+
assistant_messages: session.assistantMessages || 0,
|
|
810
|
+
context_tokens: session.contextTokens || 0,
|
|
811
|
+
tools,
|
|
812
|
+
models: Array.isArray(session.models) ? session.models.slice(0, 3) : [],
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function realpathOrSelf(inputPath) {
|
|
817
|
+
try {
|
|
818
|
+
return fs.realpathSync(inputPath);
|
|
819
|
+
} catch {
|
|
820
|
+
return String(inputPath || '');
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function contentDeltaForEvent(event, current) {
|
|
825
|
+
const type = event?.type || '';
|
|
826
|
+
if (type !== 'content_partial' && type !== 'content') return null;
|
|
827
|
+
const text = event?.data?.text ?? event?.data?.content ?? '';
|
|
828
|
+
if (!text) return { delta: '', next: current };
|
|
829
|
+
if (type === 'content_partial') return { delta: text, next: current + text };
|
|
830
|
+
const delta = current && text.startsWith(current)
|
|
831
|
+
? text.slice(current.length)
|
|
832
|
+
: text === current
|
|
833
|
+
? ''
|
|
834
|
+
: text;
|
|
835
|
+
const next = current && !text.startsWith(current) ? current + text : text;
|
|
836
|
+
return { delta, next };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function browserMessages(history = []) {
|
|
840
|
+
return history
|
|
841
|
+
.filter((entry) => entry?.role === 'user' || entry?.role === 'assistant')
|
|
842
|
+
.map((entry) => ({
|
|
843
|
+
role: entry.role,
|
|
844
|
+
content: typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content || ''),
|
|
845
|
+
timestamp: entry.timestamp || null,
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function browserTraceItems(history = []) {
|
|
850
|
+
return history
|
|
851
|
+
.filter((entry) => entry?.role === 'tool')
|
|
852
|
+
.map((entry) => ({
|
|
853
|
+
type: `history_tool_${entry.kind || 'event'}`,
|
|
854
|
+
timestamp: entry.timestamp || null,
|
|
855
|
+
tool: entry.tool || null,
|
|
856
|
+
kind: entry.kind || null,
|
|
857
|
+
content: typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content || ''),
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function pick(obj, keys) {
|
|
862
|
+
return Object.fromEntries(keys.filter(key => obj[key] !== undefined).map(key => [key, obj[key]]));
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function truncateOutput(value, max = 1600) {
|
|
866
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
867
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
868
|
+
}
|