@zq-silk/yui 2.1.0 → 2.2.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 (102) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commandCatalog.js +24 -3
  3. package/dist/cli/commandDiscovery.js +4 -1
  4. package/dist/cli.js +23 -3078
  5. package/dist/commands/taskCommands.js +79 -21
  6. package/dist/commands/taskIntegrationCommands.js +3 -1
  7. package/dist/commands/taskUpstreamCommands.js +3 -1
  8. package/dist/context/runContextPack.js +132 -5
  9. package/dist/context/sourceRunContext.js +4 -2
  10. package/dist/context/taskContext.js +6 -1
  11. package/dist/controlPlaneCli.js +3092 -0
  12. package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
  13. package/dist/controller/jobControl.js +21 -0
  14. package/dist/executor/agentExecutor.js +1 -1
  15. package/dist/executor/effectiveLaunch.js +18 -5
  16. package/dist/executor/fileRoleLaunchPlanner.js +15 -19
  17. package/dist/integration/gitIntegrationService.js +50 -15
  18. package/dist/message/messageContinuation.js +7 -2
  19. package/dist/nativeAgent/agent.js +176 -74
  20. package/dist/nativeAgent/cliDemo.js +37 -0
  21. package/dist/nativeAgent/codingTools.js +11 -0
  22. package/dist/nativeAgent/commandTool.js +215 -0
  23. package/dist/nativeAgent/compactionDemo.js +158 -0
  24. package/dist/nativeAgent/composition.js +71 -0
  25. package/dist/nativeAgent/context/budget.js +44 -0
  26. package/dist/nativeAgent/context/index.js +339 -0
  27. package/dist/nativeAgent/context/providerCompressor.js +103 -0
  28. package/dist/nativeAgent/demo.js +12 -1
  29. package/dist/nativeAgent/evaluation/cases.js +38 -0
  30. package/dist/nativeAgent/evaluation/checks.js +91 -0
  31. package/dist/nativeAgent/evaluation/demo.js +19 -0
  32. package/dist/nativeAgent/evaluation/files.js +54 -0
  33. package/dist/nativeAgent/evaluation/fixture.js +36 -0
  34. package/dist/nativeAgent/evaluation/index.js +239 -0
  35. package/dist/nativeAgent/executionOwner.js +209 -0
  36. package/dist/nativeAgent/filePatterns.js +170 -0
  37. package/dist/nativeAgent/fileToolsSupport.js +202 -0
  38. package/dist/nativeAgent/index.js +13 -0
  39. package/dist/nativeAgent/interaction/cli.js +358 -0
  40. package/dist/nativeAgent/interaction/contracts.js +1 -0
  41. package/dist/nativeAgent/interaction/index.js +3 -0
  42. package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
  43. package/dist/nativeAgent/interaction/renderer.js +34 -0
  44. package/dist/nativeAgent/localSafety.js +258 -0
  45. package/dist/nativeAgent/model/anthropicMessages.js +204 -0
  46. package/dist/nativeAgent/model/chatCompletions.js +210 -0
  47. package/dist/nativeAgent/model/errors.js +47 -0
  48. package/dist/nativeAgent/model/gateway.js +423 -0
  49. package/dist/nativeAgent/model/index.js +7 -0
  50. package/dist/nativeAgent/model/observationAdapter.js +21 -0
  51. package/dist/nativeAgent/model/protocols.js +19 -0
  52. package/dist/nativeAgent/model/responses.js +263 -0
  53. package/dist/nativeAgent/model/types.js +1 -0
  54. package/dist/nativeAgent/model/wire.js +73 -0
  55. package/dist/nativeAgent/observability/index.js +220 -0
  56. package/dist/nativeAgent/product/catalog.js +82 -0
  57. package/dist/nativeAgent/product/config.js +295 -0
  58. package/dist/nativeAgent/product/facts.js +30 -0
  59. package/dist/nativeAgent/product/index.js +62 -0
  60. package/dist/nativeAgent/product/location.js +44 -0
  61. package/dist/nativeAgent/product/runtime.js +276 -0
  62. package/dist/nativeAgent/product/storage.js +49 -0
  63. package/dist/nativeAgent/product/tools.js +47 -0
  64. package/dist/nativeAgent/product/transport.js +54 -0
  65. package/dist/nativeAgent/projectGuidance/index.js +425 -0
  66. package/dist/nativeAgent/searchTools.js +305 -0
  67. package/dist/nativeAgent/session/backends.js +293 -0
  68. package/dist/nativeAgent/session/catalog.js +97 -0
  69. package/dist/nativeAgent/session/catalogDemo.js +87 -0
  70. package/dist/nativeAgent/session/contracts.js +1 -0
  71. package/dist/nativeAgent/session/format.js +269 -0
  72. package/dist/nativeAgent/session/index.js +5 -0
  73. package/dist/nativeAgent/session/location.js +36 -0
  74. package/dist/nativeAgent/session/sqliteFormat.js +134 -0
  75. package/dist/nativeAgent/session/store.js +248 -0
  76. package/dist/nativeAgent/textTools.js +270 -133
  77. package/dist/nativeAgent/toolManager/executor.js +290 -0
  78. package/dist/nativeAgent/toolManager/index.js +4 -0
  79. package/dist/nativeAgent/validation.js +2 -2
  80. package/dist/task/taskAuthority.js +56 -0
  81. package/dist/web/assets/client/app.js +64 -3
  82. package/dist/web/assets/client/detail.js +6 -6
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/overview.js +12 -9
  85. package/dist/web/assets/client/sidebar.js +31 -2
  86. package/dist/web/assets/shell.js +2 -1
  87. package/dist/web/assets/styles/components.js +2 -0
  88. package/dist/web/assets/styles/layout.js +11 -4
  89. package/dist/web/assets/styles/responsive.js +12 -4
  90. package/dist/web/assets/styles/views.js +16 -13
  91. package/docs/agent-result-consumption.md +16 -0
  92. package/docs/agent-result-consumption.zh-CN.md +13 -0
  93. package/docs/examples/agent-offline.mjs +194 -0
  94. package/docs/native-agent.md +283 -0
  95. package/docs/release-workflow.md +47 -9
  96. package/docs/release-workflow.zh-CN.md +36 -6
  97. package/docs/roles-and-configuration.md +32 -0
  98. package/docs/roles-and-configuration.zh-CN.md +26 -0
  99. package/package.json +1 -1
  100. package/skills/yui-leader/SKILL.md +9 -0
  101. package/skills/yui-reviewer/SKILL.md +5 -0
  102. package/skills/yui-runtime/SKILL.md +7 -0
