dsh-wsr-execution 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,538 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { IntakeSessionBindingRepository } from "./binding-repository.js";
6
+ import { parseWsrCommand } from "./command.js";
7
+ import {
8
+ createDshSessionControlPlaneReadModel,
9
+ registerDeliveryControlPlaneGateway,
10
+ } from "../host/delivery-control-plane.js";
11
+
12
+ export const name = "workflow-execution";
13
+ export const inject = ["commands", "tools", "attachments", "agents", "workspaceRegistry", "connection"];
14
+
15
+ function profile(candidate) {
16
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)
17
+ || Object.keys(candidate).sort().join(",") !== "bindingFile,configFile"
18
+ || typeof candidate.configFile !== "string" || !path.isAbsolute(candidate.configFile)
19
+ || typeof candidate.bindingFile !== "string" || !path.isAbsolute(candidate.bindingFile)) {
20
+ throw new TypeError("DSH_INTAKE_CONFIG_INVALID");
21
+ }
22
+ return Object.freeze({ configFile: path.resolve(candidate.configFile), bindingFile: path.resolve(candidate.bindingFile) });
23
+ }
24
+
25
+ function defaultDependencies(attachmentBytes, present) {
26
+ return Object.freeze({
27
+ clock: Object.freeze({ now: () => Date.now() }),
28
+ ids: Object.freeze({ create: () => `delivery-${randomUUID()}` }),
29
+ filesystem: Object.freeze({
30
+ read: async (file, maxBytes) => {
31
+ const bytes = Uint8Array.from(await readFile(file));
32
+ if (maxBytes !== undefined && bytes.byteLength > maxBytes) throw new TypeError("FILESYSTEM_READ_BOUND_EXCEEDED");
33
+ return bytes;
34
+ },
35
+ writeImmutable: async (file, bytes) => writeFile(file, bytes, { flag: "wx", mode: 0o600 }),
36
+ list: async (directory) => Object.freeze(await readdir(directory)),
37
+ inspect: async (file) => {
38
+ try {
39
+ const value = await stat(file);
40
+ return Object.freeze({ kind: value.isFile() ? "file" : value.isDirectory() ? "directory" : "missing" });
41
+ } catch (cause) {
42
+ if (cause?.code === "ENOENT") return Object.freeze({ kind: "missing" });
43
+ throw cause;
44
+ }
45
+ },
46
+ }),
47
+ network: Object.freeze({ request: async (url) => {
48
+ const response = await fetch(url);
49
+ return Object.freeze({ status: response.status, body: new Uint8Array(await response.arrayBuffer()) });
50
+ } }),
51
+ intake: Object.freeze({ publish: present }),
52
+ attachments: Object.freeze({ read: async (contentRef) => {
53
+ const bytes = attachmentBytes.get(contentRef);
54
+ if (bytes === undefined) throw new TypeError("ATTACHMENT_REFERENCE_UNAVAILABLE");
55
+ return Uint8Array.from(bytes);
56
+ } }),
57
+ });
58
+ }
59
+
60
+ function error(code) {
61
+ return Object.freeze({ kind: "ERROR", code, message: code });
62
+ }
63
+
64
+ function textOf(content) {
65
+ if (!Array.isArray(content)) return "";
66
+ return content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
67
+ }
68
+
69
+ function turnFromAgent(agent) {
70
+ const events = Array.isArray(agent?.session?.events) ? agent.session.events : [];
71
+ const event = [...events].reverse().find((candidate) => candidate?.type === "user/message" && candidate?.data?.message?.source?.kind === "user");
72
+ const message = event?.data?.message;
73
+ return Object.freeze({ text: textOf(message?.content), images: Object.freeze((message?.content ?? []).filter((block) => block?.type === "image")) });
74
+ }
75
+
76
+ function workspaceUnauthorized() {
77
+ return Object.assign(new TypeError("DSH_INTAKE_WORKSPACE_UNAUTHORIZED"), { code: "DSH_INTAKE_WORKSPACE_UNAUTHORIZED" });
78
+ }
79
+
80
+ export async function resolveConversationWorkspace(ctx, agent) {
81
+ const sessionKey = agent !== null && typeof agent === "object" && typeof agent.id === "string" && agent.id.length > 0
82
+ ? agent.id
83
+ : undefined;
84
+ if (ctx === null || typeof ctx !== "object" || typeof ctx.agents?.get !== "function"
85
+ || typeof ctx.workspaceRegistry?.resolveByPath !== "function" || typeof sessionKey !== "string" || sessionKey.length === 0) {
86
+ throw workspaceUnauthorized();
87
+ }
88
+ const liveAgent = ctx.agents.get(sessionKey);
89
+ const cwd = agent?.session?.header?.cwd;
90
+ if (liveAgent !== agent || typeof cwd !== "string" || !path.isAbsolute(cwd)) {
91
+ throw workspaceUnauthorized();
92
+ }
93
+ let workspace;
94
+ try { workspace = await ctx.workspaceRegistry.resolveByPath(cwd); }
95
+ catch { throw workspaceUnauthorized(); }
96
+ if (workspace === undefined || typeof workspace.id !== "string" || typeof workspace.path !== "string"
97
+ || !path.isAbsolute(workspace.path) || !Array.isArray(workspace.sessionIds)
98
+ || !workspace.sessionIds.some((candidate) => String(candidate) === sessionKey)) {
99
+ throw workspaceUnauthorized();
100
+ }
101
+ let canonicalCwd;
102
+ let canonicalWorkspace;
103
+ try { [canonicalCwd, canonicalWorkspace] = await Promise.all([realpath(cwd), realpath(workspace.path)]); }
104
+ catch { throw workspaceUnauthorized(); }
105
+ if (canonicalCwd !== canonicalWorkspace || workspace.path !== canonicalWorkspace) throw workspaceUnauthorized();
106
+ return Object.freeze({ sessionKey, workspaceId: String(workspace.id), path: canonicalWorkspace });
107
+ }
108
+
109
+ export function recordConsumedActionReply(agent, message) {
110
+ if (agent === null || typeof agent !== "object" || typeof agent.session?.append !== "function"
111
+ || message === null || typeof message !== "object" || message.source?.kind !== "user") {
112
+ throw new TypeError("DSH_INTAKE_USER_INPUT_INVALID");
113
+ }
114
+ agent.session.append("user/message", message, { surfaceOp: "append" });
115
+ }
116
+
117
+ const PRESENTATION_VERSION = "wsr.presentation@1.0.0";
118
+ const PRESENTATION_KINDS = new Set(["command-accepted", "delivery-running", "delivery-list", "delivery-status", "action-output", "action-input-request", "terminal-result", "error"]);
119
+
120
+ function boundedPresentation(presentation) {
121
+ const valid = presentation !== null && typeof presentation === "object" && !Array.isArray(presentation)
122
+ && presentation.schemaVersion === PRESENTATION_VERSION
123
+ && typeof presentation.correlation === "string" && presentation.correlation.length > 0
124
+ && PRESENTATION_KINDS.has(presentation.kind)
125
+ && presentation.data !== null && typeof presentation.data === "object" && !Array.isArray(presentation.data);
126
+ const safe = valid ? presentation : Object.freeze({
127
+ schemaVersion: PRESENTATION_VERSION,
128
+ correlation: "presentation-invalid",
129
+ kind: "error",
130
+ data: Object.freeze({ code: "WSR_PRESENTATION_INVALID", message: "WSR_PRESENTATION_INVALID" }),
131
+ });
132
+ const text = JSON.stringify(safe);
133
+ if (Buffer.byteLength(text, "utf8") <= 4096) return text;
134
+ return JSON.stringify({ schemaVersion: PRESENTATION_VERSION, correlation: safe.correlation, kind: "error", data: { code: "OUTPUT_TRUNCATED", message: "OUTPUT_TRUNCATED" } });
135
+ }
136
+
137
+ export function presentToDshSession(agent, presentation, createId = () => `cmd-workflow-execution-${randomUUID()}`) {
138
+ if (agent === null || typeof agent !== "object" || typeof agent.session?.append !== "function"
139
+ || typeof createId !== "function") {
140
+ throw new TypeError("DSH_INTAKE_PRESENTATION_INVALID");
141
+ }
142
+ const text = boundedPresentation(presentation);
143
+ const commandId = createId();
144
+ agent.session.append("command/run", {
145
+ commandId,
146
+ name: "wsr",
147
+ source: { kind: "plugin", plugin: "workflow-execution" },
148
+ });
149
+ agent.session.append("command/done", { commandId, kind: presentation?.kind === "error" ? "error" : "success", text });
150
+ }
151
+
152
+ export function createSessionPresentationRouter(agents) {
153
+ if (agents === null || typeof agents !== "object" || typeof agents.get !== "function") {
154
+ throw new TypeError("DSH_INTAKE_SESSION_UNAVAILABLE");
155
+ }
156
+ const retained = new Map();
157
+ return Object.freeze({
158
+ retain(sessionKey, agent) {
159
+ if (typeof sessionKey !== "string" || sessionKey.length === 0 || agent === null || typeof agent !== "object"
160
+ || String(agent.id) !== sessionKey || typeof agent.session?.append !== "function") {
161
+ throw new TypeError("DSH_INTAKE_SESSION_UNAVAILABLE");
162
+ }
163
+ retained.set(sessionKey, agent);
164
+ },
165
+ release(sessionKey) { retained.delete(sessionKey); },
166
+ present({ sessionKey, presentation }) {
167
+ const agent = agents.get(sessionKey) ?? retained.get(sessionKey);
168
+ if (agent === undefined) throw new TypeError("DSH_INTAKE_SESSION_UNAVAILABLE");
169
+ try { presentToDshSession(agent, presentation); }
170
+ finally {
171
+ if (["terminal-result", "error"].includes(presentation?.kind)) retained.delete(sessionKey);
172
+ }
173
+ },
174
+ });
175
+ }
176
+
177
+ export function presentationForDshOperation(api, correlation, operation, result, maxBytes = 4096) {
178
+ if (operation?.operation === "create" && result?.kind === "RECOVERY") {
179
+ return api.createIntakePresentation(correlation, "delivery-status", {
180
+ worktree: result.worktree,
181
+ deliveryId: result.deliveryId,
182
+ state: result.state,
183
+ created: false,
184
+ reason: "CURRENT_DELIVERY_EXISTS",
185
+ });
186
+ }
187
+ return api.presentationForIntakeResult(correlation, result, maxBytes);
188
+ }
189
+
190
+ export async function createPluginRuntime(config, options = {}) {
191
+ const admitted = profile(config);
192
+ const api = await (options.moduleLoader?.() ?? import("wsr-execution"));
193
+ const attachmentBytes = new Map();
194
+ const bindings = options.bindings ?? new IntakeSessionBindingRepository(admitted.bindingFile);
195
+ const sessionByCorrelation = new Map();
196
+ const present = async (presentation) => {
197
+ const sessionKey = sessionByCorrelation.get(presentation.correlation);
198
+ if (sessionKey !== undefined) await options.present?.(Object.freeze({ sessionKey, presentation }));
199
+ };
200
+ const dependencies = options.dependencies ?? defaultDependencies(attachmentBytes, present);
201
+ const factory = options.factory ?? new api.DefaultExecutionApplicationFactory();
202
+ const application = await factory.create(admitted.configFile, dependencies);
203
+ const control = options.control ?? api.getExecutionApplicationControl(application);
204
+ const bindingInventory = () => typeof control.bindingInventory === "function" ? control.bindingInventory() : control.list();
205
+ try {
206
+ await application.start();
207
+ const inventory = await bindingInventory();
208
+ await bindings.start(inventory);
209
+ for (const binding of await bindings.list()) {
210
+ const sameDelivery = inventory.filter((item) => item.deliveryId === binding.deliveryId || item.worktree === binding.worktree);
211
+ const matches = sameDelivery.filter((item) => item.deliveryId === binding.deliveryId && item.worktree === binding.worktree
212
+ && item.deliveryBindingIdentity === binding.deliveryBindingIdentity);
213
+ if (matches.length === 0 && sameDelivery.length === 0) {
214
+ await bindings.detach(binding.deliveryId);
215
+ continue;
216
+ }
217
+ if (matches.length !== 1 || sameDelivery.length !== 1) {
218
+ throw Object.assign(new Error("INTAKE_BINDING_INVARIANT_VIOLATION"), { code: "INTAKE_BINDING_INVARIANT_VIOLATION" });
219
+ }
220
+ if (options.sessionAvailable !== undefined && !await options.sessionAvailable(binding.sessionKey)) {
221
+ await bindings.markDetached(binding.deliveryId);
222
+ continue;
223
+ }
224
+ control.attach(binding.deliveryId, binding.correlation);
225
+ sessionByCorrelation.set(binding.correlation, binding.sessionKey);
226
+ }
227
+ } catch (cause) {
228
+ try { await application.close(); } catch { /* preserve the first startup failure */ }
229
+ throw cause;
230
+ }
231
+ const service = new api.WorkflowIntakeService(Object.freeze({
232
+ application,
233
+ control,
234
+ ...(typeof control.executeFromConversationWorkspace !== "function" ? {} : {
235
+ execute: (request, authorization) => control.executeFromConversationWorkspace(request, authorization),
236
+ }),
237
+ }));
238
+ const active = new Set();
239
+ let accepting = true;
240
+ let closePromise;
241
+
242
+ async function captureImages(images, attachmentStore, signal) {
243
+ const captured = [];
244
+ for (const block of images ?? []) {
245
+ const stored = await attachmentStore.readImage(block.attachment, signal);
246
+ const bytes = Uint8Array.from(stored.data);
247
+ const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
248
+ const contentRef = `intake-attachment-${randomUUID()}`;
249
+ attachmentBytes.set(contentRef, bytes);
250
+ captured.push(Object.freeze({ identity: `attachment-${randomUUID()}`, filename: stored.ref.name ?? "attachment", mediaType: stored.ref.mediaType, byteLength: bytes.byteLength, digest, contentRef }));
251
+ }
252
+ return Object.freeze(captured);
253
+ }
254
+
255
+ function track(promise) {
256
+ active.add(promise);
257
+ void promise.finally(() => active.delete(promise)).catch(() => undefined);
258
+ return promise;
259
+ }
260
+
261
+ async function invokeForSession(input) {
262
+ if (!accepting) return error("APPLICATION_CLOSING");
263
+ const candidateBinding = await bindings.bySession(input.sessionKey);
264
+ const existing = candidateBinding?.state === "BOUND" ? candidateBinding : undefined;
265
+ const correlation = existing?.correlation ?? `intake-${randomUUID()}`;
266
+ const attachments = await captureImages(input.images ?? [], input.attachmentStore, input.signal);
267
+ const turn = Object.freeze({ text: input.turnText ?? "", attachments });
268
+ const operation = input.operation;
269
+ async function conversationAuthorization() {
270
+ try {
271
+ if (options.resolveConversationWorkspace === undefined || input.agent === null || typeof input.agent !== "object"
272
+ || String(input.agent.id) !== input.sessionKey) return undefined;
273
+ const authority = await options.resolveConversationWorkspace(input.agent);
274
+ if (authority === null || typeof authority !== "object" || authority.sessionKey !== input.sessionKey
275
+ || typeof authority.workspaceId !== "string" || authority.workspaceId.length === 0
276
+ || typeof authority.path !== "string" || !path.isAbsolute(authority.path)) return undefined;
277
+ const canonical = await realpath(authority.path);
278
+ if (canonical !== authority.path) return undefined;
279
+ return Object.freeze({
280
+ schemaVersion: "execution.conversation-workspace-authorization@1.0.0",
281
+ sessionKey: input.sessionKey,
282
+ workspaceId: authority.workspaceId,
283
+ path: canonical,
284
+ });
285
+ } catch { return undefined; }
286
+ }
287
+ if (operation.operation === "create") {
288
+ if (candidateBinding !== undefined) return error("SESSION_INTAKE_BOUND");
289
+ const authorization = await conversationAuthorization();
290
+ if (authorization === undefined) return error("DSH_INTAKE_WORKSPACE_UNAUTHORIZED");
291
+ sessionByCorrelation.set(correlation, input.sessionKey);
292
+ 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 Promise.race([
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
+ ]);
298
+ if (first.kind === "result") {
299
+ if (first.result.kind === "ERROR" || first.result.kind === "TERMINAL" || first.result.kind === "RECOVERY") sessionByCorrelation.delete(correlation);
300
+ return first.result;
301
+ }
302
+ 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
+ await bindings.claim(Object.freeze({ sessionKey: input.sessionKey, correlation, deliveryId: delivery.deliveryId, worktree: delivery.worktree, deliveryBindingIdentity: delivery.deliveryBindingIdentity }));
309
+ void track(execution.then(async (result) => {
310
+ try { await options.present?.(Object.freeze({
311
+ sessionKey: input.sessionKey,
312
+ presentation: api.presentationForIntakeResult(correlation, result, 4096),
313
+ })); }
314
+ catch { /* presentation is not Delivery control */ }
315
+ if (result.kind === "TERMINAL" || result.kind === "ERROR") {
316
+ await bindings.detach(delivery.deliveryId);
317
+ sessionByCorrelation.delete(correlation);
318
+ }
319
+ })).catch(() => undefined);
320
+ return Object.freeze({ kind: "START_UNCERTAIN", worktree: delivery.worktree, deliveryId: delivery.deliveryId });
321
+ }
322
+ if (operation.operation === "list") return service.invoke(Object.freeze({ operation: "list", correlation }));
323
+ if (operation.operation === "recover") {
324
+ if (existing !== undefined) return error("SESSION_INTAKE_BOUND");
325
+ const authorization = await conversationAuthorization();
326
+ if (authorization === undefined) return error("DSH_INTAKE_WORKSPACE_UNAUTHORIZED");
327
+ const recoverable = (await bindingInventory()).filter((item) => operation.deliveryId === undefined
328
+ ? item.worktree === authorization.path
329
+ : item.deliveryId === operation.deliveryId);
330
+ if (operation.deliveryId !== undefined && recoverable.length === 1 && recoverable[0].worktree !== authorization.path) {
331
+ return error("DSH_INTAKE_WORKSPACE_UNAUTHORIZED");
332
+ }
333
+ if (recoverable.length === 1) {
334
+ const claimed = await bindings.byDelivery(recoverable[0].deliveryId);
335
+ if (claimed?.state === "BOUND") return error("DELIVERY_INTAKE_BOUND");
336
+ }
337
+ const result = await service.invoke(Object.freeze({ operation: "recover", worktree: authorization.path, ...(operation.deliveryId === undefined ? {} : { deliveryId: operation.deliveryId }), correlation }));
338
+ if (result.kind === "RECOVERY") {
339
+ const recovered = (await bindingInventory()).filter((item) => item.deliveryId === result.deliveryId && item.worktree === result.worktree);
340
+ if (recovered.length !== 1) return error("INTAKE_BINDING_INVARIANT_VIOLATION");
341
+ await bindings.claim(Object.freeze({ sessionKey: input.sessionKey, correlation, deliveryId: result.deliveryId, worktree: result.worktree, deliveryBindingIdentity: recovered[0].deliveryBindingIdentity }));
342
+ sessionByCorrelation.set(correlation, input.sessionKey);
343
+ }
344
+ return result;
345
+ }
346
+ if (operation.operation === "status") {
347
+ const deliveryId = operation.deliveryId ?? existing?.deliveryId;
348
+ const authorization = deliveryId === undefined ? await conversationAuthorization() : undefined;
349
+ const worktree = authorization?.path;
350
+ if (deliveryId === undefined && worktree === undefined) return error("DSH_INTAKE_WORKSPACE_UNAUTHORIZED");
351
+ return service.invoke(Object.freeze({ operation: "status", ...(worktree === undefined ? {} : { worktree }), ...(deliveryId === undefined ? {} : { deliveryId }), correlation }));
352
+ }
353
+ if (operation.operation === "action-finish") {
354
+ if (existing === undefined) return error("DELIVERY_UNKNOWN");
355
+ return service.invoke(Object.freeze({ operation: "action-finish", ...(operation.remainder === undefined && attachments.length === 0 ? {} : { turn: Object.freeze({ text: operation.remainder ?? "", attachments }) }), correlation: existing.correlation }));
356
+ }
357
+ const result = await service.invoke(Object.freeze({ operation: "abandon", deliveryId: operation.deliveryId, correlation }));
358
+ if (result.kind === "TERMINAL") {
359
+ const detached = await bindings.byDelivery(operation.deliveryId);
360
+ await bindings.detach(operation.deliveryId);
361
+ if (detached !== undefined) sessionByCorrelation.delete(detached.correlation);
362
+ }
363
+ return result;
364
+ }
365
+
366
+ async function answerForSession(input) {
367
+ if (!accepting) return error("APPLICATION_CLOSING");
368
+ const binding = await bindings.bySession(input.sessionKey);
369
+ if (binding === undefined) return error("DELIVERY_UNKNOWN");
370
+ const attachments = await captureImages(input.images ?? [], input.attachmentStore, input.signal);
371
+ return control.answerAction(Object.freeze({ correlation: binding.correlation, prompt: Object.freeze({ text: input.text, attachments }) }));
372
+ }
373
+
374
+ function close() {
375
+ if (closePromise !== undefined) return closePromise;
376
+ closePromise = (async () => {
377
+ accepting = false;
378
+ await Promise.race([
379
+ Promise.allSettled([...active]),
380
+ new Promise((resolve) => setTimeout(resolve, options.quiesceTimeoutMs ?? 10_000)),
381
+ ]);
382
+ await application.close();
383
+ attachmentBytes.clear();
384
+ })();
385
+ return closePromise;
386
+ }
387
+
388
+ return Object.freeze({ application, service, control, bindings, invokeForSession, answerForSession, close });
389
+ }
390
+
391
+ function commandTurn(rawInput) {
392
+ const normalized = rawInput.startsWith(" ") ? rawInput.slice(1) : rawInput;
393
+ return `/wsr ${normalized}`;
394
+ }
395
+
396
+ export async function recordWsrCommandInput(agent, rawInput, attachments = [], createId = () => `message-workflow-execution-${randomUUID()}`) {
397
+ if (agent === null || typeof agent !== "object" || typeof agent.followup !== "function" || typeof agent.whenIdle !== "function"
398
+ || typeof rawInput !== "string" || !Array.isArray(attachments) || typeof createId !== "function") {
399
+ throw new TypeError("DSH_INTAKE_USER_INPUT_INVALID");
400
+ }
401
+ const message = Object.freeze({
402
+ id: createId(),
403
+ role: "user",
404
+ source: Object.freeze({ kind: "user", workflowCommand: "wsr" }),
405
+ content: Object.freeze([
406
+ Object.freeze({ type: "text", text: commandTurn(rawInput) }),
407
+ ...attachments,
408
+ ]),
409
+ });
410
+ agent.followup(message);
411
+ await agent.whenIdle();
412
+ return message;
413
+ }
414
+
415
+ export function mapIntakeToolOperation(args) {
416
+ const operationNames = ["list", "create", "recover", "status", "action-finish", "abandon"];
417
+ if (args === null || typeof args !== "object" || Array.isArray(args)
418
+ || Object.keys(args).some((key) => !["operation", "selector", "deliveryId"].includes(key))
419
+ || !operationNames.includes(args.operation)
420
+ || (args.operation === "create") !== (typeof args.selector === "string" && args.selector.length > 0)
421
+ || (args.operation === "abandon" && (typeof args.deliveryId !== "string" || args.deliveryId.length === 0))
422
+ || (!["recover", "status", "abandon"].includes(args.operation) && args.deliveryId !== undefined)) {
423
+ throw new TypeError("INTAKE_OPERATION_INVALID");
424
+ }
425
+ return Object.freeze({ operation: args.operation, ...(args.selector === undefined ? {} : { selector: args.selector }), ...(args.deliveryId === undefined ? {} : { deliveryId: args.deliveryId }), ...(args.operation === "create" ? { directive: "/workflow-execution" } : {}) });
426
+ }
427
+
428
+ export async function apply(ctx, config) {
429
+ const presentationRouter = createSessionPresentationRouter(ctx.agents);
430
+ const runtime = await createPluginRuntime(config, { present: (value) => presentationRouter.present(value),
431
+ sessionAvailable: (sessionKey) => ctx.agents.get(sessionKey) !== undefined,
432
+ resolveConversationWorkspace: async (agent) => resolveConversationWorkspace(ctx, agent) });
433
+ const executionApi = await import("wsr-execution");
434
+ const ownerProjection = executionApi.getExecutionControlPlaneProjection(runtime.application);
435
+ await registerDeliveryControlPlaneGateway(
436
+ ctx,
437
+ createDshSessionControlPlaneReadModel(ownerProjection, runtime.bindings),
438
+ );
439
+ const active = new Set();
440
+ const attachmentStore = ctx.attachments;
441
+ const run = (task) => { active.add(task); void task.finally(() => active.delete(task)).catch(() => undefined); return task; };
442
+ const command = ctx.commands.register({
443
+ name: "wsr",
444
+ description: "Create, list, recover, inspect, finish, or abandon a Workflow Delivery",
445
+ input: { hint: "list | create <selector> | recover [delivery-id] | status [delivery-id] | action finish | abandon <delivery-id>", images: true },
446
+ recordInput: true,
447
+ async handler(invocation) {
448
+ return run((async () => {
449
+ let query = false;
450
+ try {
451
+ 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
+ const { createIntakePresentation, presentationForIntakeResult, serializeIntakePresentation } = await import("wsr-execution");
457
+ if (!query) {
458
+ await recordWsrCommandInput(invocation.agent, invocation.rawInput, invocation.attachments);
459
+ presentToDshSession(invocation.agent, createIntakePresentation(
460
+ String(invocation.commandId), "command-accepted", {},
461
+ ));
462
+ }
463
+ if (invocation.attachments.length > 0 && !["create", "action-finish"].includes(operation.operation)) {
464
+ const presentation = createIntakePresentation(
465
+ `presentation-${randomUUID()}`, "error", { code: "WSR_COMMAND_INVALID", message: "WSR_COMMAND_INVALID" },
466
+ );
467
+ if (!query) presentToDshSession(invocation.agent, presentation);
468
+ return { kind: "error", text: serializeIntakePresentation(presentation, 4096) };
469
+ }
470
+ 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
+ if (["create", "recover"].includes(operation.operation) && !["START_UNCERTAIN", "RECOVERY"].includes(result.kind)) {
472
+ presentationRouter.release(String(invocation.agent.id));
473
+ }
474
+ const presentation = presentationForDshOperation({ createIntakePresentation, presentationForIntakeResult }, `presentation-${randomUUID()}`, operation, result, 4096);
475
+ if (!query) presentToDshSession(invocation.agent, presentation);
476
+ return { kind: result.kind === "ERROR" ? "error" : "success", text: serializeIntakePresentation(presentation, 4096) };
477
+ } catch (cause) {
478
+ const { createIntakePresentation, serializeIntakePresentation } = await import("wsr-execution");
479
+ const code = typeof cause?.code === "string" ? cause.code : "DSH_INTAKE_FAILED";
480
+ const presentation = createIntakePresentation(`presentation-${randomUUID()}`, "error", { code, message: code });
481
+ if (!query) presentToDshSession(invocation.agent, presentation);
482
+ return { kind: "error", text: serializeIntakePresentation(presentation, 4096) };
483
+ }
484
+ })());
485
+ },
486
+ });
487
+ const { defineTool } = await import("@deepseek-ai/dsh-tools");
488
+ const tool = ctx.tools.register(defineTool({
489
+ name: "workflow_execution_intake",
490
+ description: "Invoke exactly one closed Workflow Intake operation for the current DSH-I turn.",
491
+ parameters: { operation: { type: "string", required: true }, selector: { type: "string" }, deliveryId: { type: "string" } },
492
+ output: { schema: { type: "object", properties: { result: { type: "string", required: true } }, additionalProperties: false }, render: (_args, value) => [{ type: "text", text: value.result }] },
493
+ async execute(args, execution) {
494
+ const agent = execution.agent;
495
+ if (agent === undefined) throw new TypeError("DSH_INTAKE_SESSION_UNAVAILABLE");
496
+ const turn = turnFromAgent(agent);
497
+ const operation = mapIntakeToolOperation(args);
498
+ if (["create", "recover"].includes(operation.operation)) presentationRouter.retain(String(agent.id), agent);
499
+ const result = await runtime.invokeForSession({ sessionKey: String(agent.id), agent, operation, turnText: turn.text, images: turn.images, attachmentStore, signal: execution.signal });
500
+ if (["create", "recover"].includes(operation.operation) && !["START_UNCERTAIN", "RECOVERY"].includes(result.kind)) {
501
+ presentationRouter.release(String(agent.id));
502
+ }
503
+ const { createIntakePresentation, presentationForIntakeResult, serializeIntakePresentation } = await import("wsr-execution");
504
+ return { result: serializeIntakePresentation(presentationForDshOperation(
505
+ { createIntakePresentation, presentationForIntakeResult }, `presentation-${randomUUID()}`, operation, result, 4096,
506
+ ), 4096) };
507
+ },
508
+ }));
509
+ const preStep = ctx.on?.("agent/pre-step", async (payload, next) => {
510
+ const messages = payload.messages.filter((message) => message.source?.kind === "user");
511
+ const command = messages.find((message) => message.source?.workflowCommand === "wsr");
512
+ if (command !== undefined) {
513
+ recordConsumedActionReply(payload.agent, command);
514
+ return { kind: "reject" };
515
+ }
516
+ if (await runtime.bindings.bySession(String(payload.agent.id)) === undefined) return next();
517
+ if (messages.length !== 1) return next();
518
+ try {
519
+ presentationRouter.retain(String(payload.agent.id), payload.agent);
520
+ const result = await runtime.answerForSession({ sessionKey: String(payload.agent.id), text: textOf(messages[0].content), images: messages[0].content.filter((block) => block.type === "image"), attachmentStore, signal: payload.signal });
521
+ if (result.kind === "ERROR" && result.code === "ACTION_NOT_AWAITING_INPUT") return next();
522
+ recordConsumedActionReply(payload.agent, messages[0]);
523
+ return { kind: "reject" };
524
+ } catch (cause) {
525
+ recordConsumedActionReply(payload.agent, messages[0]);
526
+ throw cause;
527
+ }
528
+ });
529
+ ctx.effect(function* () {
530
+ yield async () => {
531
+ await command?.();
532
+ await tool?.();
533
+ await preStep?.();
534
+ await Promise.allSettled([...active]);
535
+ await runtime.close();
536
+ };
537
+ }, "workflow-execution intake lifecycle");
538
+ }