@timurproko/a1 0.1.1-dev.2 → 0.1.1-dev.3
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/README.md +13 -0
- package/bin/a1-ui.js +2 -2
- package/dist/src/composition/index.d.ts +18 -0
- package/dist/src/composition/index.js +36 -0
- package/dist/src/composition/structured-workspace-application.d.ts +38 -0
- package/dist/src/composition/structured-workspace-application.js +261 -0
- package/dist/src/features/workspace/index.d.ts +1 -0
- package/dist/src/features/workspace/index.js +1 -0
- package/dist/src/features/workspace/router.d.ts +5 -0
- package/dist/src/features/workspace/router.js +26 -0
- package/dist/src/features/workspace/structured-tabs.d.ts +83 -0
- package/dist/src/features/workspace/structured-tabs.js +453 -0
- package/dist/src/foundation/release/bootstrap.js +1 -1
- package/dist/src/foundation/release/update.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -44,6 +44,19 @@ npm start
|
|
|
44
44
|
|
|
45
45
|
A1 control state uses `%APPDATA%\\A1` and `%LOCALAPPDATA%\\A1` on Windows, and the `a1` directory under XDG config/data/runtime roots on Unix. Override it only with declared `A1_*` variables such as `A1_CONFIG_DIR`, `A1_DATA_DIR`, `A1_RUNTIME_DIR`, `A1_DATABASE_PATH`, and `A1_ENDPOINT`. Pi profile roots remain `~/.a1/agent`, `~/.pi/agent`, and `~/.a1/sandbox`. This is a no-migration identity hard cut; see [`docs/architecture/toolchain.md`](docs/architecture/toolchain.md#identity-hard-cut-and-cleanup) before removing obsolete control state.
|
|
46
46
|
|
|
47
|
+
### Branch lifecycle
|
|
48
|
+
|
|
49
|
+
Create every topic branch and every `milestone/<name>` work branch from `develop`; never implement directly on `develop` or `master`. A source branch is closed only after this sequence completes:
|
|
50
|
+
|
|
51
|
+
1. Validate the source branch with its required gates.
|
|
52
|
+
2. Merge it into `develop` and push the merged `develop` commit.
|
|
53
|
+
3. Switch to `develop`.
|
|
54
|
+
4. Preview the exact cleanup set with `npm run branches:prune -- --branch <source>`.
|
|
55
|
+
5. Safely delete the reviewed local source with `npm run branches:prune -- --apply --branch <source>`.
|
|
56
|
+
6. When the non-protected remote source exists, delete it in the same cleanup with `npm run branches:prune -- --apply --branch <source> --remote origin`.
|
|
57
|
+
|
|
58
|
+
The command defaults to a dry run against `develop`. It always retains `develop`, `master`, the checked-out branch, explicitly protected branches, and branches whose tips are not ancestors of the integration target. Apply mode uses only Git safe deletion (`git branch -d`), never force deletion. Use `--base <branch>` for another explicit integration target, `--protect <branch>` for additional protection, and `--json` for machine-readable review.
|
|
59
|
+
|
|
47
60
|
Run the non-desktop gates with:
|
|
48
61
|
|
|
49
62
|
```sh
|
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 }, { composeStructuredWorkspaceApplication }] = 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 composeStructuredWorkspaceApplication({ cwd: process.cwd() });
|
|
12
12
|
return await runOwnedUi({ application });
|
|
13
13
|
},
|
|
14
14
|
transparent: async profileId => {
|
|
@@ -1,6 +1,9 @@
|
|
|
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";
|
|
4
7
|
export interface ProcessCompositionOptions {
|
|
5
8
|
readonly cwd?: string;
|
|
6
9
|
readonly sessionId?: string;
|
|
@@ -14,6 +17,21 @@ export interface OwnedUiCompositionOptions {
|
|
|
14
17
|
readonly createPiAdapter?: () => Promise<PiEngineAdapter>;
|
|
15
18
|
}
|
|
16
19
|
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>;
|
|
17
35
|
export interface ProcessComposition {
|
|
18
36
|
readonly engine: AgentEnginePort;
|
|
19
37
|
createPresentation(root: PresentationComponentPort, terminal: PresentationTerminalPort): PresentationRuntimePort;
|
|
@@ -3,6 +3,9 @@ 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";
|
|
6
9
|
const CAPABILITIES = {
|
|
7
10
|
contractVersion: AGENT_ENGINE_CONTRACT_VERSION,
|
|
8
11
|
commands: ["prompt", "steer", "follow-up", "abort", "retry", "compact", "bash", "replace-session"],
|
|
@@ -25,6 +28,39 @@ export async function composeOwnedUiApplication(options = {}) {
|
|
|
25
28
|
dispose: () => shell.dispose(),
|
|
26
29
|
};
|
|
27
30
|
}
|
|
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
|
+
}
|
|
28
64
|
export async function composeProcess(options = {}) {
|
|
29
65
|
let engine = options.engine;
|
|
30
66
|
if (!engine) {
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
}
|
|
@@ -22,10 +22,15 @@ 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>>;
|
|
25
29
|
stopAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
26
30
|
removeAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
27
31
|
recordActivity(agentId: string, amount?: number): Promise<WorkspaceRouterResult<number>>;
|
|
28
32
|
sendStructuredCommand(agentId: string, command: string, payload: unknown): Promise<WorkspaceRouterResult<RoutedStructuredCommand>>;
|
|
29
33
|
cancelStructuredCommand(agentId: string, targetCorrelationId: string): Promise<WorkspaceRouterResult<StructuredCommandRecord>>;
|
|
34
|
+
settleStructuredCommand(agentId: string, correlationId: string, outcome: "completed" | "failed"): Promise<WorkspaceRouterResult<StructuredCommandRecord>>;
|
|
30
35
|
view(): WorkspaceView;
|
|
31
36
|
}
|
|
@@ -23,6 +23,18 @@ 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
|
+
}
|
|
26
38
|
stopAgent(agentId) {
|
|
27
39
|
return this.#enqueue(() => this.reducer.stopAgent(agentId));
|
|
28
40
|
}
|
|
@@ -92,6 +104,20 @@ export class WorkspaceRouter {
|
|
|
92
104
|
return applied(this.reducer.view(), result.record);
|
|
93
105
|
});
|
|
94
106
|
}
|
|
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
|
+
}
|
|
95
121
|
view() {
|
|
96
122
|
return this.reducer.view();
|
|
97
123
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,453 @@
|
|
|
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
|
+
}
|
|
@@ -43,7 +43,7 @@ export async function runBootstrap(options) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
const candidate = await materializeRelease(options.packageRoot, paths.dataDir, {
|
|
46
|
-
onProgress: progress => output.write(`${PRODUCT_TEXT.diagnostic(`
|
|
46
|
+
onProgress: progress => output.write(`${PRODUCT_TEXT.diagnostic(`installing ${progress.fileCount} files.`)}\n`),
|
|
47
47
|
});
|
|
48
48
|
await stateStore.recordCandidate(candidate);
|
|
49
49
|
state = await stateStore.read();
|
|
@@ -107,7 +107,7 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
107
107
|
},
|
|
108
108
|
async activateInstalled(packageRoot, targetVersion, phase) {
|
|
109
109
|
const candidate = await materializeRelease(packageRoot, paths.dataDir, {
|
|
110
|
-
onProgress: progress => output.stdout(`${PRODUCT_TEXT.diagnostic(`
|
|
110
|
+
onProgress: progress => output.stdout(`${PRODUCT_TEXT.diagnostic(`installing ${progress.fileCount} files.`)}\n`),
|
|
111
111
|
});
|
|
112
112
|
if (candidate.packageVersion !== targetVersion)
|
|
113
113
|
throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${targetVersion}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.3",
|
|
4
4
|
"description": "Standalone terminal workspace for supervised native and managed agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.13.0",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"check:architecture": "node scripts/check-architecture.mjs && node scripts/product-identifier-policy.mjs --check && node scripts/check-product-identity-boundaries.mjs && node scripts/check-package-identity.mjs && node scripts/check-pinned-pi-source-ledger.mjs && node scripts/check-terminal-host-provenance.mjs",
|
|
25
25
|
"check:customization-ready": "node scripts/check-owned-ui-customization-prerequisites.mjs",
|
|
26
26
|
"check:deprecated": "node scripts/check-deprecated-dependencies.mjs",
|
|
27
|
+
"branches:prune": "node scripts/prune-merged-branches.mjs",
|
|
27
28
|
"check": "npm run typecheck && npm run check:architecture && npm run check:customization-ready && npm run check:deprecated && npm test && npm run test:release",
|
|
28
29
|
"validate:agent": "npm run check",
|
|
29
30
|
"publish:next": "tsx scripts/publish-next.ts",
|