dsh-wsr-execution 0.2.1 → 0.2.2
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/lib/client.js +379 -105
- package/package.json +1 -1
- package/src/action-presentation/model.js +13 -7
- package/src/action-presentation/view.js +145 -18
- package/src/client/browser-entry.js +23 -4
- package/src/client/delivery/control-plane-port.js +11 -2
- package/src/client/delivery/session-delivery-view.js +148 -37
- package/src/client/delivery-inventory/model.js +17 -3
- package/src/host/delivery-control-plane.js +39 -12
- package/src/intake/binding-repository.js +118 -41
- package/src/intake/command.js +10 -0
- package/src/intake/plugin.js +131 -37
package/src/intake/plugin.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { lstat, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
import { IntakeSessionBindingRepository } from "./binding-repository.js";
|
|
6
|
-
import { parseWsrCommand } from "./command.js";
|
|
7
|
+
import { parseWsrCommand, promptDiagnostic } from "./command.js";
|
|
7
8
|
import {
|
|
8
9
|
createDshSessionControlPlaneReadModel,
|
|
9
10
|
registerDeliveryControlPlaneGateway,
|
|
@@ -61,6 +62,72 @@ function error(code) {
|
|
|
61
62
|
return Object.freeze({ kind: "ERROR", code, message: code });
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
function gitInitFailure(cause) {
|
|
66
|
+
return Object.assign(new Error("GIT_INIT_FAILED", { cause }), { code: "GIT_INIT_FAILED" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function defaultGitInit(workspace) {
|
|
70
|
+
await runGit(workspace, ["init", "--quiet"]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function runGit(workspace, arguments_) {
|
|
74
|
+
return new Promise((accept, reject) => {
|
|
75
|
+
execFile("git", arguments_, { cwd: workspace }, (cause, stdout) => cause === null ? accept(stdout) : reject(cause));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function hasGitHead(workspace) {
|
|
80
|
+
try {
|
|
81
|
+
await runGit(workspace, ["rev-parse", "--verify", "HEAD"]);
|
|
82
|
+
return true;
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
if (cause?.code === 128) return false;
|
|
85
|
+
throw cause;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function ensureGitHead(workspace) {
|
|
90
|
+
if (!await hasGitHead(workspace)) {
|
|
91
|
+
await runGit(workspace, ["add", "-A", "--", "."]);
|
|
92
|
+
await runGit(workspace, [
|
|
93
|
+
"-c", "user.name=WSR Workspace Initializer",
|
|
94
|
+
"-c", "user.email=wsr@localhost",
|
|
95
|
+
"-c", "commit.gpgSign=false",
|
|
96
|
+
"commit", "--quiet", "--allow-empty", "--no-verify", "-m", "Initialize WSR workspace",
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
await runGit(workspace, ["rev-parse", "--verify", "HEAD^{tree}"]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function hasGitMarker(workspace) {
|
|
103
|
+
try {
|
|
104
|
+
const marker = await lstat(path.join(workspace, ".git"));
|
|
105
|
+
return marker.isDirectory() || marker.isFile();
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
if (cause?.code === "ENOENT") return false;
|
|
108
|
+
throw cause;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Establish the Git boundary only for an already authorized exact workspace. */
|
|
113
|
+
export async function ensureGitWorktree(workspace, initialize = defaultGitInit) {
|
|
114
|
+
try {
|
|
115
|
+
if (typeof workspace !== "string" || !path.isAbsolute(workspace) || typeof initialize !== "function") {
|
|
116
|
+
throw new TypeError("workspace must be an absolute path");
|
|
117
|
+
}
|
|
118
|
+
const canonical = await realpath(workspace);
|
|
119
|
+
if (canonical !== workspace || !(await stat(canonical)).isDirectory()) throw new TypeError("workspace is not canonical");
|
|
120
|
+
const initialized = !await hasGitMarker(canonical);
|
|
121
|
+
if (initialized) await initialize(canonical);
|
|
122
|
+
if (!await hasGitMarker(canonical)) throw new TypeError("git marker was not created");
|
|
123
|
+
await ensureGitHead(canonical);
|
|
124
|
+
return Object.freeze({ path: canonical, initialized });
|
|
125
|
+
} catch (cause) {
|
|
126
|
+
if (cause?.code === "GIT_INIT_FAILED") throw cause;
|
|
127
|
+
throw gitInitFailure(cause);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
64
131
|
function textOf(content) {
|
|
65
132
|
if (!Array.isArray(content)) return "";
|
|
66
133
|
return content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
|
|
@@ -143,7 +210,7 @@ export function presentToDshSession(agent, presentation, createId = () => `cmd-w
|
|
|
143
210
|
const commandId = createId();
|
|
144
211
|
agent.session.append("command/run", {
|
|
145
212
|
commandId,
|
|
146
|
-
name: "wsr",
|
|
213
|
+
name: "wsr-presentation",
|
|
147
214
|
source: { kind: "plugin", plugin: "workflow-execution" },
|
|
148
215
|
});
|
|
149
216
|
agent.session.append("command/done", { commandId, kind: presentation?.kind === "error" ? "error" : "success", text });
|
|
@@ -201,16 +268,36 @@ export async function createPluginRuntime(config, options = {}) {
|
|
|
201
268
|
const factory = options.factory ?? new api.DefaultExecutionApplicationFactory();
|
|
202
269
|
const application = await factory.create(admitted.configFile, dependencies);
|
|
203
270
|
const control = options.control ?? api.getExecutionApplicationControl(application);
|
|
271
|
+
const ownerProjection = options.ownerProjection ?? api.getExecutionControlPlaneProjection(application);
|
|
204
272
|
const bindingInventory = () => typeof control.bindingInventory === "function" ? control.bindingInventory() : control.list();
|
|
273
|
+
const archiveTerminal = async (sessionKey, correlation, deliveryId) => {
|
|
274
|
+
const snapshot = await ownerProjection.snapshot();
|
|
275
|
+
const matches = snapshot.deliveries.filter((delivery) => delivery.lifecycle === "TERMINAL"
|
|
276
|
+
&& delivery.navigation?.sessionCorrelation === correlation
|
|
277
|
+
&& (deliveryId === undefined || delivery.deliveryId === deliveryId));
|
|
278
|
+
if (matches.length !== 1) {
|
|
279
|
+
throw Object.assign(new Error("INTAKE_BINDING_INVARIANT_VIOLATION"), { code: "INTAKE_BINDING_INVARIANT_VIOLATION" });
|
|
280
|
+
}
|
|
281
|
+
await bindings.archiveTerminal(sessionKey, matches[0]);
|
|
282
|
+
};
|
|
205
283
|
try {
|
|
206
284
|
await application.start();
|
|
207
285
|
const inventory = await bindingInventory();
|
|
208
286
|
await bindings.start(inventory);
|
|
287
|
+
const snapshot = await ownerProjection.snapshot();
|
|
209
288
|
for (const binding of await bindings.list()) {
|
|
210
289
|
const sameDelivery = inventory.filter((item) => item.deliveryId === binding.deliveryId || item.worktree === binding.worktree);
|
|
211
290
|
const matches = sameDelivery.filter((item) => item.deliveryId === binding.deliveryId && item.worktree === binding.worktree
|
|
212
291
|
&& item.deliveryBindingIdentity === binding.deliveryBindingIdentity);
|
|
213
292
|
if (matches.length === 0 && sameDelivery.length === 0) {
|
|
293
|
+
const terminal = snapshot.deliveries.filter((delivery) => delivery.lifecycle === "TERMINAL"
|
|
294
|
+
&& delivery.deliveryId === binding.deliveryId && delivery.worktree === binding.worktree
|
|
295
|
+
&& delivery.deliveryBindingIdentity === binding.deliveryBindingIdentity
|
|
296
|
+
&& delivery.navigation?.sessionCorrelation === binding.correlation);
|
|
297
|
+
if (terminal.length === 1) {
|
|
298
|
+
await bindings.archiveTerminal(binding.sessionKey, terminal[0]);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
214
301
|
await bindings.detach(binding.deliveryId);
|
|
215
302
|
continue;
|
|
216
303
|
}
|
|
@@ -258,6 +345,19 @@ export async function createPluginRuntime(config, options = {}) {
|
|
|
258
345
|
return promise;
|
|
259
346
|
}
|
|
260
347
|
|
|
348
|
+
async function awaitRegistrationOrResult(execution, correlation) {
|
|
349
|
+
const result = execution.then((value) => Object.freeze({ kind: "result", result: value }));
|
|
350
|
+
while (true) {
|
|
351
|
+
const first = await Promise.race([
|
|
352
|
+
result,
|
|
353
|
+
control.waitForDelivery(correlation, options.deliveryRegistrationTimeoutMs ?? 10_000)
|
|
354
|
+
.then((delivery) => Object.freeze({ kind: "delivery", delivery })),
|
|
355
|
+
]);
|
|
356
|
+
if (first.kind === "result" || first.delivery !== undefined) return first;
|
|
357
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
261
361
|
async function invokeForSession(input) {
|
|
262
362
|
if (!accepting) return error("APPLICATION_CLOSING");
|
|
263
363
|
const candidateBinding = await bindings.bySession(input.sessionKey);
|
|
@@ -288,24 +388,19 @@ export async function createPluginRuntime(config, options = {}) {
|
|
|
288
388
|
if (candidateBinding !== undefined) return error("SESSION_INTAKE_BOUND");
|
|
289
389
|
const authorization = await conversationAuthorization();
|
|
290
390
|
if (authorization === undefined) return error("DSH_INTAKE_WORKSPACE_UNAUTHORIZED");
|
|
391
|
+
try { await (options.ensureGitWorktree ?? ensureGitWorktree)(authorization.path); }
|
|
392
|
+
catch { return error("GIT_INIT_FAILED"); }
|
|
291
393
|
sessionByCorrelation.set(correlation, input.sessionKey);
|
|
292
394
|
const execution = track(service.invoke(Object.freeze({ operation: "create", selector: operation.selector, worktree: authorization.path, directive: operation.directive, turn, correlation }), authorization));
|
|
293
|
-
const first = await
|
|
294
|
-
execution.then((result) => Object.freeze({ kind: "result", result })),
|
|
295
|
-
control.waitForDelivery(correlation, options.deliveryRegistrationTimeoutMs ?? 10_000)
|
|
296
|
-
.then((delivery) => Object.freeze({ kind: "delivery", delivery })),
|
|
297
|
-
]);
|
|
395
|
+
const first = await awaitRegistrationOrResult(execution, correlation);
|
|
298
396
|
if (first.kind === "result") {
|
|
397
|
+
if (first.result.kind === "TERMINAL") await archiveTerminal(input.sessionKey, correlation, first.result.deliveryId);
|
|
299
398
|
if (first.result.kind === "ERROR" || first.result.kind === "TERMINAL" || first.result.kind === "RECOVERY") sessionByCorrelation.delete(correlation);
|
|
300
399
|
return first.result;
|
|
301
400
|
}
|
|
302
401
|
const delivery = first.delivery;
|
|
303
|
-
if (delivery === undefined) {
|
|
304
|
-
const result = await execution;
|
|
305
|
-
if (result.kind === "ERROR" || result.kind === "TERMINAL") sessionByCorrelation.delete(correlation);
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
402
|
await bindings.claim(Object.freeze({ sessionKey: input.sessionKey, correlation, deliveryId: delivery.deliveryId, worktree: delivery.worktree, deliveryBindingIdentity: delivery.deliveryBindingIdentity }));
|
|
403
|
+
control.attach(delivery.deliveryId, correlation);
|
|
309
404
|
void track(execution.then(async (result) => {
|
|
310
405
|
try { await options.present?.(Object.freeze({
|
|
311
406
|
sessionKey: input.sessionKey,
|
|
@@ -313,7 +408,8 @@ export async function createPluginRuntime(config, options = {}) {
|
|
|
313
408
|
})); }
|
|
314
409
|
catch { /* presentation is not Delivery control */ }
|
|
315
410
|
if (result.kind === "TERMINAL" || result.kind === "ERROR") {
|
|
316
|
-
await
|
|
411
|
+
if (result.kind === "TERMINAL") await archiveTerminal(input.sessionKey, correlation, delivery.deliveryId);
|
|
412
|
+
else await bindings.detach(delivery.deliveryId);
|
|
317
413
|
sessionByCorrelation.delete(correlation);
|
|
318
414
|
}
|
|
319
415
|
})).catch(() => undefined);
|
|
@@ -339,6 +435,7 @@ export async function createPluginRuntime(config, options = {}) {
|
|
|
339
435
|
const recovered = (await bindingInventory()).filter((item) => item.deliveryId === result.deliveryId && item.worktree === result.worktree);
|
|
340
436
|
if (recovered.length !== 1) return error("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
341
437
|
await bindings.claim(Object.freeze({ sessionKey: input.sessionKey, correlation, deliveryId: result.deliveryId, worktree: result.worktree, deliveryBindingIdentity: recovered[0].deliveryBindingIdentity }));
|
|
438
|
+
control.attach(result.deliveryId, correlation);
|
|
342
439
|
sessionByCorrelation.set(correlation, input.sessionKey);
|
|
343
440
|
}
|
|
344
441
|
return result;
|
|
@@ -357,7 +454,7 @@ export async function createPluginRuntime(config, options = {}) {
|
|
|
357
454
|
const result = await service.invoke(Object.freeze({ operation: "abandon", deliveryId: operation.deliveryId, correlation }));
|
|
358
455
|
if (result.kind === "TERMINAL") {
|
|
359
456
|
const detached = await bindings.byDelivery(operation.deliveryId);
|
|
360
|
-
await
|
|
457
|
+
if (detached !== undefined) await archiveTerminal(detached.sessionKey, detached.correlation, operation.deliveryId);
|
|
361
458
|
if (detached !== undefined) sessionByCorrelation.delete(detached.correlation);
|
|
362
459
|
}
|
|
363
460
|
return result;
|
|
@@ -385,7 +482,7 @@ export async function createPluginRuntime(config, options = {}) {
|
|
|
385
482
|
return closePromise;
|
|
386
483
|
}
|
|
387
484
|
|
|
388
|
-
return Object.freeze({ application, service, control, bindings, invokeForSession, answerForSession, close });
|
|
485
|
+
return Object.freeze({ application, service, control, ownerProjection, bindings, invokeForSession, answerForSession, close });
|
|
389
486
|
}
|
|
390
487
|
|
|
391
488
|
function commandTurn(rawInput) {
|
|
@@ -394,7 +491,7 @@ function commandTurn(rawInput) {
|
|
|
394
491
|
}
|
|
395
492
|
|
|
396
493
|
export async function recordWsrCommandInput(agent, rawInput, attachments = [], createId = () => `message-workflow-execution-${randomUUID()}`) {
|
|
397
|
-
if (agent === null || typeof agent !== "object" || typeof agent.
|
|
494
|
+
if (agent === null || typeof agent !== "object" || typeof agent.session?.append !== "function"
|
|
398
495
|
|| typeof rawInput !== "string" || !Array.isArray(attachments) || typeof createId !== "function") {
|
|
399
496
|
throw new TypeError("DSH_INTAKE_USER_INPUT_INVALID");
|
|
400
497
|
}
|
|
@@ -407,8 +504,7 @@ export async function recordWsrCommandInput(agent, rawInput, attachments = [], c
|
|
|
407
504
|
...attachments,
|
|
408
505
|
]),
|
|
409
506
|
});
|
|
410
|
-
agent.
|
|
411
|
-
await agent.whenIdle();
|
|
507
|
+
agent.session.append("user/message", message, { surfaceOp: "append" });
|
|
412
508
|
return message;
|
|
413
509
|
}
|
|
414
510
|
|
|
@@ -430,11 +526,9 @@ export async function apply(ctx, config) {
|
|
|
430
526
|
const runtime = await createPluginRuntime(config, { present: (value) => presentationRouter.present(value),
|
|
431
527
|
sessionAvailable: (sessionKey) => ctx.agents.get(sessionKey) !== undefined,
|
|
432
528
|
resolveConversationWorkspace: async (agent) => resolveConversationWorkspace(ctx, agent) });
|
|
433
|
-
const executionApi = await import("wsr-execution");
|
|
434
|
-
const ownerProjection = executionApi.getExecutionControlPlaneProjection(runtime.application);
|
|
435
529
|
await registerDeliveryControlPlaneGateway(
|
|
436
530
|
ctx,
|
|
437
|
-
createDshSessionControlPlaneReadModel(ownerProjection, runtime.bindings),
|
|
531
|
+
createDshSessionControlPlaneReadModel(runtime.ownerProjection, runtime.bindings),
|
|
438
532
|
);
|
|
439
533
|
const active = new Set();
|
|
440
534
|
const attachmentStore = ctx.attachments;
|
|
@@ -446,39 +540,39 @@ export async function apply(ctx, config) {
|
|
|
446
540
|
recordInput: true,
|
|
447
541
|
async handler(invocation) {
|
|
448
542
|
return run((async () => {
|
|
449
|
-
let query = false;
|
|
450
543
|
try {
|
|
544
|
+
await recordWsrCommandInput(invocation.agent, invocation.rawInput, invocation.attachments);
|
|
451
545
|
const operation = parseWsrCommand(invocation.rawInput);
|
|
452
|
-
if (["create", "recover"].includes(operation.operation)) {
|
|
453
|
-
presentationRouter.retain(String(invocation.agent.id), invocation.agent);
|
|
454
|
-
}
|
|
455
|
-
query = operation.operation === "list" || operation.operation === "status";
|
|
456
546
|
const { createIntakePresentation, presentationForIntakeResult, serializeIntakePresentation } = await import("wsr-execution");
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
547
|
+
const complete = (presentation, kind) => {
|
|
548
|
+
presentToDshSession(invocation.agent, presentation);
|
|
549
|
+
return { kind, text: serializeIntakePresentation(presentation, 4096) };
|
|
550
|
+
};
|
|
551
|
+
const diagnostic = promptDiagnostic(operation, invocation.attachments);
|
|
552
|
+
if (diagnostic !== undefined) {
|
|
553
|
+
const presentation = createIntakePresentation(`presentation-${randomUUID()}`, "error", diagnostic);
|
|
554
|
+
return complete(presentation, "error");
|
|
462
555
|
}
|
|
463
556
|
if (invocation.attachments.length > 0 && !["create", "action-finish"].includes(operation.operation)) {
|
|
464
557
|
const presentation = createIntakePresentation(
|
|
465
558
|
`presentation-${randomUUID()}`, "error", { code: "WSR_COMMAND_INVALID", message: "WSR_COMMAND_INVALID" },
|
|
466
559
|
);
|
|
467
|
-
|
|
468
|
-
|
|
560
|
+
return complete(presentation, "error");
|
|
561
|
+
}
|
|
562
|
+
if (["create", "recover"].includes(operation.operation)) {
|
|
563
|
+
presentationRouter.retain(String(invocation.agent.id), invocation.agent);
|
|
469
564
|
}
|
|
470
565
|
const result = await runtime.invokeForSession({ sessionKey: String(invocation.agent.id), agent: invocation.agent, operation, turnText: commandTurn(invocation.rawInput), images: invocation.attachments, attachmentStore, signal: invocation.signal });
|
|
471
566
|
if (["create", "recover"].includes(operation.operation) && !["START_UNCERTAIN", "RECOVERY"].includes(result.kind)) {
|
|
472
567
|
presentationRouter.release(String(invocation.agent.id));
|
|
473
568
|
}
|
|
474
569
|
const presentation = presentationForDshOperation({ createIntakePresentation, presentationForIntakeResult }, `presentation-${randomUUID()}`, operation, result, 4096);
|
|
475
|
-
|
|
476
|
-
return { kind: result.kind === "ERROR" ? "error" : "success", text: serializeIntakePresentation(presentation, 4096) };
|
|
570
|
+
return complete(presentation, result.kind === "ERROR" ? "error" : "success");
|
|
477
571
|
} catch (cause) {
|
|
478
572
|
const { createIntakePresentation, serializeIntakePresentation } = await import("wsr-execution");
|
|
479
573
|
const code = typeof cause?.code === "string" ? cause.code : "DSH_INTAKE_FAILED";
|
|
480
574
|
const presentation = createIntakePresentation(`presentation-${randomUUID()}`, "error", { code, message: code });
|
|
481
|
-
|
|
575
|
+
presentToDshSession(invocation.agent, presentation);
|
|
482
576
|
return { kind: "error", text: serializeIntakePresentation(presentation, 4096) };
|
|
483
577
|
}
|
|
484
578
|
})());
|