@bbigbang/agent-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/agentHost.js +483 -0
  2. package/dist/appVersion.js +14 -0
  3. package/dist/assetCachePaths.js +35 -0
  4. package/dist/attachmentInput.js +588 -0
  5. package/dist/attachmentMaterializer.js +230 -0
  6. package/dist/bigbangCli.js +17 -0
  7. package/dist/bigbangMessageSendDetection.js +284 -0
  8. package/dist/builtinSkillRoots.js +54 -0
  9. package/dist/claudeConfig.js +32 -0
  10. package/dist/claudeDirectRuntime.js +1960 -0
  11. package/dist/claudeSessionControls.js +78 -0
  12. package/dist/claudeTranscriptFs.js +147 -0
  13. package/dist/codexAppServerClient.js +188 -0
  14. package/dist/codexAppServerEnv.js +14 -0
  15. package/dist/codexAppServerRpc.js +273 -0
  16. package/dist/codexAppServerRuntime.js +3495 -0
  17. package/dist/codexBuiltinPrompt.js +117 -0
  18. package/dist/codexConversationSummarizer.js +76 -0
  19. package/dist/codexTranscriptFs.js +145 -0
  20. package/dist/config.js +129 -0
  21. package/dist/connection.js +151 -0
  22. package/dist/dispatchQueueStore.js +39 -0
  23. package/dist/dreamEnv.js +1 -0
  24. package/dist/dreamMemoryFallback.js +118 -0
  25. package/dist/dreamToolPolicy.js +293 -0
  26. package/dist/droidMissionRunner.js +808 -0
  27. package/dist/executor.js +1078 -0
  28. package/dist/hostRuntime.js +1 -0
  29. package/dist/libraryAuthorityFs.js +74 -0
  30. package/dist/libraryMirror.js +183 -0
  31. package/dist/main.js +1659 -0
  32. package/dist/native-worker/native-worker.mjs +475 -0
  33. package/dist/nativeMissionAgentDispatch.js +463 -0
  34. package/dist/nativeMissionRunner.js +461 -0
  35. package/dist/nativeSkillMounts.js +204 -0
  36. package/dist/nativeWorkerHost.js +142 -0
  37. package/dist/nodeSink.js +142 -0
  38. package/dist/panelHttpFetch.js +334 -0
  39. package/dist/runtimeDrivers.js +62 -0
  40. package/dist/skillFs.js +229 -0
  41. package/dist/soloHost.js +165 -0
  42. package/dist/soloNodeSink.js +138 -0
  43. package/dist/terminalManager.js +254 -0
  44. package/dist/workspaceFs.js +1020 -0
  45. package/dist/workspaceGit.js +694 -0
  46. package/dist/workspaceInspect.js +22 -0
  47. package/package.json +49 -0
