@timurproko/a1 0.1.1-dev.3 → 0.1.1-dev.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,453 +0,0 @@
1
- import { AGENT_ENGINE_CONTRACT_VERSION, assertAgentCapabilityContract, assertAgentEvent, assertAgentSnapshot, } from "../../foundation/agent-engine-contracts/index.js";
2
- import { WORKSPACE_CONTRACT_VERSION, } from "../../foundation/workspace-contracts/index.js";
3
- import { presentWorkspace } from "./presentation.js";
4
- import { WorkspaceReducer } from "./reducer.js";
5
- import { WorkspaceRouter } from "./router.js";
6
- const DEFAULT_LIMITS = Object.freeze({
7
- maxAgents: 8,
8
- maxMessagesPerAgent: 512,
9
- maxMessageBytes: 256 * 1024,
10
- maxEditorBytes: 64 * 1024,
11
- });
12
- export class StructuredWorkspaceTabs {
13
- router;
14
- #cwd;
15
- #createEngine;
16
- #limits;
17
- #now;
18
- #tabs = new Map();
19
- #listeners = new Set();
20
- #disposed = false;
21
- constructor(options) {
22
- if (!options.cwd || options.cwd.includes("\0"))
23
- throw new TypeError("structured workspace cwd is invalid");
24
- if (typeof options.createEngine !== "function")
25
- throw new TypeError("structured workspace engine factory is required");
26
- this.#cwd = options.cwd;
27
- this.#createEngine = options.createEngine;
28
- this.#limits = validateLimits({ ...DEFAULT_LIMITS, ...options.limits });
29
- this.#now = options.now ?? (() => new Date().toISOString());
30
- this.router = new WorkspaceRouter(new WorkspaceReducer(options.workspaceId ?? "workspace-default"));
31
- }
32
- subscribe(listener) {
33
- this.#listeners.add(listener);
34
- listener(this.view());
35
- return () => this.#listeners.delete(listener);
36
- }
37
- async createAgent(input) {
38
- if (this.#disposed)
39
- return rejected("workspace-disposed", "structured workspace is disposed");
40
- if (this.#tabs.size >= this.#limits.maxAgents)
41
- return rejected("agent-limit", `structured workspace is limited to ${this.#limits.maxAgents} agents`);
42
- if (this.#tabs.has(input.id))
43
- return rejected("duplicate-agent", `structured workspace agent already exists: ${input.id}`);
44
- const sessionId = input.sessionId ?? `${input.id}.session`;
45
- let runtime;
46
- try {
47
- runtime = await this.#createRuntime(input.id, sessionId);
48
- }
49
- catch (error) {
50
- return rejected("engine-start-failed", diagnostic(error));
51
- }
52
- const descriptor = {
53
- id: input.id,
54
- displayName: input.displayName,
55
- adapterId: `engine.${input.id}`,
56
- runtime: "structured",
57
- lifecycle: workspaceLifecycle(runtime.snapshot.lifecycle),
58
- capability: workspaceCapability(runtime.engine, input.id, this.#limits),
59
- createdAt: this.#now(),
60
- recoveryReferenceId: null,
61
- };
62
- const created = await this.router.createAgent(descriptor);
63
- if (created.kind === "rejected") {
64
- await disposeRuntime(runtime.session, runtime.engine);
65
- return created;
66
- }
67
- const tab = {
68
- agentId: input.id,
69
- sessionId,
70
- engine: runtime.engine,
71
- session: runtime.session,
72
- unsubscribe: () => { },
73
- eventTail: Promise.resolve(),
74
- lifecycle: runtime.snapshot.lifecycle,
75
- transcript: this.#boundedSnapshot(runtime.snapshot),
76
- editorText: "",
77
- activeCommandIds: new Set(runtime.snapshot.activeCommandIds),
78
- lastSequence: runtime.snapshot.sequence,
79
- failure: null,
80
- };
81
- tab.unsubscribe = tab.session.subscribe(event => this.#queueEvent(tab, event));
82
- this.#tabs.set(input.id, tab);
83
- await this.#reflectLifecycle(tab, runtime.snapshot.lifecycle, null);
84
- this.#notify();
85
- return applied(this.view(), this.#tabView(tab));
86
- }
87
- async selectAgent(agentId) {
88
- const selected = await this.router.selectAgent(agentId);
89
- if (selected.kind === "rejected")
90
- return selected;
91
- const tab = this.#tabs.get(agentId);
92
- if (!tab)
93
- return rejected("missing-runtime", `structured runtime is missing for ${agentId}`);
94
- this.#notify();
95
- return applied(this.view(), this.#tabView(tab));
96
- }
97
- setEditorText(agentId, text) {
98
- const tab = this.#tabs.get(agentId);
99
- if (!tab)
100
- return rejected("unknown-agent", `structured workspace agent does not exist: ${agentId}`);
101
- if (typeof text !== "string" || byteLength(text) > this.#limits.maxEditorBytes) {
102
- return rejected("editor-limit", `structured editor exceeds ${this.#limits.maxEditorBytes} bytes`);
103
- }
104
- tab.editorText = text;
105
- this.#notify();
106
- return applied(this.view(), this.#tabView(tab));
107
- }
108
- async submitSelected() {
109
- const selectedAgentId = this.router.view().selectedAgentId;
110
- if (!selectedAgentId)
111
- return rejected("no-selected-agent", "no structured workspace agent is selected");
112
- const tab = this.#tabs.get(selectedAgentId);
113
- if (!tab)
114
- return rejected("missing-runtime", `structured runtime is missing for ${selectedAgentId}`);
115
- return await this.sendPrompt(selectedAgentId, tab.editorText);
116
- }
117
- async sendPrompt(agentId, text) {
118
- if (typeof text !== "string" || text.length === 0)
119
- return rejected("empty-prompt", "structured prompt must not be empty");
120
- if (byteLength(text) > this.#limits.maxEditorBytes)
121
- return rejected("editor-limit", `structured prompt exceeds ${this.#limits.maxEditorBytes} bytes`);
122
- const routed = await this.router.sendStructuredCommand(agentId, "prompt", { text });
123
- if (routed.kind === "rejected")
124
- return routed;
125
- const tab = this.#tabs.get(agentId);
126
- if (!tab)
127
- return rejected("missing-runtime", `structured runtime is missing for ${agentId}`);
128
- const correlationId = routed.value.correlationId;
129
- tab.activeCommandIds.add(correlationId);
130
- if (tab.editorText === text)
131
- tab.editorText = "";
132
- let outcome;
133
- try {
134
- outcome = await tab.session.execute({
135
- contractVersion: AGENT_ENGINE_CONTRACT_VERSION,
136
- type: "prompt",
137
- commandId: correlationId,
138
- sessionId: tab.sessionId,
139
- text,
140
- });
141
- }
142
- catch (error) {
143
- outcome = "failed";
144
- await this.#failTab(tab, "command-failed", diagnostic(error));
145
- }
146
- if (outcome !== "accepted") {
147
- tab.activeCommandIds.delete(correlationId);
148
- await this.router.settleStructuredCommand(agentId, correlationId, outcome === "completed" ? "completed" : "failed");
149
- }
150
- this.#notify();
151
- return applied(this.view(), { correlationId, outcome });
152
- }
153
- async stopAgent(agentId) {
154
- const tab = this.#tabs.get(agentId);
155
- if (!tab)
156
- return rejected("unknown-agent", `structured workspace agent does not exist: ${agentId}`);
157
- tab.unsubscribe();
158
- try {
159
- await disposeRuntime(tab.session, tab.engine);
160
- }
161
- catch (error) {
162
- await this.#failTab(tab, "stop-failed", diagnostic(error));
163
- return rejected("stop-failed", diagnostic(error));
164
- }
165
- tab.lifecycle = "stopped";
166
- tab.activeCommandIds.clear();
167
- const stopped = await this.router.stopAgent(agentId);
168
- if (stopped.kind === "rejected")
169
- return stopped;
170
- this.#notify();
171
- return applied(this.view(), this.#tabView(tab));
172
- }
173
- async restartAgent(agentId) {
174
- const tab = this.#tabs.get(agentId);
175
- if (!tab)
176
- return rejected("unknown-agent", `structured workspace agent does not exist: ${agentId}`);
177
- await this.router.restartAgent(agentId);
178
- tab.unsubscribe();
179
- await disposeRuntime(tab.session, tab.engine).catch(() => undefined);
180
- try {
181
- const runtime = await this.#createRuntime(agentId, tab.sessionId);
182
- tab.engine = runtime.engine;
183
- tab.session = runtime.session;
184
- tab.lifecycle = runtime.snapshot.lifecycle;
185
- tab.transcript = this.#boundedSnapshot(runtime.snapshot);
186
- tab.activeCommandIds = new Set(runtime.snapshot.activeCommandIds);
187
- tab.lastSequence = runtime.snapshot.sequence;
188
- tab.failure = null;
189
- tab.eventTail = Promise.resolve();
190
- tab.unsubscribe = tab.session.subscribe(event => this.#queueEvent(tab, event));
191
- await this.#reflectLifecycle(tab, runtime.snapshot.lifecycle, null);
192
- this.#notify();
193
- return applied(this.view(), this.#tabView(tab));
194
- }
195
- catch (error) {
196
- await this.#failTab(tab, "restart-failed", diagnostic(error));
197
- return rejected("restart-failed", diagnostic(error));
198
- }
199
- }
200
- async refreshAgent(agentId) {
201
- const tab = this.#tabs.get(agentId);
202
- if (!tab)
203
- return rejected("unknown-agent", `structured workspace agent does not exist: ${agentId}`);
204
- try {
205
- const snapshot = await tab.session.snapshot();
206
- assertAgentSnapshot(snapshot);
207
- if (snapshot.sessionId !== tab.sessionId)
208
- throw new TypeError("structured snapshot session identity changed");
209
- tab.lifecycle = snapshot.lifecycle;
210
- tab.transcript = this.#boundedSnapshot(snapshot);
211
- tab.activeCommandIds = new Set(snapshot.activeCommandIds);
212
- tab.lastSequence = snapshot.sequence;
213
- tab.failure = null;
214
- await this.#reflectLifecycle(tab, snapshot.lifecycle, null);
215
- this.#notify();
216
- return applied(this.view(), this.#tabView(tab));
217
- }
218
- catch (error) {
219
- await this.#failTab(tab, "snapshot-failed", diagnostic(error));
220
- return rejected("snapshot-failed", diagnostic(error));
221
- }
222
- }
223
- async removeAgent(agentId) {
224
- const tab = this.#tabs.get(agentId);
225
- if (!tab)
226
- return rejected("unknown-agent", `structured workspace agent does not exist: ${agentId}`);
227
- const removed = await this.router.removeAgent(agentId);
228
- if (removed.kind === "rejected")
229
- return removed;
230
- tab.unsubscribe();
231
- await disposeRuntime(tab.session, tab.engine).catch(() => undefined);
232
- this.#tabs.delete(agentId);
233
- this.#notify();
234
- return applied(this.view(), agentId);
235
- }
236
- async flush() {
237
- await Promise.all([...this.#tabs.values()].map(tab => tab.eventTail));
238
- }
239
- view() {
240
- const workspace = this.router.view();
241
- const panels = workspace.agents.flatMap(agent => {
242
- const tab = this.#tabs.get(agent.id);
243
- return tab ? [this.#tabView(tab)] : [];
244
- });
245
- const presentation = presentWorkspace(workspace);
246
- const rows = new Map(presentation.rows.map(row => [row.agentId, row]));
247
- const tabs = panels.map(panel => {
248
- const row = rows.get(panel.agentId);
249
- return Object.freeze({
250
- role: "tab",
251
- agentId: panel.agentId,
252
- label: row?.label ?? panel.agentId,
253
- selected: panel.selected,
254
- accessibleDescription: row?.accessibleDescription ?? panel.accessibleDescription,
255
- });
256
- });
257
- return Object.freeze({
258
- role: "tablist",
259
- workspace,
260
- presentation,
261
- tabs: Object.freeze(tabs),
262
- panels: Object.freeze(panels),
263
- selectedPanel: panels.find(panel => panel.selected) ?? null,
264
- });
265
- }
266
- async dispose() {
267
- if (this.#disposed)
268
- return;
269
- this.#disposed = true;
270
- const failures = [];
271
- for (const tab of [...this.#tabs.values()].reverse()) {
272
- tab.unsubscribe();
273
- await tab.eventTail.catch(error => failures.push(error));
274
- await disposeRuntime(tab.session, tab.engine).catch(error => failures.push(error));
275
- }
276
- this.#tabs.clear();
277
- this.#listeners.clear();
278
- if (failures.length > 0)
279
- throw new AggregateError(failures, "structured workspace disposal failed");
280
- }
281
- async #createRuntime(agentId, sessionId) {
282
- const engine = await this.#createEngine(agentId);
283
- try {
284
- assertAgentCapabilityContract(engine.capabilities);
285
- const session = await engine.createSession({ sessionId, cwd: this.#cwd });
286
- assertAgentCapabilityContract(session.capabilities);
287
- const snapshot = await session.snapshot();
288
- assertAgentSnapshot(snapshot);
289
- if (session.sessionId !== sessionId || snapshot.sessionId !== sessionId)
290
- throw new TypeError("structured session identity does not match its workspace tab");
291
- return { engine, session, snapshot };
292
- }
293
- catch (error) {
294
- await engine.dispose().catch(() => undefined);
295
- throw error;
296
- }
297
- }
298
- #queueEvent(tab, event) {
299
- tab.eventTail = tab.eventTail
300
- .then(() => this.#applyEvent(tab, event))
301
- .catch(error => this.#failTab(tab, "event-failed", diagnostic(error)))
302
- .then(() => this.#notify());
303
- }
304
- async #applyEvent(tab, event) {
305
- assertAgentEvent(event, tab.session.capabilities);
306
- if (event.sessionId !== tab.sessionId)
307
- throw new TypeError("structured event crossed workspace tab identity");
308
- if (event.sequence <= tab.lastSequence)
309
- return;
310
- if (event.sequence !== tab.lastSequence + 1 || event.type === "snapshot-invalidated") {
311
- await this.refreshAgent(tab.agentId);
312
- return;
313
- }
314
- tab.lastSequence = event.sequence;
315
- if (event.type === "content") {
316
- this.#appendMessage(tab, event.content);
317
- await this.router.recordActivity(tab.agentId);
318
- return;
319
- }
320
- if (event.type === "lifecycle") {
321
- tab.lifecycle = event.lifecycle;
322
- await this.#reflectLifecycle(tab, event.lifecycle, event.reason);
323
- return;
324
- }
325
- if (event.type === "command-outcome") {
326
- tab.activeCommandIds.delete(event.commandId);
327
- await this.router.settleStructuredCommand(tab.agentId, event.commandId, event.outcome === "completed" ? "completed" : "failed");
328
- if (event.outcome === "failed")
329
- await this.router.requestAttention(tab.agentId);
330
- return;
331
- }
332
- if (event.type === "diagnostic") {
333
- if (event.recoverable)
334
- await this.router.requestAttention(tab.agentId);
335
- else
336
- await this.#failTab(tab, event.code, event.message);
337
- }
338
- }
339
- #appendMessage(tab, message) {
340
- if (byteLength(JSON.stringify(message)) > this.#limits.maxMessageBytes)
341
- throw new RangeError("structured message exceeds its workspace tab byte limit");
342
- tab.transcript.push(message);
343
- while (tab.transcript.length > this.#limits.maxMessagesPerAgent)
344
- tab.transcript.shift();
345
- }
346
- #boundedSnapshot(snapshot) {
347
- if (byteLength(JSON.stringify(snapshot)) > snapshot.capabilities.snapshots.maxBytes)
348
- throw new RangeError("structured snapshot exceeds its negotiated byte limit");
349
- const messages = snapshot.content.slice(-this.#limits.maxMessagesPerAgent);
350
- for (const message of messages) {
351
- if (byteLength(JSON.stringify(message)) > this.#limits.maxMessageBytes)
352
- throw new RangeError("structured snapshot message exceeds its workspace tab byte limit");
353
- }
354
- return [...messages];
355
- }
356
- async #reflectLifecycle(tab, lifecycle, reason) {
357
- if (lifecycle === "ready" || lifecycle === "busy")
358
- await this.router.markRecovered(tab.agentId);
359
- else if (lifecycle === "starting")
360
- await this.router.restartAgent(tab.agentId);
361
- else if (lifecycle === "stopped")
362
- await this.router.stopAgent(tab.agentId);
363
- else if (lifecycle === "failed")
364
- await this.#failTab(tab, "engine-failed", reason ?? "structured engine failed");
365
- }
366
- async #failTab(tab, code, message) {
367
- tab.lifecycle = "failed";
368
- tab.failure = `${code}: ${message}`;
369
- tab.activeCommandIds.clear();
370
- await this.router.markFailed(tab.agentId, code, message);
371
- }
372
- #notify() {
373
- const view = this.view();
374
- for (const listener of this.#listeners)
375
- listener(view);
376
- }
377
- #tabView(tab) {
378
- const selected = this.router.view().selectedAgentId === tab.agentId;
379
- const toolMessages = tab.transcript.filter(message => message.role === "tool" || message.content.some(content => content.kind === "tool-call" || content.kind === "tool-result"));
380
- return Object.freeze({
381
- role: "tabpanel",
382
- agentId: tab.agentId,
383
- sessionId: tab.sessionId,
384
- selected,
385
- lifecycle: tab.lifecycle,
386
- transcript: Object.freeze([...tab.transcript]),
387
- toolMessages: Object.freeze(toolMessages),
388
- editorText: tab.editorText,
389
- activeCommandIds: Object.freeze([...tab.activeCommandIds]),
390
- lastSequence: tab.lastSequence,
391
- failure: tab.failure,
392
- accessibleDescription: `${selected ? "selected" : "background"} structured agent ${tab.agentId}; ${tab.lifecycle}; ${tab.transcript.length} messages`,
393
- });
394
- }
395
- }
396
- function workspaceCapability(engine, agentId, limits) {
397
- return {
398
- kind: "structured",
399
- protocolVersion: WORKSPACE_CONTRACT_VERSION,
400
- adapterId: `engine.${agentId}`,
401
- commands: Object.freeze([...engine.capabilities.commands]),
402
- eventTypes: Object.freeze([...engine.capabilities.events]),
403
- snapshots: engine.capabilities.snapshots.supported ? "authoritative" : "none",
404
- resume: engine.capabilities.snapshots.supported ? "snapshot" : "none",
405
- cancellation: engine.capabilities.commands.includes("abort") ? "correlated" : "none",
406
- attachmentTypes: Object.freeze([]),
407
- flow: Object.freeze({
408
- maxEventBytes: limits.maxMessageBytes,
409
- maxSnapshotBytes: engine.capabilities.snapshots.maxBytes,
410
- maxAttachmentBytes: limits.maxMessageBytes,
411
- maxQueuedEvents: limits.maxMessagesPerAgent,
412
- maxConcurrentCommands: 4,
413
- maxReconnectEvents: limits.maxMessagesPerAgent,
414
- }),
415
- };
416
- }
417
- function workspaceLifecycle(lifecycle) {
418
- if (lifecycle === "starting")
419
- return "creating";
420
- if (lifecycle === "stopping")
421
- return "stopping";
422
- if (lifecycle === "stopped")
423
- return "stopped";
424
- if (lifecycle === "failed")
425
- return "failed";
426
- return "ready";
427
- }
428
- function validateLimits(limits) {
429
- for (const [name, value] of Object.entries(limits)) {
430
- if (!Number.isSafeInteger(value) || value <= 0)
431
- throw new RangeError(`structured workspace ${name} must be a positive safe integer`);
432
- }
433
- return Object.freeze(limits);
434
- }
435
- async function disposeRuntime(session, engine) {
436
- const failures = [];
437
- await session.dispose().catch(error => failures.push(error));
438
- await engine.dispose().catch(error => failures.push(error));
439
- if (failures.length > 0)
440
- throw new AggregateError(failures, "structured agent runtime disposal failed");
441
- }
442
- function applied(view, value) {
443
- return { kind: "applied", view, value };
444
- }
445
- function rejected(code, message) {
446
- return { kind: "rejected", code, diagnostic: message };
447
- }
448
- function diagnostic(error) {
449
- return error instanceof Error ? error.message : String(error);
450
- }
451
- function byteLength(value) {
452
- return new TextEncoder().encode(value).byteLength;
453
- }