@@ -0,0 +1,358 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { createTextRenderer, terminalText } from './renderer.js';
3
+ const PAGE = 20;
4
+ const HELP = 'Text: submit | /new [title] | /sessions [offset] | /use ID | /history [offset] | /diagnostics [offset] | /refresh | /more | /cancel | /quit\n';
5
+ /** Caller owns the streams and session service. Detaching never cancels a turn. */
6
+ export async function openCli(options) {
7
+ const { sessions, input, output } = options;
8
+ const help = options.catalog
9
+ ? 'Text: submit | /new [title] | /sessions [CURSOR] | /use ID | /info | /rename REV JSON_TITLE_OR_NULL | /history [CURSOR] | /diagnostics [offset] | /refresh | /more | /cancel | /quit\n'
10
+ : HELP;
11
+ const renderer = options.renderer ?? createTextRenderer();
12
+ let selected = options.initialSessionId ?? (await sessions.create('CLI session')).id;
13
+ let cursor = 0;
14
+ let closed = false;
15
+ let unsubscribe = () => { };
16
+ let unsubscribeDiagnostics = () => { };
17
+ let unsubscribeProgress = () => { };
18
+ let progressQueued = false;
19
+ let pendingProgress;
20
+ let diagnosticsQueued = false;
21
+ let refreshQueued = false;
22
+ let scheduled;
23
+ let queue = Promise.resolve();
24
+ let resolveDone;
25
+ const done = new Promise(resolve => { resolveDone = resolve; });
26
+ const lines = createInterface({ input, crlfDelay: Infinity, terminal: false });
27
+ const finish = (end) => {
28
+ if (closed)
29
+ return;
30
+ closed = true;
31
+ if (scheduled)
32
+ clearImmediate(scheduled);
33
+ unsubscribe();
34
+ try {
35
+ unsubscribeProgress();
36
+ }
37
+ catch (error) {
38
+ end.error ??= error;
39
+ }
40
+ try {
41
+ unsubscribeDiagnostics();
42
+ }
43
+ catch (error) {
44
+ end.error ??= error;
45
+ }
46
+ lines.close();
47
+ lines.off('error', inputError);
48
+ input.off('error', inputError);
49
+ output.off('error', displayError);
50
+ output.off('close', outputClose);
51
+ resolveDone(end);
52
+ };
53
+ const displayError = (error) => finish({ reason: 'display-error', error });
54
+ const inputError = (error) => finish({ reason: 'input-error', error });
55
+ const outputClose = () => displayError(new Error('Output closed'));
56
+ input.on('error', inputError);
57
+ lines.on('error', inputError);
58
+ output.on('error', displayError);
59
+ output.on('close', outputClose);
60
+ const write = async (text) => {
61
+ if (closed)
62
+ return;
63
+ // Await the callback rather than buffering an unbounded amount for slow terminals.
64
+ await new Promise(resolve => {
65
+ try {
66
+ output.write(text, error => {
67
+ // Writable emits its error after this callback. Keep our error listener
68
+ // installed until that event; removing it here causes an unhandled error.
69
+ if (error)
70
+ return resolve();
71
+ resolve();
72
+ });
73
+ }
74
+ catch (error) {
75
+ displayError(error);
76
+ resolve();
77
+ }
78
+ });
79
+ };
80
+ const enqueue = (work) => {
81
+ queue = queue.then(async () => {
82
+ if (closed)
83
+ return;
84
+ try {
85
+ await work();
86
+ }
87
+ catch (error) {
88
+ await write(`[error] ${terminalText(error instanceof Error ? error.message : String(error))}\n`);
89
+ }
90
+ });
91
+ };
92
+ const diagnostics = async (offset) => {
93
+ if (!options.diagnostics) {
94
+ await write('[observation unavailable: no adapter configured]\n');
95
+ return;
96
+ }
97
+ try {
98
+ if (offset !== undefined) {
99
+ const page = await options.diagnostics.query(selected, offset, PAGE);
100
+ if (page.lines.length > PAGE)
101
+ throw new Error('Observation page exceeds limit');
102
+ for (const line of page.lines)
103
+ await write(`[observation] ${terminalText(line)}\n`);
104
+ if (page.nextOffset !== null)
105
+ await write(`[next: /diagnostics ${page.nextOffset}]\n`);
106
+ }
107
+ await write(`[observation health; NOT execution state] ${terminalText(await options.diagnostics.health())}\n`);
108
+ }
109
+ catch (error) {
110
+ await write(`[observation unavailable] ${terminalText(error instanceof Error ? error.message : String(error))}\n`);
111
+ }
112
+ };
113
+ const refresh = async () => {
114
+ const page = await sessions.read(selected, cursor, PAGE);
115
+ if (closed)
116
+ return;
117
+ if (page.records.length > PAGE || !Number.isSafeInteger(page.cursor) || page.cursor < cursor
118
+ || (page.hasMore && page.cursor <= cursor))
119
+ throw new Error('Invalid display page progress');
120
+ let previous = cursor;
121
+ for (const record of page.records) {
122
+ const id = record.kind === 'event' ? record.event.sessionId : record.sessionId;
123
+ if (id !== selected || !Number.isSafeInteger(record.cursor) || record.cursor <= previous || record.cursor > page.cursor) {
124
+ throw new Error('Invalid display record identity/order');
125
+ }
126
+ // Renderer is also an optional display adapter, never an execution sink.
127
+ let rendered;
128
+ try {
129
+ rendered = renderer.record(record);
130
+ }
131
+ catch (error) {
132
+ displayError(error);
133
+ return;
134
+ }
135
+ await write(rendered);
136
+ if (closed)
137
+ return;
138
+ previous = record.cursor;
139
+ cursor = record.cursor;
140
+ }
141
+ if (page.cursor !== previous)
142
+ throw new Error('Display cursor skipped records');
143
+ if (page.diagnostic)
144
+ await write(`[diagnostic] ${terminalText(page.diagnostic)}\n`);
145
+ if (page.hasMore)
146
+ scheduleRefresh();
147
+ };
148
+ const scheduleRefresh = () => {
149
+ if (closed || refreshQueued || scheduled)
150
+ return;
151
+ scheduled = setImmediate(() => {
152
+ scheduled = undefined;
153
+ refreshQueued = true;
154
+ enqueue(async () => {
155
+ refreshQueued = false;
156
+ await refresh();
157
+ });
158
+ });
159
+ };
160
+ const select = async (id) => {
161
+ // Validate the target before losing the current selection.
162
+ await sessions.read(id, 0, 1);
163
+ if (closed)
164
+ return;
165
+ unsubscribe();
166
+ selected = id;
167
+ cursor = 0;
168
+ // Subscribe before reading so changes concurrent with the read are not missed.
169
+ unsubscribe = sessions.subscribe(selected, scheduleRefresh);
170
+ await write(`[selected ${terminalText(selected, 200)}; replay]\n`);
171
+ await refresh();
172
+ };
173
+ const offset = (argument) => {
174
+ if (!argument)
175
+ return 0;
176
+ if (!/^\d+$/.test(argument) || !Number.isSafeInteger(Number(argument)))
177
+ throw new Error('Expected a non-negative offset');
178
+ return Number(argument);
179
+ };
180
+ const command = async (line) => {
181
+ if (line.length > 65536)
182
+ throw new Error('Input exceeds 65536 characters');
183
+ const match = /^\/(\S+)(?:\s+(.*))?$/.exec(line.trim());
184
+ if (!match) {
185
+ if (!line.trim())
186
+ return;
187
+ const scope = await sessions.submit(selected, line);
188
+ await write(`[submitted ${terminalText(scope.turnId, 100)}]\n`);
189
+ scheduleRefresh();
190
+ return;
191
+ }
192
+ const arg = (match[2] ?? '').trim();
193
+ switch (match[1]) {
194
+ case 'help':
195
+ await write(help);
196
+ break;
197
+ case 'new': {
198
+ const item = await sessions.create(arg || 'CLI session');
199
+ if (!closed)
200
+ await select(item.id);
201
+ break;
202
+ }
203
+ case 'use':
204
+ if (!arg)
205
+ throw new Error('Usage: /use ID');
206
+ await select(arg);
207
+ break;
208
+ case 'sessions': {
209
+ if (options.catalog) {
210
+ const page = await options.catalog.listSessions({ limit: PAGE, ...(arg ? { cursor: arg } : {}) });
211
+ for (const item of page.items)
212
+ await write(`${terminalText(item.sessionId, 200)} ${terminalText(item.title ?? '(untitled)', 400)} [metadata revision ${item.metadataRevision}]\n`);
213
+ if (page.nextCursor !== null)
214
+ await write(`[next: /sessions ${terminalText(page.nextCursor, 4096)}]\n`);
215
+ break;
216
+ }
217
+ const page = await sessions.list(offset(arg), PAGE);
218
+ if (page.sessions.length > PAGE)
219
+ throw new Error('Session page exceeds limit');
220
+ for (const item of page.sessions)
221
+ await write(`${terminalText(item.id, 200)} ${terminalText(item.title, 200)}\n`);
222
+ if (page.nextOffset !== null)
223
+ await write(`[next: /sessions ${page.nextOffset}]\n`);
224
+ break;
225
+ }
226
+ case 'history': {
227
+ if (options.catalog) {
228
+ const page = await options.catalog.readHistory(selected, { limit: PAGE, ...(arg ? { cursor: arg } : {}) });
229
+ await write(`[history facts ${terminalText(selected, 200)} revision ${page.revision}; not execution context]\n`);
230
+ for (const item of page.records) {
231
+ let text;
232
+ try {
233
+ text = renderer.record({ kind: 'event', cursor: item.revision, event: item.event });
234
+ }
235
+ catch (error) {
236
+ displayError(error);
237
+ return;
238
+ }
239
+ await write(`[fact ${item.revision}] ${text}`);
240
+ }
241
+ if (page.nextCursor !== null)
242
+ await write(`[next: /history ${terminalText(page.nextCursor, 4096)}]\n`);
243
+ break;
244
+ }
245
+ const page = await sessions.history(selected, offset(arg), PAGE);
246
+ if (page.messages.length > PAGE)
247
+ throw new Error('History page exceeds limit');
248
+ await write(`[history ${terminalText(selected, 200)} offset ${offset(arg)}]\n`);
249
+ for (const message of page.messages) {
250
+ try {
251
+ await write(renderer.message(message));
252
+ }
253
+ catch (error) {
254
+ displayError(error);
255
+ return;
256
+ }
257
+ }
258
+ if (page.nextOffset !== null)
259
+ await write(`[next: /history ${page.nextOffset}]\n`);
260
+ break;
261
+ }
262
+ case 'info':
263
+ if (!options.catalog || arg)
264
+ throw new Error('Usage: /info requires a catalog');
265
+ await write(`[session info] ${terminalText(JSON.stringify(await options.catalog.getSessionInfo(selected)), 8192)}\n`);
266
+ break;
267
+ case 'rename': {
268
+ const rename = /^(\d+)\s+(.+)$/u.exec(arg);
269
+ if (!options.catalog || !rename || !Number.isSafeInteger(Number(rename[1])))
270
+ throw new Error('Usage: /rename EXPECTED_METADATA_REVISION JSON_TITLE_OR_NULL');
271
+ let title;
272
+ try {
273
+ title = JSON.parse(rename[2]);
274
+ }
275
+ catch {
276
+ throw new Error('Expected JSON string or null title');
277
+ }
278
+ if (title !== null && typeof title !== 'string')
279
+ throw new Error('Expected JSON string or null title');
280
+ const saved = await options.catalog.renameSession(selected, title, Number(rename[1]));
281
+ await write(`[session renamed] ${terminalText(JSON.stringify(saved), 8192)}\n`);
282
+ break;
283
+ }
284
+ case 'diagnostics':
285
+ await diagnostics(offset(arg));
286
+ break;
287
+ case 'refresh':
288
+ cursor = 0;
289
+ await write('[replay from session facts]\n');
290
+ await refresh();
291
+ break;
292
+ case 'more':
293
+ await refresh();
294
+ break;
295
+ case 'cancel': {
296
+ const page = await sessions.read(selected, cursor, 1);
297
+ if (closed)
298
+ return;
299
+ const accepted = page.activeTurnId !== null && await sessions.cancel({ sessionId: selected, turnId: page.activeTurnId });
300
+ await write(accepted ? '[cancel requested; awaiting actual terminal]\n' : '[no matching active turn]\n');
301
+ break;
302
+ }
303
+ case 'quit':
304
+ finish({ reason: 'quit' });
305
+ break;
306
+ default: throw new Error('Unknown command; /help lists commands');
307
+ }
308
+ };
309
+ lines.on('line', line => enqueue(() => command(line)));
310
+ lines.on('close', () => {
311
+ // Let already accepted input lines finish on piped EOF.
312
+ enqueue(async () => finish({ reason: 'eof' }));
313
+ });
314
+ enqueue(async () => {
315
+ await write(help);
316
+ await select(selected);
317
+ if (closed)
318
+ return;
319
+ if (options.progress)
320
+ unsubscribeProgress = options.progress.subscribe(progress => {
321
+ if (closed || progress.sessionId !== selected)
322
+ return;
323
+ const same = pendingProgress?.turnId === progress.turnId;
324
+ const text = (same ? pendingProgress.text : '') + progress.text;
325
+ pendingProgress = { ...progress, text: text.slice(0, 8192),
326
+ omitted: text.length > 8192 || !!pendingProgress?.omitted || (!!pendingProgress && !same) };
327
+ if (progressQueued)
328
+ return;
329
+ progressQueued = true;
330
+ enqueue(async () => {
331
+ const next = pendingProgress;
332
+ pendingProgress = undefined;
333
+ progressQueued = false;
334
+ if (next?.sessionId === selected) {
335
+ await write(`[provisional ${terminalText(next.turnId, 100)}] ${terminalText(next.text)}${next.omitted ? ' [updates omitted]' : ''}\n`);
336
+ }
337
+ });
338
+ });
339
+ if (!options.diagnostics)
340
+ return;
341
+ try {
342
+ unsubscribeDiagnostics = options.diagnostics.subscribe(() => {
343
+ if (closed || diagnosticsQueued)
344
+ return;
345
+ diagnosticsQueued = true;
346
+ enqueue(async () => { diagnosticsQueued = false; await diagnostics(); });
347
+ });
348
+ }
349
+ catch (error) {
350
+ await write(`[observation unavailable] ${terminalText(error instanceof Error ? error.message : String(error))}\n`);
351
+ }
352
+ });
353
+ if (input.readableEnded)
354
+ enqueue(async () => finish({ reason: 'eof' }));
355
+ if (output.destroyed)
356
+ displayError(new Error('Output unavailable'));
357
+ return { done, close: () => finish({ reason: 'closed' }) };
358
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export { openCli } from './cli.js';
2
+ export { createTextRenderer } from './renderer.js';
3
+ export { createMemoryDemoSessions } from './memoryDemo.js';
@@ -0,0 +1,115 @@
1
+ import { createAgent } from '../index.js';
2
+ /** Disposable fixture adapter, NOT the production session/persistence module. */
3
+ export function createMemoryDemoSessions(options) {
4
+ const sessions = new Map();
5
+ const failures = new Map();
6
+ let nextSession = 0;
7
+ let nextTurn = 0;
8
+ let closing = false;
9
+ const get = (id) => {
10
+ const session = sessions.get(id);
11
+ if (!session)
12
+ throw new Error(`Unknown demo session: ${id}`);
13
+ return session;
14
+ };
15
+ const pageBounds = (offset, limit, length) => {
16
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > length
17
+ || !Number.isInteger(limit) || limit < 1 || limit > 20)
18
+ throw new Error('Invalid page bounds');
19
+ };
20
+ const notify = (session) => {
21
+ for (const listener of session.listeners) {
22
+ try {
23
+ listener();
24
+ }
25
+ catch { /* Optional observer cannot reject recording or execution. */ }
26
+ }
27
+ };
28
+ const append = (session, event) => {
29
+ const snapshot = structuredClone(event);
30
+ session.records.push({ cursor: session.records.length + 1, kind: 'event', event: snapshot });
31
+ if (snapshot.data.type === 'message_appended')
32
+ session.messages.push(snapshot.data.message);
33
+ notify(session);
34
+ };
35
+ return {
36
+ get activeCount() { return [...sessions.values()].filter(session => session.active).length; },
37
+ async create(title) {
38
+ if (closing)
39
+ throw new Error('Demo sessions closed');
40
+ const summary = { id: `demo-session-${++nextSession}`, title: title.slice(0, 200) };
41
+ sessions.set(summary.id, { summary, messages: [], records: [], listeners: new Set() });
42
+ return { ...summary };
43
+ },
44
+ async list(offset, limit) {
45
+ const all = [...sessions.values()];
46
+ pageBounds(offset, limit, all.length);
47
+ return { sessions: all.slice(offset, offset + limit).map(session => ({ ...session.summary })),
48
+ nextOffset: offset + limit < all.length ? offset + limit : null };
49
+ },
50
+ async submit(sessionId, input) {
51
+ if (closing)
52
+ throw new Error('Demo sessions closed');
53
+ const session = get(sessionId);
54
+ if (session.active)
55
+ throw new Error('Session already running; cancel or select another session');
56
+ const scope = { sessionId, turnId: `demo-turn-${++nextTurn}` };
57
+ const abort = new AbortController();
58
+ const agent = createAgent({ ...options, recorder: { async record(event) { append(session, event); } } });
59
+ const history = structuredClone(session.messages);
60
+ // Defer execution until the active handle is installed, even for immediate providers.
61
+ const done = Promise.resolve().then(async () => {
62
+ try {
63
+ await agent.runTurn({ ...scope, input, history, maxSteps: options.maxSteps ?? 8, signal: abort.signal });
64
+ }
65
+ catch (error) {
66
+ // Unexpected/preflight rejection has no trustworthy execution terminal.
67
+ // Preserve an explicit diagnostic, never fabricate an AgentEvent or replay.
68
+ failures.set(sessionId, `Execution rejected; terminal unconfirmed; no automatic replay: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
70
+ finally {
71
+ session.active = undefined;
72
+ notify(session);
73
+ }
74
+ });
75
+ failures.delete(sessionId);
76
+ session.active = { scope, abort, done };
77
+ return { ...scope };
78
+ },
79
+ async cancel(scope) {
80
+ const active = get(scope.sessionId).active;
81
+ if (!active || active.scope.turnId !== scope.turnId)
82
+ return false;
83
+ active.abort.abort();
84
+ return true;
85
+ },
86
+ async read(id, after, limit) {
87
+ const session = get(id);
88
+ pageBounds(after, limit, session.records.length);
89
+ const records = structuredClone(session.records.slice(after, after + limit));
90
+ return { records, cursor: records.at(-1)?.cursor ?? after,
91
+ hasMore: after + records.length < session.records.length, activeTurnId: session.active?.scope.turnId ?? null,
92
+ diagnostic: failures.get(id) };
93
+ },
94
+ async history(id, offset, limit) {
95
+ const session = get(id);
96
+ pageBounds(offset, limit, session.messages.length);
97
+ return { messages: structuredClone(session.messages.slice(offset, offset + limit)),
98
+ nextOffset: offset + limit < session.messages.length ? offset + limit : null };
99
+ },
100
+ subscribe(id, listener) {
101
+ const session = get(id);
102
+ session.listeners.add(listener);
103
+ return () => { session.listeners.delete(listener); };
104
+ },
105
+ async close() {
106
+ closing = true;
107
+ const active = [...sessions.values()].flatMap(session => session.active ? [session.active] : []);
108
+ for (const turn of active)
109
+ turn.abort.abort();
110
+ await Promise.all(active.map(turn => turn.done));
111
+ for (const session of sessions.values())
112
+ session.listeners.clear();
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,34 @@
1
+ /** Do not let model/tool output control the user's terminal (including OSC/CSI). */
2
+ export function terminalText(value, limit = 2000) {
3
+ const clipped = value.slice(0, limit);
4
+ const safe = clipped.replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
5
+ return safe + (clipped.length < value.length ? ` [truncated ${value.length - clipped.length} chars]` : '');
6
+ }
7
+ export function createTextRenderer() {
8
+ const message = (item) => {
9
+ if (item.role === 'tool') {
10
+ const result = item.outcome.ok ? item.outcome.content
11
+ : `${item.outcome.error.code} (effect=${item.outcome.error.effect}): ${item.outcome.error.message}`;
12
+ return `[tool result ${terminalText(item.name, 100)} ${terminalText(item.toolCallId, 100)}] ${terminalText(result)}\n`;
13
+ }
14
+ const calls = item.role === 'assistant' ? item.toolCalls.map(call => `[tool call ${terminalText(call.name, 100)} ${terminalText(call.id, 100)}] ${terminalText(JSON.stringify(call.arguments))}\n`).join('') : '';
15
+ return `[message ${item.role}] ${terminalText(item.content)}\n${calls}`;
16
+ };
17
+ return {
18
+ message,
19
+ record(record) {
20
+ if (record.kind === 'text_delta') {
21
+ return `[provisional ${terminalText(record.turnId, 100)}] ${terminalText(record.text)}\n`;
22
+ }
23
+ const event = record.event;
24
+ switch (event.data.type) {
25
+ case 'message_appended': return message(event.data.message);
26
+ case 'turn_started': return `[turn ${terminalText(event.turnId, 100)} started]\n`;
27
+ case 'step_started': return `[step ${event.data.step} started]\n`;
28
+ case 'tool_started': return `[tool running ${terminalText(event.data.name, 100)} ${terminalText(event.data.toolCallId, 100)}]\n`;
29
+ case 'step_ended': return `[step ${event.data.step} ended]\n`;
30
+ case 'turn_ended': return `[ended: ${event.data.reason}]${event.data.errorCode ? ` ${terminalText(event.data.errorCode, 100)}` : ''}\n`;
31
+ }
32
+ },
33
+ };
34
+ }