@astrosheep/keiyaku 4.5.19 → 4.5.21
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 +4 -4
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +14 -7
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +53 -20
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/references/automation.md +237 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +6 -7
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +2 -4
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +3 -3
- package/build/src/akuma/akuma-handle.d.ts +5 -1
- package/build/src/akuma/akuma-handle.js +4 -1
- package/build/src/akuma/akuma-instance.d.ts +7 -2
- package/build/src/akuma/akuma-instance.js +14 -7
- package/build/src/akuma/akuma-observe.d.ts +11 -0
- package/build/src/akuma/akuma-observe.js +23 -0
- package/build/src/akuma/akuma-product.d.ts +2 -0
- package/build/src/akuma/akuma-product.js +6 -0
- package/build/src/akuma/akuma.d.ts +20 -1
- package/build/src/akuma/akuma.js +2 -0
- package/build/src/akuma/allowed.d.ts +20 -0
- package/build/src/akuma/allowed.js +11 -0
- package/build/src/akuma/archetype.js +3 -0
- package/build/src/akuma/body.d.ts +25 -5
- package/build/src/akuma/body.js +70 -31
- package/build/src/akuma/call-request.d.ts +5 -0
- package/build/src/akuma/call-request.js +14 -4
- package/build/src/akuma/fleet-execution.d.ts +28 -0
- package/build/src/akuma/fleet-execution.js +36 -4
- package/build/src/akuma/fleet-observation.d.ts +54 -0
- package/build/src/akuma/fleet-request.d.ts +10 -0
- package/build/src/akuma/fleet-request.js +13 -3
- package/build/src/akuma/heart/facts.d.ts +2 -0
- package/build/src/akuma/heart/index.d.ts +1 -0
- package/build/src/akuma/heart/index.js +4 -1
- package/build/src/akuma/heart/rows.d.ts +2 -0
- package/build/src/akuma/heart/rows.js +7 -5
- package/build/src/akuma/heart/schema.d.ts +1 -1
- package/build/src/akuma/heart/schema.js +3 -1
- package/build/src/akuma/heart/tells.js +6 -5
- package/build/src/akuma/heart/timeline.js +1 -1
- package/build/src/akuma/provider-recipe.d.ts +1 -0
- package/build/src/akuma/provider-recipe.js +5 -0
- package/build/src/akuma/provider.d.ts +1 -1
- package/build/src/akuma/provider.js +1 -1
- package/build/src/akuma/providers/acp/core.js +3 -6
- package/build/src/akuma/providers/acp/index.js +3 -0
- package/build/src/akuma/providers/claude/index.js +10 -10
- package/build/src/akuma/providers/codex-app-server/index.js +12 -11
- package/build/src/akuma/providers/execution-environment.d.ts +6 -0
- package/build/src/akuma/providers/execution-environment.js +24 -0
- package/build/src/akuma/providers/grok-build/index.js +3 -0
- package/build/src/akuma/providers/opencode-sdk/index.js +32 -23
- package/build/src/akuma/providers/opencode-sdk/session.d.ts +8 -1
- package/build/src/akuma/providers/opencode-sdk/session.js +6 -5
- package/build/src/akuma/providers/pi/events.js +7 -2
- package/build/src/akuma/providers/pi/index.js +5 -2
- package/build/src/akuma/request-observation.d.ts +45 -0
- package/build/src/akuma/request-observation.js +162 -0
- package/build/src/akuma/request-rendezvous.d.ts +6 -3
- package/build/src/akuma/request-rendezvous.js +80 -14
- package/build/src/akuma/request-serve.js +27 -4
- package/build/src/akuma/request-wire.d.ts +5 -0
- package/build/src/akuma/request-wire.js +2 -0
- package/build/src/akuma/turn-drive.d.ts +1 -0
- package/build/src/akuma/turn-drive.js +2 -0
- package/build/src/akuma-body.js +5 -0
- package/build/src/body/amend.js +6 -4
- package/build/src/body/decode.d.ts +3 -1
- package/build/src/body/decode.js +53 -17
- package/build/src/body/region.d.ts +9 -1
- package/build/src/body/region.js +87 -12
- package/build/src/body/render.js +1 -1
- package/build/src/body/verification.d.ts +3 -1
- package/build/src/body/verification.js +4 -1
- package/build/src/cli/accepted.js +19 -2
- package/build/src/cli/commands/akuma-invoke.d.ts +3 -1
- package/build/src/cli/commands/akuma-invoke.js +134 -14
- package/build/src/cli/commands/akuma.js +24 -12
- package/build/src/cli/commands/contract-help.d.ts +15 -15
- package/build/src/cli/commands/contract-help.js +45 -19
- package/build/src/cli/commands/contract-invoke.d.ts +3 -0
- package/build/src/cli/commands/contract-invoke.js +25 -11
- package/build/src/cli/commands/contract.js +6 -4
- package/build/src/cli/commands/install.d.ts +1 -1
- package/build/src/cli/commands/install.js +1 -1
- package/build/src/cli/commands/status-set.js +15 -24
- package/build/src/cli/commands/task.js +18 -18
- package/build/src/cli/invoke.d.ts +3 -0
- package/build/src/cli/invoke.js +58 -23
- package/build/src/cli/main.js +5 -0
- package/build/src/cli/parse.d.ts +3 -0
- package/build/src/cli/parse.js +26 -4
- package/build/src/cli/render/akuma-activity.d.ts +100 -2
- package/build/src/cli/render/akuma-activity.js +485 -40
- package/build/src/cli/render/akuma.d.ts +10 -0
- package/build/src/cli/render/akuma.js +31 -21
- package/build/src/cli/render/audit.js +1 -1
- package/build/src/cli/render/board.js +1 -1
- package/build/src/cli/render/catalog.js +16 -13
- package/build/src/cli/render/contract-history.js +39 -10
- package/build/src/cli/render/contract-observation.js +10 -6
- package/build/src/cli/render/contract.js +109 -106
- package/build/src/cli/render/execution-progress.d.ts +26 -0
- package/build/src/cli/render/execution-progress.js +178 -0
- package/build/src/cli/render/kanshi-akuma.js +27 -39
- package/build/src/cli/render/kanshi.js +33 -26
- package/build/src/cli/render/nuke.js +1 -1
- package/build/src/cli/render/receipt.d.ts +1 -1
- package/build/src/cli/render/receipt.js +6 -6
- package/build/src/cli/render/refusal.js +10 -7
- package/build/src/cli/render/region.js +1 -1
- package/build/src/cli/render/settings.js +12 -2
- package/build/src/cli/render/status-line.d.ts +30 -0
- package/build/src/cli/render/status-line.js +58 -0
- package/build/src/cli/render/status-set.js +1 -1
- package/build/src/cli/render/task.d.ts +2 -0
- package/build/src/cli/render/task.js +11 -7
- package/build/src/cli/render/terminal.d.ts +0 -1
- package/build/src/cli/render/terminal.js +47 -15
- package/build/src/cli/result.d.ts +1 -0
- package/build/src/cli/runtime.d.ts +2 -0
- package/build/src/cli/runtime.js +36 -4
- package/build/src/cli/usage.js +2 -2
- package/build/src/cli/version.d.ts +1 -0
- package/build/src/cli/version.js +25 -0
- package/build/src/git/hooks.d.ts +13 -1
- package/build/src/git/hooks.js +2 -1
- package/build/src/git/workspace.d.ts +1 -1
- package/build/src/git/workspace.js +1 -1
- package/build/src/identity/mint.d.ts +13 -0
- package/build/src/identity/mint.js +26 -0
- package/build/src/identity/normalize.d.ts +4 -0
- package/build/src/identity/normalize.js +30 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/kanshi/read.js +17 -4
- package/build/src/kanshi/report.d.ts +1 -0
- package/build/src/library/akuma-creation.d.ts +2 -0
- package/build/src/library/akuma-creation.js +34 -18
- package/build/src/library/audit.d.ts +2 -0
- package/build/src/library/audit.js +1 -0
- package/build/src/library/bind.js +18 -16
- package/build/src/library/composition.d.ts +1 -0
- package/build/src/library/contract-bind.d.ts +3 -1
- package/build/src/library/contract-bind.js +5 -3
- package/build/src/library/contract-execution.d.ts +2 -0
- package/build/src/library/contract-execution.js +15 -1
- package/build/src/library/contract-forwarding.d.ts +4 -0
- package/build/src/library/contract-forwarding.js +3 -0
- package/build/src/library/contract-handle.d.ts +7 -0
- package/build/src/library/contract-handle.js +25 -0
- package/build/src/library/contract-operations.d.ts +7 -2
- package/build/src/library/contract-operations.js +21 -1
- package/build/src/library/contract-types.d.ts +1 -0
- package/build/src/library/execution-result.d.ts +2 -2
- package/build/src/library/execution.d.ts +8 -0
- package/build/src/library/execution.js +102 -0
- package/build/src/library/fleet.d.ts +4 -1
- package/build/src/library/fleet.js +32 -8
- package/build/src/library/keiyaku.d.ts +2 -0
- package/build/src/library/mutation.js +15 -7
- package/build/src/plugin/akuma-signals.d.ts +6 -0
- package/build/src/plugin/akuma-signals.js +9 -1
- package/build/src/plugin/public.d.ts +6 -2
- package/build/src/plugin/runtime.d.ts +1 -3
- package/build/src/plugin/runtime.js +95 -103
- package/build/src/protocol/amend.js +4 -3
- package/build/src/protocol/attempt.d.ts +2 -0
- package/build/src/protocol/bind.js +41 -14
- package/build/src/protocol/completion.d.ts +3 -0
- package/build/src/protocol/completion.js +35 -11
- package/build/src/protocol/deliver.js +165 -92
- package/build/src/protocol/execution-observation.d.ts +181 -0
- package/build/src/protocol/execution-observation.js +45 -0
- package/build/src/protocol/intent.d.ts +18 -22
- package/build/src/protocol/intent.js +63 -41
- package/build/src/protocol/operations.d.ts +2 -0
- package/build/src/protocol/placement.js +5 -1
- package/build/src/protocol/progress.d.ts +5 -1
- package/build/src/protocol/progress.js +10 -1
- package/build/src/protocol/read/status.d.ts +1 -0
- package/build/src/protocol/read/status.js +10 -2
- package/build/src/protocol/reintegrate.d.ts +1 -1
- package/build/src/protocol/reintegrate.js +70 -43
- package/build/src/protocol/result-codec.js +27 -11
- package/build/src/protocol/review.js +53 -30
- package/build/src/protocol/run.d.ts +83 -36
- package/build/src/protocol/run.js +90 -75
- package/build/src/runtime/proc/run.d.ts +16 -4
- package/build/src/runtime/proc/run.js +64 -22
- package/build/src/runtime/proc/windows-launch.exe +0 -0
- package/build/src/settings.js +5 -14
- package/build/src/task/document.d.ts +5 -1
- package/build/src/task/document.js +6 -1
- package/build/src/verification/execution.d.ts +10 -2
- package/build/src/verification/execution.js +143 -42
- package/build/src/verification/observation.d.ts +41 -0
- package/build/src/verification/observation.js +29 -0
- package/package.json +3 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const AKUMA_REQUESTS_ENV = "AKUMA_REQUESTS";
|
|
2
|
+
const PARENT_HARNESS_IDENTITY_KEYS = [
|
|
3
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
4
|
+
"CLAUDE_CODE_CHILD_SESSION",
|
|
5
|
+
"CLAUDECODE",
|
|
6
|
+
"CODEX_THREAD_ID",
|
|
7
|
+
"OPENCODE_SESSION_ID",
|
|
8
|
+
"PI_SESSION_ID",
|
|
9
|
+
"PI_SESSION_FILE",
|
|
10
|
+
"PASEO_AGENT_ID",
|
|
11
|
+
"SQUARE_PARTICIPANT_NAME",
|
|
12
|
+
];
|
|
13
|
+
/** Construct the environment for one isolated Akuma execution or native child. */
|
|
14
|
+
export function akumaExecutionEnvironment(inherited, overrides = {}, requests, nativeIdentity = []) {
|
|
15
|
+
const preserved = Object.fromEntries(nativeIdentity.flatMap((key) => (inherited[key] === undefined ? [] : [[key, inherited[key]]])));
|
|
16
|
+
const environment = Object.fromEntries(Object.entries({ ...inherited, ...overrides }).flatMap(([key, value]) => value === undefined ? [] : [[key, value]]));
|
|
17
|
+
for (const key of PARENT_HARNESS_IDENTITY_KEYS)
|
|
18
|
+
delete environment[key];
|
|
19
|
+
Object.assign(environment, preserved);
|
|
20
|
+
delete environment[AKUMA_REQUESTS_ENV];
|
|
21
|
+
if (requests !== undefined)
|
|
22
|
+
environment[AKUMA_REQUESTS_ENV] = requests;
|
|
23
|
+
return environment;
|
|
24
|
+
}
|
|
@@ -67,6 +67,9 @@ export const interpretGrokTool = (update) => {
|
|
|
67
67
|
fileChangeCall(name, input));
|
|
68
68
|
};
|
|
69
69
|
function optionAdmission(options) {
|
|
70
|
+
if (options.sandbox !== undefined) {
|
|
71
|
+
return { kind: "refused", diagnostic: "Grok Build does not support the sandbox option" };
|
|
72
|
+
}
|
|
70
73
|
if (options.network !== undefined) {
|
|
71
74
|
return { kind: "refused", diagnostic: "Grok Build does not support the network option" };
|
|
72
75
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { AgentEventChannel, createProviderAttempt, } from "../../provider.js";
|
|
3
3
|
import { createEventState, mapEvent } from "./events.js";
|
|
4
4
|
import { coordinate, loadOpencode, OPENCODE_SDK_PROVIDER, parseModel } from "./session.js";
|
|
5
5
|
function object(value) {
|
|
@@ -22,6 +22,8 @@ function opencodeSessionId(coordinateValue) {
|
|
|
22
22
|
return coordinateValue.sessionId;
|
|
23
23
|
}
|
|
24
24
|
function admit(options) {
|
|
25
|
+
if (options.sandbox !== undefined)
|
|
26
|
+
throw new Error("OpenCode does not support the sandbox option");
|
|
25
27
|
if (options.network !== undefined)
|
|
26
28
|
throw new Error("OpenCode does not support explicit network");
|
|
27
29
|
if (options.systemPromptMode === "replace") {
|
|
@@ -202,12 +204,13 @@ async function forceDisposeOpencode(abortController, close, finish) {
|
|
|
202
204
|
}
|
|
203
205
|
async function loadDriveRuntime(execution, input, signal, loader, onRuntime) {
|
|
204
206
|
const runtime = await loadOpencode({
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
},
|
|
210
|
-
|
|
207
|
+
execution,
|
|
208
|
+
cwd: input.cwd,
|
|
209
|
+
signal,
|
|
210
|
+
...(loader === undefined ? {} : { loader }),
|
|
211
|
+
...(onRuntime === undefined ? {} : { onRuntime }),
|
|
212
|
+
...(input.requests === undefined ? {} : { requests: input.requests.dir }),
|
|
213
|
+
});
|
|
211
214
|
if (signal.aborted) {
|
|
212
215
|
await runtime.close();
|
|
213
216
|
signal.throwIfAborted();
|
|
@@ -341,22 +344,28 @@ export function createOpencodeProvider(input = { name: OPENCODE_SDK_PROVIDER, ki
|
|
|
341
344
|
fork: (input) => createProviderAttempt(new AbortController().signal, async (custody) => {
|
|
342
345
|
const sessionId = opencodeSessionId(input.session);
|
|
343
346
|
let close;
|
|
344
|
-
const runtime = await loadOpencode(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
347
|
+
const runtime = await loadOpencode({
|
|
348
|
+
execution,
|
|
349
|
+
cwd: input.cwd,
|
|
350
|
+
signal: custody.signal,
|
|
351
|
+
...(loader === undefined ? {} : { loader }),
|
|
352
|
+
onRuntime: (ready) => {
|
|
353
|
+
let closing;
|
|
354
|
+
let settleRuntimeClosed;
|
|
355
|
+
let rejectRuntimeClosed;
|
|
356
|
+
const runtimeClosed = new Promise((resolve, reject) => {
|
|
357
|
+
settleRuntimeClosed = resolve;
|
|
358
|
+
rejectRuntimeClosed = reject;
|
|
359
|
+
});
|
|
360
|
+
close = () => {
|
|
361
|
+
if (closing === undefined) {
|
|
362
|
+
closing = ready.close();
|
|
363
|
+
void closing.then(settleRuntimeClosed, rejectRuntimeClosed);
|
|
364
|
+
}
|
|
365
|
+
return closing;
|
|
366
|
+
};
|
|
367
|
+
custody.own({ closed: runtimeClosed, abort: close, forceDispose: close });
|
|
368
|
+
},
|
|
360
369
|
});
|
|
361
370
|
try {
|
|
362
371
|
const result = await runtime.client.session.fork({
|
|
@@ -23,5 +23,12 @@ export type OpencodeRuntime = Readonly<{
|
|
|
23
23
|
};
|
|
24
24
|
close: () => Promise<void>;
|
|
25
25
|
}>;
|
|
26
|
-
export declare function loadOpencode(
|
|
26
|
+
export declare function loadOpencode(input: Readonly<{
|
|
27
|
+
execution: ProviderExecution;
|
|
28
|
+
cwd: string;
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
loader?: OpencodeSdkLoader;
|
|
31
|
+
onRuntime?: (runtime: OpencodeRuntime) => void;
|
|
32
|
+
requests?: string;
|
|
33
|
+
}>): Promise<OpencodeRuntime>;
|
|
27
34
|
export declare function coordinate(sessionId: string): ResumeCoordinate;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import { abortable } from "../../abort.js";
|
|
3
|
+
import { akumaExecutionEnvironment } from "../execution-environment.js";
|
|
3
4
|
import { spawnDetachedProcess } from "../../../runtime/proc/run.js";
|
|
4
5
|
export const OPENCODE_SDK_PROVIDER = "opencode-sdk";
|
|
5
6
|
export function parseModel(model) {
|
|
@@ -37,9 +38,10 @@ async function availablePort() {
|
|
|
37
38
|
});
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
|
-
export async function loadOpencode(
|
|
41
|
+
export async function loadOpencode(input) {
|
|
42
|
+
const { execution, cwd, signal, loader, onRuntime, requests } = input;
|
|
41
43
|
if (loader) {
|
|
42
|
-
const loaded = await abortable(loader(cwd, execution, signal), signal);
|
|
44
|
+
const loaded = await abortable(loader(cwd, { ...execution, env: akumaExecutionEnvironment(process.env, execution.env, requests) }, signal), signal);
|
|
43
45
|
let closing;
|
|
44
46
|
const runtime = {
|
|
45
47
|
client: loaded.client,
|
|
@@ -64,11 +66,10 @@ export async function loadOpencode(execution, cwd, signal, loader, onRuntime) {
|
|
|
64
66
|
const owned = await spawnDetachedProcess({
|
|
65
67
|
argv: [execution.executable ?? "opencode", "serve", "--hostname", "127.0.0.1", "--port", String(port)],
|
|
66
68
|
cwd,
|
|
67
|
-
env: {
|
|
68
|
-
...process.env,
|
|
69
|
+
env: akumaExecutionEnvironment(process.env, {
|
|
69
70
|
...(execution.config === undefined ? {} : { OPENCODE_CONFIG_CONTENT: JSON.stringify(execution.config) }),
|
|
70
71
|
...execution.env,
|
|
71
|
-
},
|
|
72
|
+
}, requests),
|
|
72
73
|
log: `${cwd}/.opencode.log`,
|
|
73
74
|
});
|
|
74
75
|
const { createOpencodeClient } = await import("@opencode-ai/sdk");
|
|
@@ -89,7 +89,9 @@ function searchCall(name, value) {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
function fileChangeCall(name, value) {
|
|
92
|
-
|
|
92
|
+
// Native write cannot prove creation versus overwrite and reports no patch, so
|
|
93
|
+
// it joins the conservative update operation with unknown diffstat.
|
|
94
|
+
if ((name !== "edit" && name !== "write") || typeof value.path !== "string")
|
|
93
95
|
return undefined;
|
|
94
96
|
return { kind: "fileChange", changes: [{ op: "update", path: value.path }] };
|
|
95
97
|
}
|
|
@@ -158,7 +160,10 @@ function translateMessage(event, state) {
|
|
|
158
160
|
}
|
|
159
161
|
const text = textContent(message.content);
|
|
160
162
|
state.answer = text;
|
|
161
|
-
|
|
163
|
+
// Gemini emits an empty text block alongside every tool-use message. It is
|
|
164
|
+
// transport scaffolding, not narration; an ordinary terminal empty answer
|
|
165
|
+
// remains public evidence.
|
|
166
|
+
if (hasTextContent(message.content) && !(message.stopReason === "toolUse" && text.length === 0)) {
|
|
162
167
|
translated.push({ type: "assistant", text });
|
|
163
168
|
}
|
|
164
169
|
return translated;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { abortable } from "../../abort.js";
|
|
2
|
-
import {
|
|
2
|
+
import { akumaExecutionEnvironment } from "../execution-environment.js";
|
|
3
|
+
import { AgentEventChannel, createProviderAttempt, } from "../../provider.js";
|
|
3
4
|
import { piTerminalFailure, translatePiEvent } from "./events.js";
|
|
4
5
|
const PI_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
5
6
|
const MODEL_PATTERN = /^[^/\s]+\/[^\s]+$/u;
|
|
@@ -7,6 +8,8 @@ function diagnostic(error) {
|
|
|
7
8
|
return error instanceof Error ? error.message : String(error);
|
|
8
9
|
}
|
|
9
10
|
function admitPiOptions(options) {
|
|
11
|
+
if (options.sandbox !== undefined)
|
|
12
|
+
return { kind: "refused", diagnostic: "Pi provider does not support the sandbox option" };
|
|
10
13
|
if (options.network !== undefined)
|
|
11
14
|
return { kind: "refused", diagnostic: "Pi provider does not support the network option" };
|
|
12
15
|
if (options.model !== undefined && !MODEL_PATTERN.test(options.model)) {
|
|
@@ -54,7 +57,7 @@ async function piCreateOptions(sdk, input) {
|
|
|
54
57
|
(await import("@earendil-works/pi-coding-agent")).createBashToolDefinition(input.cwd, {
|
|
55
58
|
spawnHook: (context) => ({
|
|
56
59
|
...context,
|
|
57
|
-
env:
|
|
60
|
+
env: akumaExecutionEnvironment(context.env, {}, input.requests.dir, ["PI_SESSION_ID", "PI_SESSION_FILE"]),
|
|
58
61
|
}),
|
|
59
62
|
}),
|
|
60
63
|
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const REQUEST_PROGRESS_WINDOW = 64;
|
|
2
|
+
export type RequestProgressSnapshot = Readonly<{
|
|
3
|
+
nextSequence: number;
|
|
4
|
+
events: readonly Readonly<{
|
|
5
|
+
sequence: number;
|
|
6
|
+
value: unknown;
|
|
7
|
+
}>[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare function progressPath(directory: string, transportId: string): string;
|
|
10
|
+
export declare function cancellationPath(directory: string, transportId: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* The snapshot is a bounded, replaceable observation window. It is deliberately
|
|
13
|
+
* not a progress log: a slow observer receives an explicit sequence gap.
|
|
14
|
+
*/
|
|
15
|
+
export declare function publishRequestProgress(input: Readonly<{
|
|
16
|
+
directory: string;
|
|
17
|
+
transportId: string;
|
|
18
|
+
id: string;
|
|
19
|
+
action: string;
|
|
20
|
+
}>): Readonly<{
|
|
21
|
+
progress(value: unknown): void;
|
|
22
|
+
flush(): Promise<void>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function readRequestProgress(input: Readonly<{
|
|
26
|
+
directory: string;
|
|
27
|
+
transportId: string;
|
|
28
|
+
id: string;
|
|
29
|
+
action: string;
|
|
30
|
+
}>): Promise<RequestProgressSnapshot | undefined>;
|
|
31
|
+
export declare function publishRequestCancellation(input: Readonly<{
|
|
32
|
+
directory: string;
|
|
33
|
+
transportId: string;
|
|
34
|
+
id: string;
|
|
35
|
+
action: string;
|
|
36
|
+
}>): Promise<void>;
|
|
37
|
+
export declare function observeRequestCancellation(input: Readonly<{
|
|
38
|
+
directory: string;
|
|
39
|
+
transportId: string;
|
|
40
|
+
id: string;
|
|
41
|
+
action: string;
|
|
42
|
+
}>): Readonly<{
|
|
43
|
+
signal: AbortSignal;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { abortableDelay } from "./abort.js";
|
|
4
|
+
import { atomicJson } from "./request-wire.js";
|
|
5
|
+
export const REQUEST_PROGRESS_WINDOW = 64;
|
|
6
|
+
const OBSERVATION_POLL_MS = 25;
|
|
7
|
+
const sequenceSchema = z.number().int().nonnegative();
|
|
8
|
+
const nextSequenceSchema = z.number().int().positive();
|
|
9
|
+
const progressSnapshotSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
id: z.string(),
|
|
12
|
+
action: z.string(),
|
|
13
|
+
nextSequence: nextSequenceSchema,
|
|
14
|
+
events: z.array(z.object({ sequence: sequenceSchema, value: z.unknown() }).strict()).max(REQUEST_PROGRESS_WINDOW),
|
|
15
|
+
})
|
|
16
|
+
.strict()
|
|
17
|
+
.superRefine((snapshot, context) => {
|
|
18
|
+
for (let index = 1; index < snapshot.events.length; index += 1) {
|
|
19
|
+
if (snapshot.events[index].sequence !== snapshot.events[index - 1].sequence + 1) {
|
|
20
|
+
context.addIssue({ code: "custom", message: "progress sequences must be contiguous" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (snapshot.events.at(-1)?.sequence !== undefined && snapshot.events.at(-1).sequence >= snapshot.nextSequence) {
|
|
25
|
+
context.addIssue({ code: "custom", message: "progress sequence exceeds its snapshot" });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const cancellationSchema = z.object({ id: z.string(), action: z.string() }).strict();
|
|
29
|
+
export function progressPath(directory, transportId) {
|
|
30
|
+
return `${directory}/${transportId}.progress.json`;
|
|
31
|
+
}
|
|
32
|
+
export function cancellationPath(directory, transportId) {
|
|
33
|
+
return `${directory}/${transportId}.cancel.json`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The snapshot is a bounded, replaceable observation window. It is deliberately
|
|
37
|
+
* not a progress log: a slow observer receives an explicit sequence gap.
|
|
38
|
+
*/
|
|
39
|
+
export function publishRequestProgress(input) {
|
|
40
|
+
let nextSequence = 1;
|
|
41
|
+
let events = [];
|
|
42
|
+
let dirty = false;
|
|
43
|
+
let closed = false;
|
|
44
|
+
let writer;
|
|
45
|
+
const snapshot = () => ({ nextSequence, events });
|
|
46
|
+
const write = async () => {
|
|
47
|
+
while (dirty && !closed) {
|
|
48
|
+
dirty = false;
|
|
49
|
+
try {
|
|
50
|
+
await atomicJson(progressPath(input.directory, input.transportId), {
|
|
51
|
+
id: input.id,
|
|
52
|
+
action: input.action,
|
|
53
|
+
...snapshot(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Observation is never allowed to impede service or its durable outcome.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const startWriter = () => {
|
|
62
|
+
if (writer !== undefined)
|
|
63
|
+
return;
|
|
64
|
+
writer = write().finally(() => {
|
|
65
|
+
writer = undefined;
|
|
66
|
+
if (dirty && !closed)
|
|
67
|
+
startWriter();
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const flush = async () => {
|
|
71
|
+
while (writer !== undefined)
|
|
72
|
+
await writer;
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
progress(value) {
|
|
76
|
+
if (closed)
|
|
77
|
+
return;
|
|
78
|
+
const sequence = nextSequence++;
|
|
79
|
+
try {
|
|
80
|
+
const encoded = JSON.stringify({ value });
|
|
81
|
+
if (encoded === undefined || !Object.hasOwn(JSON.parse(encoded), "value"))
|
|
82
|
+
throw new Error("not JSON transport data");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
dirty = true;
|
|
86
|
+
startWriter();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (events.length === REQUEST_PROGRESS_WINDOW) {
|
|
90
|
+
events = events.slice(1);
|
|
91
|
+
}
|
|
92
|
+
events = [...events, { sequence, value }];
|
|
93
|
+
dirty = true;
|
|
94
|
+
startWriter();
|
|
95
|
+
},
|
|
96
|
+
flush,
|
|
97
|
+
async close() {
|
|
98
|
+
await flush();
|
|
99
|
+
closed = true;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function readRequestProgress(input) {
|
|
104
|
+
let bytes;
|
|
105
|
+
try {
|
|
106
|
+
bytes = await readFile(progressPath(input.directory, input.transportId), "utf8");
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.code === "ENOENT")
|
|
110
|
+
return undefined;
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const parsed = progressSnapshotSchema.safeParse(JSON.parse(bytes));
|
|
115
|
+
if (!parsed.success || parsed.data.id !== input.id || parsed.data.action !== input.action)
|
|
116
|
+
return undefined;
|
|
117
|
+
return { nextSequence: parsed.data.nextSequence, events: parsed.data.events };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function publishRequestCancellation(input) {
|
|
124
|
+
try {
|
|
125
|
+
await atomicJson(cancellationPath(input.directory, input.transportId), { id: input.id, action: input.action });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// The channel may have disappeared; rendezvous will report the truthful unknown outcome.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function observeRequestCancellation(input) {
|
|
132
|
+
const local = new AbortController();
|
|
133
|
+
const stop = new AbortController();
|
|
134
|
+
const run = (async () => {
|
|
135
|
+
while (!stop.signal.aborted && !local.signal.aborted) {
|
|
136
|
+
try {
|
|
137
|
+
const parsed = cancellationSchema.safeParse(JSON.parse(await readFile(cancellationPath(input.directory, input.transportId), "utf8")));
|
|
138
|
+
if (parsed.success && parsed.data.id === input.id && parsed.data.action === input.action) {
|
|
139
|
+
local.abort(new Error("request cancelled by caller"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// An absent or malformed cancellation notice has no authority.
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await abortableDelay(OBSERVATION_POLL_MS, stop.signal);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
return {
|
|
155
|
+
signal: local.signal,
|
|
156
|
+
async close() {
|
|
157
|
+
if (!stop.signal.aborted)
|
|
158
|
+
stop.abort();
|
|
159
|
+
await run;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -18,12 +18,15 @@ type RequestResponse<Output, Reference> = Readonly<{
|
|
|
18
18
|
requestId: string;
|
|
19
19
|
action: string;
|
|
20
20
|
}>;
|
|
21
|
-
|
|
22
|
-
export declare function requestBodyCommand<Input, Output, Reference>(input: Readonly<{
|
|
21
|
+
type RequestCommandInput<Input, Output, Reference> = Readonly<{
|
|
23
22
|
directory: string;
|
|
24
23
|
id?: string;
|
|
25
24
|
command: RequestProtocol<Input, Output, Reference>;
|
|
26
25
|
value: Input;
|
|
27
26
|
signal?: AbortSignal;
|
|
28
|
-
|
|
27
|
+
onProgress?(value: unknown): void;
|
|
28
|
+
onProgressGap?(dropped: number): void;
|
|
29
|
+
}>;
|
|
30
|
+
/** Generic rendezvous only: operation owners supply their own request and result codecs. */
|
|
31
|
+
export declare function requestBodyCommand<Input, Output, Reference>(input: RequestCommandInput<Input, Output, Reference>): Promise<RequestResponse<Output, Reference>>;
|
|
29
32
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { access, readFile } from "node:fs/promises";
|
|
3
3
|
import { abortableDelay } from "./abort.js";
|
|
4
|
+
import { publishRequestCancellation, readRequestProgress, } from "./request-observation.js";
|
|
4
5
|
import { atomicJson, decodeReceiptEnvelope, receiptPath, requestPath } from "./request-wire.js";
|
|
5
6
|
const POLL_MS = 100;
|
|
6
7
|
export class AkumaBodyRequestError extends Error {
|
|
@@ -110,21 +111,86 @@ export async function requestBodyCommand(input) {
|
|
|
110
111
|
throw new AkumaBodyRequestError(input.command.action, "unknown", "parent request channel closed before request publication", id);
|
|
111
112
|
}
|
|
112
113
|
const path = receiptPath(input.directory, transportId);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
return await observePublishedRequest(input, id, transportId, path);
|
|
115
|
+
}
|
|
116
|
+
async function observePublishedRequest(input, id, transportId, path) {
|
|
117
|
+
let observedSequence = 0;
|
|
118
|
+
const consumeProgress = async () => {
|
|
119
|
+
const snapshot = await readRequestProgress({
|
|
120
|
+
directory: input.directory,
|
|
121
|
+
transportId,
|
|
122
|
+
id,
|
|
123
|
+
action: input.command.action,
|
|
124
|
+
});
|
|
125
|
+
observedSequence = consumeSnapshot(snapshot, observedSequence, input.onProgress, input.onProgressGap);
|
|
126
|
+
};
|
|
127
|
+
const publishCancellation = () => {
|
|
128
|
+
void publishRequestCancellation({ directory: input.directory, transportId, id, action: input.command.action });
|
|
129
|
+
};
|
|
130
|
+
const cancellable = input.command.supportsCancellation === true;
|
|
131
|
+
if (cancellable && input.signal !== undefined) {
|
|
132
|
+
input.signal.addEventListener("abort", publishCancellation, { once: true });
|
|
133
|
+
if (input.signal.aborted)
|
|
134
|
+
publishCancellation();
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
for (;;) {
|
|
138
|
+
let response;
|
|
139
|
+
try {
|
|
140
|
+
response = await readRequestReceipt(input, path, id);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
// A visible receipt guarantees its final progress was already published, even on failure.
|
|
144
|
+
await consumeProgress();
|
|
145
|
+
}
|
|
146
|
+
if (response !== undefined)
|
|
147
|
+
return response;
|
|
148
|
+
if (!(await access(input.directory).then(() => true, () => false))) {
|
|
149
|
+
throw new AkumaBodyRequestError(input.command.action, "unknown", "parent request channel closed before a receipt", id);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await abortableDelay(POLL_MS, cancellable ? undefined : input.signal);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (input.signal?.aborted) {
|
|
156
|
+
throw new AkumaBodyRequestError(input.command.action, "unknown", "request was cancelled after publication and before a receipt", id);
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
126
159
|
}
|
|
127
|
-
throw error;
|
|
128
160
|
}
|
|
129
161
|
}
|
|
162
|
+
finally {
|
|
163
|
+
if (cancellable && input.signal !== undefined)
|
|
164
|
+
input.signal.removeEventListener("abort", publishCancellation);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function consumeSnapshot(snapshot, observedSequence, onProgress, onProgressGap) {
|
|
168
|
+
if (snapshot === undefined)
|
|
169
|
+
return observedSequence;
|
|
170
|
+
const events = snapshot.events.filter((event) => event.sequence > observedSequence);
|
|
171
|
+
const latestSequence = snapshot.nextSequence - 1;
|
|
172
|
+
const firstAvailable = events[0]?.sequence ?? latestSequence + 1;
|
|
173
|
+
const gap = firstAvailable - observedSequence - 1;
|
|
174
|
+
if (gap > 0) {
|
|
175
|
+
notifyObserver(onProgressGap, gap);
|
|
176
|
+
}
|
|
177
|
+
if (events.length === 0)
|
|
178
|
+
return Math.max(observedSequence, latestSequence);
|
|
179
|
+
for (const event of events) {
|
|
180
|
+
observedSequence = event.sequence;
|
|
181
|
+
notifyObserver(onProgress, event.value);
|
|
182
|
+
}
|
|
183
|
+
return observedSequence;
|
|
184
|
+
}
|
|
185
|
+
function notifyObserver(observer, value) {
|
|
186
|
+
if (observer === undefined)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
void Promise.resolve(observer(value)).catch(() => {
|
|
190
|
+
// A caller observation failure cannot affect its in-flight request.
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// A caller observation failure cannot affect its in-flight request.
|
|
195
|
+
}
|
|
130
196
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { admitRequest, beginRequest, isRequestInputConflict, readHeart, readRequest, serveRequest as serveChildRequest, serveUpstreamRequest, voidRequest, unproveRequest, } from "./heart/index.js";
|
|
2
2
|
import { atomicJson, receiptPath, } from "./request-wire.js";
|
|
3
|
+
import { observeRequestCancellation, publishRequestProgress } from "./request-observation.js";
|
|
3
4
|
import { BodyRequestPump as LifecycleBodyRequestPump, settleReserved, } from "./request-lifecycle.js";
|
|
4
5
|
export { clearBodyRequestTransport, settleBodyRequests } from "./request-lifecycle.js";
|
|
5
6
|
function replayReference(fact, command) {
|
|
@@ -72,16 +73,38 @@ async function serveResolvedCommand(input, command, request, execute) {
|
|
|
72
73
|
return true;
|
|
73
74
|
}
|
|
74
75
|
try {
|
|
76
|
+
const progress = publishRequestProgress({
|
|
77
|
+
directory: input.directory,
|
|
78
|
+
transportId: input.transportId,
|
|
79
|
+
id: fact.id,
|
|
80
|
+
action: fact.action,
|
|
81
|
+
});
|
|
82
|
+
const cancellation = command.completion === "service" && request.supportsCancellation
|
|
83
|
+
? observeRequestCancellation({
|
|
84
|
+
directory: input.directory,
|
|
85
|
+
transportId: input.transportId,
|
|
86
|
+
id: fact.id,
|
|
87
|
+
action: fact.action,
|
|
88
|
+
})
|
|
89
|
+
: undefined;
|
|
75
90
|
const facts = {
|
|
76
91
|
id: fact.id,
|
|
77
92
|
admittedAt: fact.admittedAt,
|
|
78
93
|
requester: fact.requester,
|
|
79
|
-
signal: input.signal,
|
|
94
|
+
signal: cancellation === undefined ? input.signal : AbortSignal.any([input.signal, cancellation.signal]),
|
|
80
95
|
admissionOpen: input.admissionOpen,
|
|
96
|
+
progress: progress.progress,
|
|
81
97
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
try {
|
|
99
|
+
const served = await execute(fact, facts);
|
|
100
|
+
fact = served.fact;
|
|
101
|
+
await progress.flush();
|
|
102
|
+
await projectCommandReceipt(input, fact, command, served.outcome);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await progress.close();
|
|
106
|
+
await cancellation?.close();
|
|
107
|
+
}
|
|
85
108
|
}
|
|
86
109
|
catch (error) {
|
|
87
110
|
const failure = encodedFailure(request, error);
|
|
@@ -71,6 +71,8 @@ export type RequestEnvelope = z.infer<typeof requestEnvelopeSchema>;
|
|
|
71
71
|
*/
|
|
72
72
|
export type RequestProtocol<Input, Output, Reference> = Readonly<{
|
|
73
73
|
action: string;
|
|
74
|
+
/** An opted-in service request accepts cancellation after publication. */
|
|
75
|
+
supportsCancellation?: boolean;
|
|
74
76
|
encodeRequest(input: Input): unknown;
|
|
75
77
|
decodeRequest(payload: unknown): Input | null;
|
|
76
78
|
encodeResult(result: Output): unknown;
|
|
@@ -86,6 +88,8 @@ export type ExecutionFacts = Readonly<{
|
|
|
86
88
|
requester: string;
|
|
87
89
|
signal: AbortSignal;
|
|
88
90
|
admissionOpen(): boolean;
|
|
91
|
+
/** Best-effort, ephemeral progress for this one live request. */
|
|
92
|
+
progress?(value: unknown): void;
|
|
89
93
|
}>;
|
|
90
94
|
export type ChildRequestCommand<Input, Output, Reference> = Readonly<{
|
|
91
95
|
completion: "child";
|
|
@@ -109,6 +113,7 @@ export type ServiceRequestCommand<Input, Output, Service, Reference> = Readonly<
|
|
|
109
113
|
}>;
|
|
110
114
|
type ErasedRequest = Readonly<{
|
|
111
115
|
payloadJson: string;
|
|
116
|
+
supportsCancellation: boolean;
|
|
112
117
|
isPermitted(allowed: readonly string[]): boolean;
|
|
113
118
|
encodeFailure(error: unknown): unknown | null;
|
|
114
119
|
}>;
|
|
@@ -53,6 +53,7 @@ export function eraseRequestCommand(command) {
|
|
|
53
53
|
return null;
|
|
54
54
|
return {
|
|
55
55
|
payloadJson: JSON.stringify(protocol.encodeRequest(input)),
|
|
56
|
+
supportsCancellation: protocol.supportsCancellation === true,
|
|
56
57
|
isPermitted: (allowed) => protocol.isPermitted(allowed),
|
|
57
58
|
execute: async (facts) => {
|
|
58
59
|
const served = await command.execute(input, facts);
|
|
@@ -73,6 +74,7 @@ export function eraseRequestCommand(command) {
|
|
|
73
74
|
return null;
|
|
74
75
|
return {
|
|
75
76
|
payloadJson: JSON.stringify(protocol.encodeRequest(input)),
|
|
77
|
+
supportsCancellation: protocol.supportsCancellation === true,
|
|
76
78
|
isPermitted: (allowed) => protocol.isPermitted(allowed),
|
|
77
79
|
execute: async (facts) => {
|
|
78
80
|
const served = await command.execute(input, facts);
|
|
@@ -25,6 +25,7 @@ export type DriveTurnInput = Readonly<{
|
|
|
25
25
|
body: string;
|
|
26
26
|
call?: string;
|
|
27
27
|
launchTells: readonly TellFact[];
|
|
28
|
+
initiator?: string;
|
|
28
29
|
schemaJson?: string;
|
|
29
30
|
supervisor: BodySupervisor;
|
|
30
31
|
runtimeSpawn(launch: AkumaCallRequestChildLaunch): Promise<OwnedProcess | void>;
|