@timurproko/a1 0.1.1-dev.3 → 0.1.1-dev.4
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/bin/a1-ui.js +2 -2
- package/dist/src/composition/index.d.ts +0 -18
- package/dist/src/composition/index.js +0 -36
- package/dist/src/features/workspace/index.d.ts +0 -1
- package/dist/src/features/workspace/index.js +0 -1
- package/dist/src/features/workspace/router.d.ts +0 -5
- package/dist/src/features/workspace/router.js +0 -26
- package/dist/src/foundation/release/bootstrap.js +1 -3
- package/dist/src/foundation/release/update.js +10 -6
- package/package.json +1 -1
- package/dist/src/composition/structured-workspace-application.d.ts +0 -38
- package/dist/src/composition/structured-workspace-application.js +0 -261
- package/dist/src/features/workspace/structured-tabs.d.ts +0 -83
- package/dist/src/features/workspace/structured-tabs.js +0 -453
package/bin/a1-ui.js
CHANGED
|
@@ -4,11 +4,11 @@ const { runSelectedInteractiveRuntime } = await import("../dist/src/features/lau
|
|
|
4
4
|
|
|
5
5
|
runSelectedInteractiveRuntime(process.env.A1_LAUNCH_PROFILE ?? "a1", {
|
|
6
6
|
ownedUi: async () => {
|
|
7
|
-
const [{ runOwnedUi }, {
|
|
7
|
+
const [{ runOwnedUi }, { composeOwnedUiApplication }] = await Promise.all([
|
|
8
8
|
import("../dist/src/features/owned-ui/index.js"),
|
|
9
9
|
import("../dist/src/composition/index.js"),
|
|
10
10
|
]);
|
|
11
|
-
const application = await
|
|
11
|
+
const application = await composeOwnedUiApplication({ cwd: process.cwd() });
|
|
12
12
|
return await runOwnedUi({ application });
|
|
13
13
|
},
|
|
14
14
|
transparent: async profileId => {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { type AgentEnginePort } from "../foundation/agent-engine-contracts/index.js";
|
|
2
2
|
import { type PiEngineAdapter } from "../foundation/pi-engine-adapter/index.js";
|
|
3
3
|
import { type OwnedUiApplicationPort, type PresentationComponentPort, type PresentationRuntimePort, type PresentationTerminalPort } from "../foundation/presentation-contracts/index.js";
|
|
4
|
-
import { StructuredWorkspaceTabs, type StructuredWorkspaceLimits } from "../features/workspace/index.js";
|
|
5
|
-
import { StructuredWorkspaceApplication } from "./structured-workspace-application.js";
|
|
6
|
-
export * from "./structured-workspace-application.js";
|
|
7
4
|
export interface ProcessCompositionOptions {
|
|
8
5
|
readonly cwd?: string;
|
|
9
6
|
readonly sessionId?: string;
|
|
@@ -17,21 +14,6 @@ export interface OwnedUiCompositionOptions {
|
|
|
17
14
|
readonly createPiAdapter?: () => Promise<PiEngineAdapter>;
|
|
18
15
|
}
|
|
19
16
|
export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOptions): Promise<OwnedUiApplicationPort>;
|
|
20
|
-
export interface StructuredWorkspaceCompositionOptions {
|
|
21
|
-
readonly cwd?: string;
|
|
22
|
-
readonly workspaceId?: string;
|
|
23
|
-
readonly limits?: Partial<StructuredWorkspaceLimits>;
|
|
24
|
-
readonly createEngine?: (agentId: string) => Promise<AgentEnginePort>;
|
|
25
|
-
readonly createPiAdapter?: (agentId: string) => Promise<PiEngineAdapter>;
|
|
26
|
-
}
|
|
27
|
-
export declare function composeStructuredWorkspace(options?: StructuredWorkspaceCompositionOptions): StructuredWorkspaceTabs;
|
|
28
|
-
export interface StructuredWorkspaceApplicationCompositionOptions extends StructuredWorkspaceCompositionOptions {
|
|
29
|
-
readonly terminal?: PresentationTerminalPort;
|
|
30
|
-
readonly mode?: "regular" | "fullscreen";
|
|
31
|
-
readonly initialAgentId?: string;
|
|
32
|
-
readonly initialAgentName?: string;
|
|
33
|
-
}
|
|
34
|
-
export declare function composeStructuredWorkspaceApplication(options?: StructuredWorkspaceApplicationCompositionOptions): Promise<StructuredWorkspaceApplication>;
|
|
35
17
|
export interface ProcessComposition {
|
|
36
18
|
readonly engine: AgentEnginePort;
|
|
37
19
|
createPresentation(root: PresentationComponentPort, terminal: PresentationTerminalPort): PresentationRuntimePort;
|
|
@@ -3,9 +3,6 @@ import { createPiEngineAdapter } from "../foundation/pi-engine-adapter/index.js"
|
|
|
3
3
|
import { createPiPresentationRuntime, createPiTerminalBridge } from "../foundation/pi-tui-runtime-adapter/index.js";
|
|
4
4
|
import { assertPresentationComponent, assertPresentationRuntime, } from "../foundation/presentation-contracts/index.js";
|
|
5
5
|
import { OwnedUiSessionShell } from "../foundation/pi-owned-ui-integration/index.js";
|
|
6
|
-
import { StructuredWorkspaceTabs, } from "../features/workspace/index.js";
|
|
7
|
-
import { StructuredWorkspaceApplication } from "./structured-workspace-application.js";
|
|
8
|
-
export * from "./structured-workspace-application.js";
|
|
9
6
|
const CAPABILITIES = {
|
|
10
7
|
contractVersion: AGENT_ENGINE_CONTRACT_VERSION,
|
|
11
8
|
commands: ["prompt", "steer", "follow-up", "abort", "retry", "compact", "bash", "replace-session"],
|
|
@@ -28,39 +25,6 @@ export async function composeOwnedUiApplication(options = {}) {
|
|
|
28
25
|
dispose: () => shell.dispose(),
|
|
29
26
|
};
|
|
30
27
|
}
|
|
31
|
-
export function composeStructuredWorkspace(options = {}) {
|
|
32
|
-
const cwd = options.cwd ?? process.cwd();
|
|
33
|
-
const createEngine = options.createEngine ?? (async (agentId) => {
|
|
34
|
-
const sessionId = `${agentId}.session`;
|
|
35
|
-
const adapter = options.createPiAdapter
|
|
36
|
-
? await options.createPiAdapter(agentId)
|
|
37
|
-
: await createPiEngineAdapter({ cwd, sessionId });
|
|
38
|
-
return new PiAgentEngineBridge(adapter, cwd);
|
|
39
|
-
});
|
|
40
|
-
return new StructuredWorkspaceTabs({
|
|
41
|
-
cwd,
|
|
42
|
-
createEngine,
|
|
43
|
-
...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
|
|
44
|
-
...(options.limits === undefined ? {} : { limits: options.limits }),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
export async function composeStructuredWorkspaceApplication(options = {}) {
|
|
48
|
-
const workspace = composeStructuredWorkspace(options);
|
|
49
|
-
const created = await workspace.createAgent({
|
|
50
|
-
id: options.initialAgentId ?? "agent-1",
|
|
51
|
-
displayName: options.initialAgentName ?? "Agent 1",
|
|
52
|
-
});
|
|
53
|
-
if (created.kind === "rejected") {
|
|
54
|
-
await workspace.dispose();
|
|
55
|
-
throw new Error(`could not create the initial structured workspace agent: ${created.diagnostic}`);
|
|
56
|
-
}
|
|
57
|
-
return new StructuredWorkspaceApplication({
|
|
58
|
-
workspace,
|
|
59
|
-
cwd: options.cwd ?? process.cwd(),
|
|
60
|
-
...(options.terminal === undefined ? {} : { terminal: options.terminal }),
|
|
61
|
-
...(options.mode === undefined ? {} : { mode: options.mode }),
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
28
|
export async function composeProcess(options = {}) {
|
|
65
29
|
let engine = options.engine;
|
|
66
30
|
if (!engine) {
|
|
@@ -22,15 +22,10 @@ export declare class WorkspaceRouter {
|
|
|
22
22
|
constructor(reducer: WorkspaceReducer);
|
|
23
23
|
createAgent(agent: ManagedAgentDescriptor): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
24
24
|
selectAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
25
|
-
restartAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
26
|
-
markRecovered(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
27
|
-
markFailed(agentId: string, code: string, message: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
28
|
-
requestAttention(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
29
25
|
stopAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
30
26
|
removeAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
31
27
|
recordActivity(agentId: string, amount?: number): Promise<WorkspaceRouterResult<number>>;
|
|
32
28
|
sendStructuredCommand(agentId: string, command: string, payload: unknown): Promise<WorkspaceRouterResult<RoutedStructuredCommand>>;
|
|
33
29
|
cancelStructuredCommand(agentId: string, targetCorrelationId: string): Promise<WorkspaceRouterResult<StructuredCommandRecord>>;
|
|
34
|
-
settleStructuredCommand(agentId: string, correlationId: string, outcome: "completed" | "failed"): Promise<WorkspaceRouterResult<StructuredCommandRecord>>;
|
|
35
30
|
view(): WorkspaceView;
|
|
36
31
|
}
|
|
@@ -23,18 +23,6 @@ export class WorkspaceRouter {
|
|
|
23
23
|
selectAgent(agentId) {
|
|
24
24
|
return this.#enqueue(() => this.reducer.selectAgent(agentId));
|
|
25
25
|
}
|
|
26
|
-
restartAgent(agentId) {
|
|
27
|
-
return this.#enqueue(() => this.reducer.restartAgent(agentId));
|
|
28
|
-
}
|
|
29
|
-
markRecovered(agentId) {
|
|
30
|
-
return this.#enqueue(() => this.reducer.markRecovered(agentId));
|
|
31
|
-
}
|
|
32
|
-
markFailed(agentId, code, message) {
|
|
33
|
-
return this.#enqueue(() => this.reducer.markFailed(agentId, code, message));
|
|
34
|
-
}
|
|
35
|
-
requestAttention(agentId) {
|
|
36
|
-
return this.#enqueue(() => this.reducer.requestAttention(agentId));
|
|
37
|
-
}
|
|
38
26
|
stopAgent(agentId) {
|
|
39
27
|
return this.#enqueue(() => this.reducer.stopAgent(agentId));
|
|
40
28
|
}
|
|
@@ -104,20 +92,6 @@ export class WorkspaceRouter {
|
|
|
104
92
|
return applied(this.reducer.view(), result.record);
|
|
105
93
|
});
|
|
106
94
|
}
|
|
107
|
-
settleStructuredCommand(agentId, correlationId, outcome) {
|
|
108
|
-
return this.#enqueue(() => {
|
|
109
|
-
const agent = this.reducer.view().agents.find(candidate => candidate.id === agentId);
|
|
110
|
-
if (!agent)
|
|
111
|
-
return reject("unknown-agent", `workspace agent does not exist: ${agentId}`);
|
|
112
|
-
const tracker = this.#trackerFor(agent);
|
|
113
|
-
if (!tracker)
|
|
114
|
-
return reject("capability-mismatch", `agent ${agent.id} is not structured`);
|
|
115
|
-
const result = tracker.complete(correlationId, outcome);
|
|
116
|
-
if (result.kind === "rejected")
|
|
117
|
-
return result;
|
|
118
|
-
return applied(this.reducer.view(), result.record);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
95
|
view() {
|
|
122
96
|
return this.reducer.view();
|
|
123
97
|
}
|
|
@@ -42,9 +42,7 @@ export async function runBootstrap(options) {
|
|
|
42
42
|
return await launchUi(retained, environment);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
const candidate = await materializeRelease(options.packageRoot, paths.dataDir
|
|
46
|
-
onProgress: progress => output.write(`${PRODUCT_TEXT.diagnostic(`installing ${progress.fileCount} files.`)}\n`),
|
|
47
|
-
});
|
|
45
|
+
const candidate = await materializeRelease(options.packageRoot, paths.dataDir);
|
|
48
46
|
await stateStore.recordCandidate(candidate);
|
|
49
47
|
state = await stateStore.read();
|
|
50
48
|
if (!state.references.active) {
|
|
@@ -106,9 +106,7 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
async activateInstalled(packageRoot, targetVersion, phase) {
|
|
109
|
-
const candidate = await materializeRelease(packageRoot, paths.dataDir
|
|
110
|
-
onProgress: progress => output.stdout(`${PRODUCT_TEXT.diagnostic(`installing ${progress.fileCount} files.`)}\n`),
|
|
111
|
-
});
|
|
109
|
+
const candidate = await materializeRelease(packageRoot, paths.dataDir);
|
|
112
110
|
if (candidate.packageVersion !== targetVersion)
|
|
113
111
|
throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${targetVersion}`);
|
|
114
112
|
await stateStore.recordCandidate(candidate);
|
|
@@ -198,14 +196,20 @@ export async function runSelfUpdate(options) {
|
|
|
198
196
|
transaction = await transactionStore.advance("ownership-released");
|
|
199
197
|
}
|
|
200
198
|
if (phaseBefore(transaction.phase, "package-installed")) {
|
|
201
|
-
|
|
202
|
-
const installation = await runNpm(runner, ["install", "--global", `${PRODUCT_PACKAGE}@${targetVersion}`], false, output, "start the global npm installation", false);
|
|
199
|
+
const installation = await runNpm(runner, ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_PACKAGE}@${targetVersion}`], true, output, "start the global npm installation", false);
|
|
203
200
|
if (installation.result === null)
|
|
204
201
|
throw new UpdateFailure(installation.exitCode, "npm process failed");
|
|
205
|
-
if (installation.result.code !== 0)
|
|
202
|
+
if (installation.result.code !== 0) {
|
|
203
|
+
if (installation.result.stdout.trim().length > 0)
|
|
204
|
+
output.stderr(`${installation.result.stdout.trimEnd()}\n`);
|
|
206
205
|
throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
|
|
206
|
+
}
|
|
207
207
|
transaction = await transactionStore.advance("package-installed");
|
|
208
208
|
}
|
|
209
|
+
// Ownership can be reacquired after an interrupted installation (for
|
|
210
|
+
// example, if bare A1 is launched before the update is resumed). Recheck
|
|
211
|
+
// immediately before activation so recovery cannot start a second cohort.
|
|
212
|
+
await lifecycle.shutdownVerifiedOwners(targetVersion);
|
|
209
213
|
await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => { transaction = await transactionStore.advance(phase); });
|
|
210
214
|
await transactionStore.advance("supervisor-verified");
|
|
211
215
|
await transactionStore.finish("completed");
|
package/package.json
CHANGED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { OwnedUiSessionShellRoot } from "../foundation/pi-owned-ui-integration/index.js";
|
|
2
|
-
import { PiTuiRuntimeAdapter, type PiTuiComponentPort } from "../foundation/pi-tui-runtime-adapter/index.js";
|
|
3
|
-
import type { OwnedUiApplicationPort, PresentationTerminalPort } from "../foundation/presentation-contracts/index.js";
|
|
4
|
-
import { type StructuredWorkspaceTabs } from "../features/workspace/index.js";
|
|
5
|
-
export interface StructuredWorkspaceApplicationOptions {
|
|
6
|
-
readonly workspace: StructuredWorkspaceTabs;
|
|
7
|
-
readonly cwd: string;
|
|
8
|
-
readonly terminal?: PresentationTerminalPort;
|
|
9
|
-
readonly mode?: "regular" | "fullscreen";
|
|
10
|
-
}
|
|
11
|
-
export declare class StructuredWorkspaceApplication implements OwnedUiApplicationPort {
|
|
12
|
-
#private;
|
|
13
|
-
readonly workspace: StructuredWorkspaceTabs;
|
|
14
|
-
readonly root: OwnedUiSessionShellRoot;
|
|
15
|
-
readonly runtime: PiTuiRuntimeAdapter;
|
|
16
|
-
readonly component: StructuredWorkspaceRootComponent;
|
|
17
|
-
constructor(options: StructuredWorkspaceApplicationOptions);
|
|
18
|
-
get disposed(): boolean;
|
|
19
|
-
start(): void;
|
|
20
|
-
flush(): Promise<void>;
|
|
21
|
-
waitUntilStopped(): Promise<void>;
|
|
22
|
-
submit(raw: string): Promise<void>;
|
|
23
|
-
switchRelative(direction: -1 | 1): Promise<void>;
|
|
24
|
-
dispose(): Promise<void>;
|
|
25
|
-
}
|
|
26
|
-
export declare class StructuredWorkspaceRootComponent implements PiTuiComponentPort {
|
|
27
|
-
readonly workspace: StructuredWorkspaceTabs;
|
|
28
|
-
readonly sessionRoot: OwnedUiSessionShellRoot;
|
|
29
|
-
readonly onSwitch: (direction: -1 | 1) => void;
|
|
30
|
-
readonly onEditorChange: (text: string) => void;
|
|
31
|
-
readonly notice: () => string | null;
|
|
32
|
-
constructor(workspace: StructuredWorkspaceTabs, sessionRoot: OwnedUiSessionShellRoot, onSwitch: (direction: -1 | 1) => void, onEditorChange: (text: string) => void, notice: () => string | null);
|
|
33
|
-
render(width: number): readonly string[];
|
|
34
|
-
handleInput(data: string): void;
|
|
35
|
-
invalidate(): void;
|
|
36
|
-
setFocused(focused: boolean): void;
|
|
37
|
-
dispose(): void;
|
|
38
|
-
}
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { OWNED_UI_CONTRACT_VERSION, } from "../foundation/owned-ui-contracts/index.js";
|
|
2
|
-
import { OwnedUiSessionShellRoot } from "../foundation/pi-owned-ui-integration/index.js";
|
|
3
|
-
import { createPiTerminalBridge, PiTuiRuntimeAdapter, } from "../foundation/pi-tui-runtime-adapter/index.js";
|
|
4
|
-
import {} from "../features/workspace/index.js";
|
|
5
|
-
export class StructuredWorkspaceApplication {
|
|
6
|
-
workspace;
|
|
7
|
-
root;
|
|
8
|
-
runtime;
|
|
9
|
-
component;
|
|
10
|
-
#unsubscribe;
|
|
11
|
-
#resolveStopped;
|
|
12
|
-
#stopped;
|
|
13
|
-
#notice = null;
|
|
14
|
-
#disposed = false;
|
|
15
|
-
#started = false;
|
|
16
|
-
#agentSequence;
|
|
17
|
-
constructor(options) {
|
|
18
|
-
this.workspace = options.workspace;
|
|
19
|
-
const initial = selectedPanel(options.workspace.view());
|
|
20
|
-
if (!initial)
|
|
21
|
-
throw new TypeError("structured workspace application requires an initial agent tab");
|
|
22
|
-
this.#agentSequence = options.workspace.view().panels.length;
|
|
23
|
-
this.#stopped = new Promise(resolve => { this.#resolveStopped = resolve; });
|
|
24
|
-
this.root = new OwnedUiSessionShellRoot(toOwnedView(options.workspace.view(), initial, null, 80, 24), options.cwd, {
|
|
25
|
-
getColumns: () => this.runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
|
|
26
|
-
getRows: () => this.runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
|
|
27
|
-
requestRender: () => this.runtime?.requestRender(),
|
|
28
|
-
onSubmit: text => { void this.submit(text); },
|
|
29
|
-
onInterrupt: () => { this.#notice = "Use /agent stop to stop the selected agent."; this.runtime?.requestRender(); },
|
|
30
|
-
onClear: () => { this.root.editor.setText(""); this.runtime?.requestRender(); },
|
|
31
|
-
onExit: () => { void this.dispose(); },
|
|
32
|
-
onModelSelect: () => { this.#notice = "Model selection remains scoped to each structured agent session."; this.runtime?.requestRender(); },
|
|
33
|
-
onThinkingCycle: () => { this.#notice = "Thinking settings remain scoped to each structured agent session."; this.runtime?.requestRender(); },
|
|
34
|
-
});
|
|
35
|
-
this.component = new StructuredWorkspaceRootComponent(this.workspace, this.root, direction => { void this.switchRelative(direction); }, text => this.#saveEditor(text), () => this.#notice);
|
|
36
|
-
this.runtime = new PiTuiRuntimeAdapter({
|
|
37
|
-
root: this.component,
|
|
38
|
-
mode: options.mode ?? "regular",
|
|
39
|
-
...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
|
|
40
|
-
hardwareCursor: true,
|
|
41
|
-
mouse: false,
|
|
42
|
-
});
|
|
43
|
-
this.#unsubscribe = this.workspace.subscribe(view => this.#sync(view));
|
|
44
|
-
}
|
|
45
|
-
get disposed() { return this.#disposed; }
|
|
46
|
-
start() {
|
|
47
|
-
if (this.#started || this.#disposed)
|
|
48
|
-
return;
|
|
49
|
-
this.#started = true;
|
|
50
|
-
this.runtime.start();
|
|
51
|
-
this.#sync(this.workspace.view());
|
|
52
|
-
}
|
|
53
|
-
async flush() {
|
|
54
|
-
await this.workspace.flush();
|
|
55
|
-
this.#sync(this.workspace.view());
|
|
56
|
-
}
|
|
57
|
-
waitUntilStopped() { return this.#stopped; }
|
|
58
|
-
async submit(raw) {
|
|
59
|
-
const input = raw.trim();
|
|
60
|
-
if (!input)
|
|
61
|
-
return;
|
|
62
|
-
if (input === "/agent" || input.startsWith("/agent ")) {
|
|
63
|
-
await this.#agentCommand(input.slice("/agent".length).trim());
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const selected = selectedPanel(this.workspace.view());
|
|
67
|
-
if (!selected)
|
|
68
|
-
return;
|
|
69
|
-
this.workspace.setEditorText(selected.agentId, input);
|
|
70
|
-
const result = await this.workspace.sendPrompt(selected.agentId, input);
|
|
71
|
-
this.#notice = result.kind === "rejected" ? result.diagnostic : null;
|
|
72
|
-
this.#sync(this.workspace.view());
|
|
73
|
-
}
|
|
74
|
-
async switchRelative(direction) {
|
|
75
|
-
const view = this.workspace.view();
|
|
76
|
-
if (view.panels.length < 2)
|
|
77
|
-
return;
|
|
78
|
-
this.#saveEditor(this.root.editor.getText());
|
|
79
|
-
const current = Math.max(0, view.panels.findIndex(panel => panel.selected));
|
|
80
|
-
const next = (current + direction + view.panels.length) % view.panels.length;
|
|
81
|
-
const target = view.panels[next];
|
|
82
|
-
if (target)
|
|
83
|
-
await this.workspace.selectAgent(target.agentId);
|
|
84
|
-
}
|
|
85
|
-
async dispose() {
|
|
86
|
-
if (this.#disposed)
|
|
87
|
-
return;
|
|
88
|
-
this.#disposed = true;
|
|
89
|
-
this.#unsubscribe();
|
|
90
|
-
const failures = [];
|
|
91
|
-
await this.runtime.stop().catch(error => failures.push(error));
|
|
92
|
-
await this.workspace.dispose().catch(error => failures.push(error));
|
|
93
|
-
this.#resolveStopped?.();
|
|
94
|
-
if (failures.length > 0)
|
|
95
|
-
throw new AggregateError(failures, "structured workspace application disposal failed");
|
|
96
|
-
}
|
|
97
|
-
async #agentCommand(argument) {
|
|
98
|
-
const [command = "list", ...rest] = argument.split(/\s+/).filter(Boolean);
|
|
99
|
-
const view = this.workspace.view();
|
|
100
|
-
const selected = selectedPanel(view);
|
|
101
|
-
if (command === "new") {
|
|
102
|
-
this.#agentSequence += 1;
|
|
103
|
-
const id = `agent-${this.#agentSequence}`;
|
|
104
|
-
const displayName = rest.join(" ") || `Agent ${this.#agentSequence}`;
|
|
105
|
-
const created = await this.workspace.createAgent({ id, displayName });
|
|
106
|
-
if (created.kind === "applied")
|
|
107
|
-
await this.workspace.selectAgent(id);
|
|
108
|
-
this.#notice = created.kind === "rejected" ? created.diagnostic : `Created ${created.value.agentId}.`;
|
|
109
|
-
}
|
|
110
|
-
else if (command === "next" || command === "previous" || command === "prev") {
|
|
111
|
-
await this.switchRelative(command === "next" ? 1 : -1);
|
|
112
|
-
this.#notice = null;
|
|
113
|
-
}
|
|
114
|
-
else if (command === "select") {
|
|
115
|
-
const target = rest[0];
|
|
116
|
-
const selectedResult = target ? await this.workspace.selectAgent(target) : null;
|
|
117
|
-
this.#notice = selectedResult === null ? "Usage: /agent select <id>" : selectedResult.kind === "rejected" ? selectedResult.diagnostic : null;
|
|
118
|
-
}
|
|
119
|
-
else if (command === "stop") {
|
|
120
|
-
const result = selected ? await this.workspace.stopAgent(selected.agentId) : null;
|
|
121
|
-
this.#notice = result === null ? "No agent selected." : result.kind === "rejected" ? result.diagnostic : `Stopped ${selected.agentId}.`;
|
|
122
|
-
}
|
|
123
|
-
else if (command === "restart") {
|
|
124
|
-
const result = selected ? await this.workspace.restartAgent(selected.agentId) : null;
|
|
125
|
-
this.#notice = result === null ? "No agent selected." : result.kind === "rejected" ? result.diagnostic : `Restarted ${selected.agentId}.`;
|
|
126
|
-
}
|
|
127
|
-
else if (command === "remove") {
|
|
128
|
-
if (!selected)
|
|
129
|
-
this.#notice = "No agent selected.";
|
|
130
|
-
else {
|
|
131
|
-
if (selected.lifecycle !== "stopped" && selected.lifecycle !== "failed")
|
|
132
|
-
await this.workspace.stopAgent(selected.agentId);
|
|
133
|
-
const result = await this.workspace.removeAgent(selected.agentId);
|
|
134
|
-
this.#notice = result.kind === "rejected" ? result.diagnostic : `Removed ${selected.agentId}.`;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else if (command === "list") {
|
|
138
|
-
this.#notice = view.tabs.length === 0 ? "No managed agents." : view.tabs.map(tab => `${tab.selected ? "*" : " "}${tab.agentId}:${tab.label}`).join(" ");
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
this.#notice = "Usage: /agent [list|new [name]|next|prev|select <id>|stop|restart|remove]";
|
|
142
|
-
}
|
|
143
|
-
this.#sync(this.workspace.view());
|
|
144
|
-
}
|
|
145
|
-
#saveEditor(text) {
|
|
146
|
-
const selected = selectedPanel(this.workspace.view());
|
|
147
|
-
if (selected)
|
|
148
|
-
this.workspace.setEditorText(selected.agentId, text);
|
|
149
|
-
}
|
|
150
|
-
#sync(view) {
|
|
151
|
-
const selected = selectedPanel(view);
|
|
152
|
-
if (!selected)
|
|
153
|
-
return;
|
|
154
|
-
const viewport = this.runtime?.viewport() ?? { columns: 80, rows: 24 };
|
|
155
|
-
this.root.update(toOwnedView(view, selected, this.#notice, viewport.columns, viewport.rows));
|
|
156
|
-
if (this.root.editor.getText() !== selected.editorText)
|
|
157
|
-
this.root.editor.setText(selected.editorText);
|
|
158
|
-
this.component.invalidate();
|
|
159
|
-
this.runtime?.requestRender();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
export class StructuredWorkspaceRootComponent {
|
|
163
|
-
workspace;
|
|
164
|
-
sessionRoot;
|
|
165
|
-
onSwitch;
|
|
166
|
-
onEditorChange;
|
|
167
|
-
notice;
|
|
168
|
-
constructor(workspace, sessionRoot, onSwitch, onEditorChange, notice) {
|
|
169
|
-
this.workspace = workspace;
|
|
170
|
-
this.sessionRoot = sessionRoot;
|
|
171
|
-
this.onSwitch = onSwitch;
|
|
172
|
-
this.onEditorChange = onEditorChange;
|
|
173
|
-
this.notice = notice;
|
|
174
|
-
}
|
|
175
|
-
render(width) {
|
|
176
|
-
const view = this.workspace.view();
|
|
177
|
-
const tabLine = truncate(view.tabs.map(tab => `${tab.selected ? "[" : " "}${tab.label}${tab.selected ? "]" : " "}`).join(" "), width);
|
|
178
|
-
const notice = this.notice();
|
|
179
|
-
return [tabLine, ...(notice ? [truncate(notice, width)] : []), ...this.sessionRoot.render(width)];
|
|
180
|
-
}
|
|
181
|
-
handleInput(data) {
|
|
182
|
-
if (data === "\x1b[6;5~" || data === "\x1b[1;3C") {
|
|
183
|
-
this.onEditorChange(this.sessionRoot.editor.getText());
|
|
184
|
-
this.onSwitch(1);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (data === "\x1b[5;5~" || data === "\x1b[1;3D") {
|
|
188
|
-
this.onEditorChange(this.sessionRoot.editor.getText());
|
|
189
|
-
this.onSwitch(-1);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
this.sessionRoot.editor.handleInput?.(data);
|
|
193
|
-
}
|
|
194
|
-
invalidate() { this.sessionRoot.invalidate(); }
|
|
195
|
-
setFocused(focused) { this.sessionRoot.editor.setFocused?.(focused); }
|
|
196
|
-
dispose() { this.sessionRoot.dispose?.(); }
|
|
197
|
-
}
|
|
198
|
-
function selectedPanel(view) {
|
|
199
|
-
return view.selectedPanel ?? view.panels[0] ?? null;
|
|
200
|
-
}
|
|
201
|
-
function toOwnedView(workspace, panel, notice, columns, rows) {
|
|
202
|
-
const agent = workspace.workspace.agents.find(candidate => candidate.id === panel.agentId);
|
|
203
|
-
return {
|
|
204
|
-
contractVersion: OWNED_UI_CONTRACT_VERSION,
|
|
205
|
-
sessionId: panel.sessionId,
|
|
206
|
-
revision: workspace.workspace.revision + panel.lastSequence,
|
|
207
|
-
lifecycle: ownedLifecycle(panel.lifecycle),
|
|
208
|
-
transcript: panel.transcript.flatMap(messageBlocks),
|
|
209
|
-
editor: {
|
|
210
|
-
text: panel.editorText,
|
|
211
|
-
queuedSubmissions: [],
|
|
212
|
-
selection: null,
|
|
213
|
-
cursorOffset: panel.editorText.length,
|
|
214
|
-
historyRevision: panel.lastSequence,
|
|
215
|
-
submitEnabled: panel.lifecycle !== "stopping" && panel.lifecycle !== "stopped" && panel.lifecycle !== "failed",
|
|
216
|
-
},
|
|
217
|
-
status: {
|
|
218
|
-
title: agent?.displayName ?? panel.agentId,
|
|
219
|
-
workingMessage: panel.lifecycle === "busy" ? "Working" : null,
|
|
220
|
-
diagnostics: panel.failure ? [panel.failure] : notice ? [notice] : [],
|
|
221
|
-
badges: [
|
|
222
|
-
`${workspace.panels.length} agents`,
|
|
223
|
-
...(agent?.unreadActivity ? [`${agent.unreadActivity} unread`] : []),
|
|
224
|
-
...(agent?.attention ? ["attention"] : []),
|
|
225
|
-
],
|
|
226
|
-
footer: { branch: null, sessionName: agent?.displayName ?? null, availableProviderCount: 0, extensionStatuses: [] },
|
|
227
|
-
},
|
|
228
|
-
terminal: { columns, rows, focusedRegion: "editor", hardwareCursor: true },
|
|
229
|
-
activeModel: null,
|
|
230
|
-
thinkingLevel: "medium",
|
|
231
|
-
activeCommandIds: panel.activeCommandIds,
|
|
232
|
-
dialog: null,
|
|
233
|
-
overlay: null,
|
|
234
|
-
customizations: [],
|
|
235
|
-
diagnostics: panel.failure ? [{ sequence: panel.lastSequence, code: "structured-agent", severity: "error", message: panel.failure, recoverable: true }] : [],
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
function messageBlocks(message) {
|
|
239
|
-
return message.content.map((content, index) => {
|
|
240
|
-
const base = { id: `${message.id}.${index}`, status: message.status === "streaming" ? "live" : "finalized", revision: index + 1, title: null };
|
|
241
|
-
if (content.kind === "thinking")
|
|
242
|
-
return { ...base, kind: "thinking", text: content.text, payload: {} };
|
|
243
|
-
if (content.kind === "tool-call")
|
|
244
|
-
return { ...base, kind: "tool-call", title: content.toolName, text: JSON.stringify(content.input), payload: content };
|
|
245
|
-
if (content.kind === "tool-result")
|
|
246
|
-
return { ...base, kind: "tool-result", text: JSON.stringify(content.output), payload: content };
|
|
247
|
-
if (content.kind === "image")
|
|
248
|
-
return { ...base, kind: "custom", title: content.mediaType, text: "[image]", payload: content };
|
|
249
|
-
if (content.kind === "unknown")
|
|
250
|
-
return { ...base, kind: "custom", title: content.sourceType, text: JSON.stringify(content.payload), payload: content };
|
|
251
|
-
return { ...base, kind: message.role === "user" ? "user" : message.role === "system" ? "system" : message.role === "tool" ? "tool-result" : "assistant", text: content.text, payload: {} };
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
function ownedLifecycle(lifecycle) {
|
|
255
|
-
return lifecycle;
|
|
256
|
-
}
|
|
257
|
-
function truncate(value, width) {
|
|
258
|
-
if (width <= 0)
|
|
259
|
-
return "";
|
|
260
|
-
return value.length <= width ? value : width === 1 ? "…" : `${value.slice(0, width - 1)}…`;
|
|
261
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { type AgentCommandOutcome, type AgentEnginePort, type AgentMessage, type AgentSessionLifecycle } from "../../foundation/agent-engine-contracts/index.js";
|
|
2
|
-
import { type WorkspacePresentationModel } from "./presentation.js";
|
|
3
|
-
import { type WorkspaceView } from "./reducer.js";
|
|
4
|
-
import { WorkspaceRouter } from "./router.js";
|
|
5
|
-
export interface StructuredWorkspaceLimits {
|
|
6
|
-
readonly maxAgents: number;
|
|
7
|
-
readonly maxMessagesPerAgent: number;
|
|
8
|
-
readonly maxMessageBytes: number;
|
|
9
|
-
readonly maxEditorBytes: number;
|
|
10
|
-
}
|
|
11
|
-
export interface StructuredWorkspaceTabsOptions {
|
|
12
|
-
readonly workspaceId?: string;
|
|
13
|
-
readonly cwd: string;
|
|
14
|
-
readonly createEngine: (agentId: string) => Promise<AgentEnginePort>;
|
|
15
|
-
readonly limits?: Partial<StructuredWorkspaceLimits>;
|
|
16
|
-
readonly now?: () => string;
|
|
17
|
-
}
|
|
18
|
-
export interface StructuredAgentTabView {
|
|
19
|
-
readonly role: "tabpanel";
|
|
20
|
-
readonly agentId: string;
|
|
21
|
-
readonly sessionId: string;
|
|
22
|
-
readonly selected: boolean;
|
|
23
|
-
readonly lifecycle: AgentSessionLifecycle;
|
|
24
|
-
readonly transcript: readonly AgentMessage[];
|
|
25
|
-
readonly toolMessages: readonly AgentMessage[];
|
|
26
|
-
readonly editorText: string;
|
|
27
|
-
readonly activeCommandIds: readonly string[];
|
|
28
|
-
readonly lastSequence: number;
|
|
29
|
-
readonly failure: string | null;
|
|
30
|
-
readonly accessibleDescription: string;
|
|
31
|
-
}
|
|
32
|
-
export interface StructuredWorkspaceTabSelector {
|
|
33
|
-
readonly role: "tab";
|
|
34
|
-
readonly agentId: string;
|
|
35
|
-
readonly label: string;
|
|
36
|
-
readonly selected: boolean;
|
|
37
|
-
readonly accessibleDescription: string;
|
|
38
|
-
}
|
|
39
|
-
export interface StructuredWorkspaceTabsView {
|
|
40
|
-
readonly role: "tablist";
|
|
41
|
-
readonly workspace: WorkspaceView;
|
|
42
|
-
readonly presentation: WorkspacePresentationModel;
|
|
43
|
-
readonly tabs: readonly StructuredWorkspaceTabSelector[];
|
|
44
|
-
readonly panels: readonly StructuredAgentTabView[];
|
|
45
|
-
readonly selectedPanel: StructuredAgentTabView | null;
|
|
46
|
-
}
|
|
47
|
-
export type StructuredWorkspaceTabsResult<T> = {
|
|
48
|
-
readonly kind: "applied";
|
|
49
|
-
readonly view: StructuredWorkspaceTabsView;
|
|
50
|
-
readonly value: T;
|
|
51
|
-
} | {
|
|
52
|
-
readonly kind: "rejected";
|
|
53
|
-
readonly code: string;
|
|
54
|
-
readonly diagnostic: string;
|
|
55
|
-
};
|
|
56
|
-
export declare class StructuredWorkspaceTabs {
|
|
57
|
-
#private;
|
|
58
|
-
readonly router: WorkspaceRouter;
|
|
59
|
-
constructor(options: StructuredWorkspaceTabsOptions);
|
|
60
|
-
subscribe(listener: (view: StructuredWorkspaceTabsView) => void): () => void;
|
|
61
|
-
createAgent(input: {
|
|
62
|
-
readonly id: string;
|
|
63
|
-
readonly displayName: string;
|
|
64
|
-
readonly sessionId?: string;
|
|
65
|
-
}): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
66
|
-
selectAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
67
|
-
setEditorText(agentId: string, text: string): StructuredWorkspaceTabsResult<StructuredAgentTabView>;
|
|
68
|
-
submitSelected(): Promise<StructuredWorkspaceTabsResult<{
|
|
69
|
-
readonly correlationId: string;
|
|
70
|
-
readonly outcome: AgentCommandOutcome;
|
|
71
|
-
}>>;
|
|
72
|
-
sendPrompt(agentId: string, text: string): Promise<StructuredWorkspaceTabsResult<{
|
|
73
|
-
readonly correlationId: string;
|
|
74
|
-
readonly outcome: AgentCommandOutcome;
|
|
75
|
-
}>>;
|
|
76
|
-
stopAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
77
|
-
restartAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
78
|
-
refreshAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
79
|
-
removeAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<string>>;
|
|
80
|
-
flush(): Promise<void>;
|
|
81
|
-
view(): StructuredWorkspaceTabsView;
|
|
82
|
-
dispose(): Promise<void>;
|
|
83
|
-
}
|
|
@@ -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
|
-
}
|