@@ -0,0 +1,229 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { normalizeSkillRootsWithBuiltins } from './builtinSkillRoots.js';
4
+ import { WorkspaceFsError } from './workspaceFs.js';
5
+ const MAX_PREVIEW_BYTES = 256 * 1024;
6
+ export function listSkills(skillRoots, requestedPath) {
7
+ const roots = normalizeRoots(skillRoots);
8
+ if (roots.length === 0) {
9
+ return { path: null, roots: [], skills: [], entries: [] };
10
+ }
11
+ if (requestedPath?.trim()) {
12
+ const resolvedPath = resolveSkillPath(roots, requestedPath);
13
+ const stat = resolvedPath.configuredRootSymlink
14
+ ? fs.statSync(resolvedPath.path, { throwIfNoEntry: false })
15
+ : fs.lstatSync(resolvedPath.path, { throwIfNoEntry: false });
16
+ if (!stat)
17
+ throw new WorkspaceFsError('not_found', 'Path not found.');
18
+ if (stat.isSymbolicLink()) {
19
+ throw new WorkspaceFsError('path_outside_workspace', 'Symlink paths are not allowed in skill roots.');
20
+ }
21
+ if (!stat.isDirectory())
22
+ throw new WorkspaceFsError('not_directory', 'Path is not a directory.');
23
+ return {
24
+ path: resolvedPath.path,
25
+ roots: roots.map((root) => root.path),
26
+ skills: [],
27
+ entries: listDirectoryEntries(resolvedPath.path),
28
+ };
29
+ }
30
+ const seen = new Set();
31
+ const skills = [];
32
+ for (const root of roots) {
33
+ for (const skill of scanSkillRoot(root)) {
34
+ if (seen.has(skill.path))
35
+ continue;
36
+ seen.add(skill.path);
37
+ skills.push(skill);
38
+ }
39
+ }
40
+ skills.sort((a, b) => a.name.localeCompare(b.name));
41
+ return {
42
+ path: null,
43
+ roots: roots.map((root) => root.path),
44
+ skills,
45
+ entries: [],
46
+ };
47
+ }
48
+ export function readSkillFile(skillRoots, skillPath) {
49
+ const roots = normalizeRoots(skillRoots);
50
+ const resolvedPath = resolveSkillPath(roots, skillPath);
51
+ const stat = fs.lstatSync(resolvedPath.path, { throwIfNoEntry: false });
52
+ if (!stat)
53
+ throw new WorkspaceFsError('not_found', 'Path not found.');
54
+ if (stat.isSymbolicLink()) {
55
+ throw new WorkspaceFsError('path_outside_workspace', 'Symlink files are not allowed in skill roots.');
56
+ }
57
+ if (!stat.isFile())
58
+ throw new WorkspaceFsError('not_file', 'Path is not a file.');
59
+ if (stat.size > MAX_PREVIEW_BYTES) {
60
+ throw new WorkspaceFsError('file_too_large', `File exceeds preview limit (${MAX_PREVIEW_BYTES} bytes).`);
61
+ }
62
+ const contentBuffer = fs.readFileSync(resolvedPath.path);
63
+ if (looksBinary(contentBuffer)) {
64
+ throw new WorkspaceFsError('binary_file', 'Binary files are not supported for preview.');
65
+ }
66
+ return {
67
+ path: resolvedPath.path,
68
+ content: contentBuffer.toString('utf8'),
69
+ mimeType: resolvedPath.path.toLowerCase().endsWith('.md') ? 'text/markdown' : 'text/plain',
70
+ size: stat.size,
71
+ modifiedAt: Math.floor(stat.mtimeMs),
72
+ };
73
+ }
74
+ function normalizeRoots(skillRoots) {
75
+ const roots = [];
76
+ const seen = new Set();
77
+ for (const value of normalizeSkillRootsWithBuiltins(skillRoots)) {
78
+ if (!value || !path.isAbsolute(value))
79
+ continue;
80
+ const resolved = path.resolve(value);
81
+ if (seen.has(resolved))
82
+ continue;
83
+ const stat = fs.statSync(resolved, { throwIfNoEntry: false });
84
+ if (!stat?.isDirectory())
85
+ continue;
86
+ roots.push({ path: resolved, realPath: fs.realpathSync(resolved) });
87
+ seen.add(resolved);
88
+ }
89
+ return roots;
90
+ }
91
+ function resolveSkillPath(roots, candidatePath) {
92
+ if (!path.isAbsolute(candidatePath)) {
93
+ throw new WorkspaceFsError('path_outside_workspace', 'Skill path must be absolute.');
94
+ }
95
+ const resolved = path.resolve(candidatePath);
96
+ const allowed = roots.some((root) => {
97
+ const relative = path.relative(root.path, resolved);
98
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
99
+ });
100
+ let realPathAllowed = false;
101
+ const stat = fs.lstatSync(resolved, { throwIfNoEntry: false });
102
+ if (stat && !stat.isSymbolicLink()) {
103
+ const realPath = fs.realpathSync(resolved);
104
+ realPathAllowed = roots.some((root) => {
105
+ const relative = path.relative(root.realPath, realPath);
106
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
107
+ });
108
+ }
109
+ if (!allowed && !realPathAllowed) {
110
+ throw new WorkspaceFsError('path_outside_workspace', 'Path escapes configured skill roots.');
111
+ }
112
+ if (!stat)
113
+ return { path: resolved, configuredRootSymlink: false };
114
+ if (stat.isSymbolicLink()) {
115
+ const realPath = fs.realpathSync(resolved);
116
+ const configuredRootSymlink = roots.some((root) => root.path === resolved && root.realPath === realPath);
117
+ if (configuredRootSymlink) {
118
+ return { path: resolved, configuredRootSymlink: true };
119
+ }
120
+ throw new WorkspaceFsError('path_outside_workspace', 'Symlink paths are not allowed in skill roots.');
121
+ }
122
+ if (!realPathAllowed) {
123
+ throw new WorkspaceFsError('path_outside_workspace', 'Path escapes configured skill roots.');
124
+ }
125
+ const hasSymlinkDescendant = roots.some((root) => pathIsInsideOrEqual(root.path, resolved) && pathHasSymlinkDescendant(root.path, resolved));
126
+ if (hasSymlinkDescendant) {
127
+ throw new WorkspaceFsError('path_outside_workspace', 'Symlink paths are not allowed in skill roots.');
128
+ }
129
+ return { path: resolved, configuredRootSymlink: false };
130
+ }
131
+ function pathIsInsideOrEqual(rootPath, candidatePath) {
132
+ const relative = path.relative(rootPath, candidatePath);
133
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
134
+ }
135
+ function pathHasSymlinkDescendant(rootPath, candidatePath) {
136
+ const relative = path.relative(rootPath, candidatePath);
137
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
138
+ return false;
139
+ const parts = relative.split(path.sep).filter(Boolean);
140
+ let current = rootPath;
141
+ for (const part of parts.slice(0, -1)) {
142
+ current = path.join(current, part);
143
+ const stat = fs.lstatSync(current, { throwIfNoEntry: false });
144
+ if (stat?.isSymbolicLink())
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ function listDirectoryEntries(directoryPath) {
150
+ return fs.readdirSync(directoryPath, { withFileTypes: true }).flatMap((entry) => {
151
+ if (entry.isSymbolicLink())
152
+ return [];
153
+ const absoluteEntry = path.join(directoryPath, entry.name);
154
+ const entryStat = fs.lstatSync(absoluteEntry, { throwIfNoEntry: false });
155
+ if (!entryStat || entryStat.isSymbolicLink())
156
+ return [];
157
+ return {
158
+ name: entry.name,
159
+ path: absoluteEntry,
160
+ kind: entry.isDirectory() ? 'directory' : 'file',
161
+ size: entry.isDirectory() ? null : (entryStat?.size ?? null),
162
+ modifiedAt: entryStat?.mtimeMs ? Math.floor(entryStat.mtimeMs) : null,
163
+ };
164
+ }).sort((a, b) => {
165
+ if (a.kind !== b.kind)
166
+ return a.kind === 'directory' ? -1 : 1;
167
+ return a.name.localeCompare(b.name);
168
+ });
169
+ }
170
+ function scanSkillRoot(root) {
171
+ let entries = [];
172
+ try {
173
+ entries = fs.readdirSync(root.path, { withFileTypes: true });
174
+ }
175
+ catch {
176
+ return [];
177
+ }
178
+ const skills = [];
179
+ for (const entry of entries) {
180
+ if (entry.isDirectory()) {
181
+ const skillPath = path.join(root.path, entry.name, 'SKILL.md');
182
+ const stat = fs.lstatSync(skillPath, { throwIfNoEntry: false });
183
+ if (!stat?.isFile())
184
+ continue;
185
+ const realPath = fs.realpathSync(skillPath);
186
+ const relative = path.relative(root.realPath, realPath);
187
+ if (relative.startsWith('..') || path.isAbsolute(relative))
188
+ continue;
189
+ skills.push(parseSkillSummary(entry.name, skillPath, root.path));
190
+ }
191
+ }
192
+ return skills;
193
+ }
194
+ function parseSkillSummary(defaultName, skillPath, sourceRoot) {
195
+ const summary = {
196
+ name: defaultName,
197
+ path: skillPath,
198
+ sourceRoot,
199
+ };
200
+ try {
201
+ const content = fs.readFileSync(skillPath, 'utf8');
202
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
203
+ if (!frontmatter)
204
+ return summary;
205
+ for (const line of frontmatter[1].split('\n')) {
206
+ const colonIndex = line.indexOf(':');
207
+ if (colonIndex === -1)
208
+ continue;
209
+ const key = line.slice(0, colonIndex).trim();
210
+ const value = line.slice(colonIndex + 1).trim();
211
+ if (key === 'name' && value)
212
+ summary.name = value;
213
+ if (key === 'description' && value)
214
+ summary.description = value;
215
+ }
216
+ }
217
+ catch {
218
+ // Ignore malformed or unreadable frontmatter and fall back to basename metadata.
219
+ }
220
+ return summary;
221
+ }
222
+ function looksBinary(content) {
223
+ const limit = Math.min(content.length, 8_000);
224
+ for (let index = 0; index < limit; index += 1) {
225
+ if (content[index] === 0)
226
+ return true;
227
+ }
228
+ return false;
229
+ }
@@ -0,0 +1,165 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { clearAcpSessionId, log, } from '@bbigbang/runtime-acp';
3
+ import { getRuntimeDriver } from '@bbigbang/protocol';
4
+ import { CodexAppServerRuntime } from './codexAppServerRuntime.js';
5
+ import { ensureWorkspaceScaffold, migrateWorkspaceLegacyFiles } from './workspaceFs.js';
6
+ import { SoloNodeSink } from './soloNodeSink.js';
7
+ const SOLO_IDLE_TIMEOUT_MS = 30_000;
8
+ /** Remove transient solo runtime session rows from the node-local DB. */
9
+ export function purgeSoloRuntimeSession(db, sessionKey) {
10
+ clearAcpSessionId(db, sessionKey);
11
+ db.prepare('DELETE FROM sessions WHERE session_key = ?').run(sessionKey);
12
+ db.prepare('DELETE FROM bindings WHERE session_key = ?').run(sessionKey);
13
+ }
14
+ export class SoloHost {
15
+ db;
16
+ soloSessionId;
17
+ workspaceRoot;
18
+ sessionKey;
19
+ runtime;
20
+ send;
21
+ activeRunId = null;
22
+ running = false;
23
+ lastActivityAt = Date.now();
24
+ idleTimer = null;
25
+ closed = false;
26
+ constructor(params) {
27
+ const { msg, db, config, toolAuth, workspaceLockManager, send, spawnImpl } = params;
28
+ this.db = db;
29
+ this.soloSessionId = msg.soloSessionId;
30
+ this.workspaceRoot = msg.workspacePath;
31
+ this.sessionKey = `solo:${msg.soloSessionId}`;
32
+ this.send = send;
33
+ ensureWorkspaceScaffold(this.workspaceRoot, msg.agentName);
34
+ migrateWorkspaceLegacyFiles(this.workspaceRoot);
35
+ const driver = getRuntimeDriver(msg.agentType);
36
+ const runtimeEnv = {
37
+ ...(driver.defaultEnv ?? {}),
38
+ ...(msg.envVars ?? {}),
39
+ };
40
+ const agentArgs = [...driver.args];
41
+ purgeSoloRuntimeSession(db, this.sessionKey);
42
+ this.runtime = new CodexAppServerRuntime({
43
+ db,
44
+ sessionKey: this.sessionKey,
45
+ bindingKey: `solo:${msg.soloSessionId}`,
46
+ toolAuth,
47
+ workspaceRoot: this.workspaceRoot,
48
+ agentCommand: driver.command,
49
+ agentArgs,
50
+ env: runtimeEnv,
51
+ model: msg.model,
52
+ reasoningEffort: msg.reasoningEffort,
53
+ codexMode: msg.codexMode,
54
+ serviceTier: msg.codexServiceTier,
55
+ disabledToolKinds: msg.disabledToolKinds,
56
+ agentSurfaceMode: config.agentSurfaceMode,
57
+ workspaceLockManager,
58
+ soloMode: true,
59
+ ...(spawnImpl ? { spawnImpl } : {}),
60
+ });
61
+ this.resetIdleTimer();
62
+ }
63
+ getSoloSessionId() {
64
+ return this.soloSessionId;
65
+ }
66
+ isRunning() {
67
+ return this.running;
68
+ }
69
+ isClosed() {
70
+ return this.closed;
71
+ }
72
+ touch() {
73
+ this.lastActivityAt = Date.now();
74
+ this.resetIdleTimer();
75
+ }
76
+ async prompt(instruction, systemPromptText) {
77
+ if (this.closed) {
78
+ throw new Error('Solo host is closed.');
79
+ }
80
+ if (this.running) {
81
+ throw new Error('Solo host already has an active run.');
82
+ }
83
+ this.touch();
84
+ const runId = randomUUID();
85
+ this.activeRunId = runId;
86
+ this.running = true;
87
+ const sink = new SoloNodeSink(this.soloSessionId, this.send);
88
+ try {
89
+ const result = await this.runtime.prompt({
90
+ runId,
91
+ promptText: instruction,
92
+ sink,
93
+ uiMode: 'summary',
94
+ systemPromptText,
95
+ actorUserId: 'solo_user',
96
+ });
97
+ this.send({
98
+ type: 'solo.run.end',
99
+ soloSessionId: this.soloSessionId,
100
+ stopReason: result.stopReason,
101
+ ...(result.stopReason === 'failed' ? { error: result.error ?? 'Solo run failed.' } : {}),
102
+ });
103
+ }
104
+ catch (error) {
105
+ const message = String(error?.message ?? error);
106
+ log.warn('[solo-host] prompt error', { soloSessionId: this.soloSessionId, error: message });
107
+ this.send({
108
+ type: 'solo.run.end',
109
+ soloSessionId: this.soloSessionId,
110
+ stopReason: 'failed',
111
+ error: message,
112
+ });
113
+ }
114
+ finally {
115
+ this.running = false;
116
+ this.activeRunId = null;
117
+ purgeSoloRuntimeSession(this.db, this.sessionKey);
118
+ this.touch();
119
+ }
120
+ }
121
+ async steer(text) {
122
+ if (this.closed || !this.running || !this.activeRunId)
123
+ return false;
124
+ this.touch();
125
+ if (!this.runtime.steerCurrentRun)
126
+ return false;
127
+ return this.runtime.steerCurrentRun(this.activeRunId, text);
128
+ }
129
+ async cancel() {
130
+ this.touch();
131
+ if (this.running && this.activeRunId) {
132
+ await this.runtime.cancelCurrentRun(this.activeRunId);
133
+ }
134
+ }
135
+ async respondToPermission(requestId, decision, selectedActionId, responseText, answers) {
136
+ this.touch();
137
+ return this.runtime.respondToPermission(requestId, decision, selectedActionId, responseText, answers);
138
+ }
139
+ isIdleExpired() {
140
+ return !this.running && Date.now() - this.lastActivityAt >= SOLO_IDLE_TIMEOUT_MS;
141
+ }
142
+ async close() {
143
+ this.closed = true;
144
+ if (this.idleTimer) {
145
+ clearTimeout(this.idleTimer);
146
+ this.idleTimer = null;
147
+ }
148
+ if (this.running && this.activeRunId) {
149
+ await this.runtime.cancelCurrentRun(this.activeRunId);
150
+ }
151
+ await this.runtime.close?.();
152
+ purgeSoloRuntimeSession(this.db, this.sessionKey);
153
+ }
154
+ resetIdleTimer() {
155
+ if (this.idleTimer) {
156
+ clearTimeout(this.idleTimer);
157
+ }
158
+ this.idleTimer = setTimeout(() => {
159
+ if (!this.running && this.isIdleExpired()) {
160
+ void this.close();
161
+ }
162
+ }, SOLO_IDLE_TIMEOUT_MS);
163
+ this.idleTimer.unref?.();
164
+ }
165
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * OutboundSink that forwards solo runtime output to core as solo.run.event messages.
3
+ */
4
+ export class SoloNodeSink {
5
+ soloSessionId;
6
+ send;
7
+ hooks;
8
+ constructor(soloSessionId, send, hooks) {
9
+ this.soloSessionId = soloSessionId;
10
+ this.send = send;
11
+ this.hooks = hooks;
12
+ }
13
+ async sendAgentText(text) {
14
+ this.emitEvent({ type: 'content.delta', text });
15
+ }
16
+ async sendActivityText(text) {
17
+ this.emitEvent({ type: 'activity.delta', text });
18
+ }
19
+ async sendText(text) {
20
+ this.emitEvent({ type: 'content.delta', text });
21
+ }
22
+ async sendThinkingText(text) {
23
+ this.emitEvent({ type: 'thinking.delta', text });
24
+ }
25
+ async requestPermission(req) {
26
+ this.hooks?.onPermissionRequest?.();
27
+ this.send({
28
+ type: 'solo.permission.request',
29
+ soloSessionId: this.soloSessionId,
30
+ requestId: req.requestId,
31
+ toolName: req.toolName ?? req.toolTitle,
32
+ toolArgs: req.toolArgs ?? null,
33
+ toolKind: req.toolKind,
34
+ ...(req.approvalKind ? { approvalKind: req.approvalKind } : {}),
35
+ ...(req.title ? { title: req.title } : {}),
36
+ ...(req.description ? { description: req.description } : {}),
37
+ ...(req.input !== undefined ? { input: req.input } : {}),
38
+ ...(req.actions?.length ? { actions: req.actions } : {}),
39
+ });
40
+ }
41
+ async sendUi(event) {
42
+ if (event.kind === 'tool') {
43
+ const toolEvent = event;
44
+ if (event.stage === 'complete') {
45
+ const normalizedStatus = event.status === 'cancelled'
46
+ ? 'cancelled'
47
+ : event.status === 'error' || event.status === 'failed' || event.status === 'declined'
48
+ ? 'failed'
49
+ : 'completed';
50
+ const isError = normalizedStatus === 'failed';
51
+ this.emitEvent({
52
+ type: 'tool.result',
53
+ toolCallId: event.toolCallId ?? '',
54
+ output: toolEvent.output ?? event.detail ?? event.status ?? 'done',
55
+ detail: event.detail,
56
+ error: isError,
57
+ status: normalizedStatus,
58
+ metadata: event.metadata,
59
+ });
60
+ }
61
+ else {
62
+ const normalizedStatus = event.status === 'cancelled'
63
+ ? 'cancelled'
64
+ : event.status === 'error' || event.status === 'failed' || event.status === 'declined'
65
+ ? 'failed'
66
+ : event.status === 'completed'
67
+ ? 'completed'
68
+ : 'running';
69
+ this.emitEvent({
70
+ type: 'tool.call',
71
+ toolCallId: event.toolCallId ?? '',
72
+ name: event.title,
73
+ input: toolEvent.input ?? event.detail ?? null,
74
+ detail: event.detail,
75
+ status: normalizedStatus,
76
+ metadata: event.metadata,
77
+ });
78
+ }
79
+ }
80
+ if (event.kind === 'usage') {
81
+ this.emitEvent({
82
+ type: 'run.usage',
83
+ inputTokens: event.inputTokens,
84
+ cachedInputTokens: event.cachedInputTokens,
85
+ outputTokens: event.outputTokens,
86
+ reasoningOutputTokens: event.reasoningOutputTokens,
87
+ totalTokens: event.totalTokens,
88
+ currentInputTokens: event.currentInputTokens,
89
+ currentCachedInputTokens: event.currentCachedInputTokens,
90
+ modelContextWindow: event.modelContextWindow,
91
+ createdAt: Date.now(),
92
+ metadata: event.metadata,
93
+ });
94
+ }
95
+ if (event.kind === 'compact') {
96
+ this.emitEvent({
97
+ type: 'runtime.compact',
98
+ threadId: event.threadId,
99
+ turnId: event.turnId,
100
+ itemId: event.itemId,
101
+ source: event.source,
102
+ eventKey: event.eventKey,
103
+ createdAt: Date.now(),
104
+ });
105
+ }
106
+ if (event.kind === 'plan_phase') {
107
+ this.emitEvent({
108
+ type: 'plan.phase',
109
+ phase: event.phase,
110
+ createdAt: Date.now(),
111
+ });
112
+ }
113
+ if (event.kind === 'plan' || event.kind === 'task') {
114
+ const createdAt = Date.now();
115
+ this.emitEvent({
116
+ type: event.kind === 'plan' ? 'plan.update' : 'task.update',
117
+ title: event.title,
118
+ detail: event.detail,
119
+ createdAt,
120
+ });
121
+ if (event.silent)
122
+ return;
123
+ const text = event.detail
124
+ ? `\n[${event.kind}] ${event.title}\n${event.detail}\n`
125
+ : `\n[${event.kind}] ${event.title}\n`;
126
+ this.emitEvent({ type: 'content.delta', text });
127
+ }
128
+ }
129
+ async breakTextStream() { }
130
+ async flush() { }
131
+ emitEvent(event) {
132
+ this.send({
133
+ type: 'solo.run.event',
134
+ soloSessionId: this.soloSessionId,
135
+ event,
136
+ });
137
+ }
138
+ }