@astrosheep/keiyaku 4.5.23 → 4.5.25

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.
Files changed (57) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +18 -0
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/references/automation.md +33 -14
  3. package/build/src/akuma/akuma-handle.d.ts +10 -0
  4. package/build/src/akuma/akuma-handle.js +23 -20
  5. package/build/src/akuma/akuma-instance.d.ts +11 -4
  6. package/build/src/akuma/akuma-instance.js +70 -41
  7. package/build/src/akuma/akuma-observe.d.ts +40 -7
  8. package/build/src/akuma/akuma-observe.js +51 -16
  9. package/build/src/akuma/akuma-product.d.ts +1 -2
  10. package/build/src/akuma/akuma-product.js +8 -5
  11. package/build/src/akuma/akuma.d.ts +3 -0
  12. package/build/src/akuma/akuma.js +1 -0
  13. package/build/src/akuma/body.d.ts +1 -1
  14. package/build/src/akuma/body.js +48 -30
  15. package/build/src/akuma/call-request.d.ts +0 -1
  16. package/build/src/akuma/call-request.js +1 -2
  17. package/build/src/akuma/fleet-execution.d.ts +5 -1
  18. package/build/src/akuma/fleet-execution.js +51 -75
  19. package/build/src/akuma/fleet-observation.d.ts +13 -2
  20. package/build/src/akuma/fleet-observation.js +2 -1
  21. package/build/src/akuma/fleet-request.js +3 -2
  22. package/build/src/akuma/heart/index.d.ts +3 -0
  23. package/build/src/akuma/heart/index.js +12 -2
  24. package/build/src/akuma/heart/timeline.d.ts +1 -1
  25. package/build/src/akuma/heart/timeline.js +5 -5
  26. package/build/src/akuma/index.d.ts +2 -2
  27. package/build/src/akuma/projection-read.d.ts +2 -0
  28. package/build/src/akuma/projection-read.js +36 -7
  29. package/build/src/akuma/projection.d.ts +2 -1
  30. package/build/src/akuma/projection.js +25 -10
  31. package/build/src/akuma/providers/pi/index.d.ts +2 -1
  32. package/build/src/akuma/providers/pi/index.js +2 -1
  33. package/build/src/akuma/publication.js +7 -4
  34. package/build/src/akuma/schema.d.ts +40 -0
  35. package/build/src/akuma/schema.js +114 -0
  36. package/build/src/akuma-body.js +1 -0
  37. package/build/src/cli/commands/akuma-invoke.d.ts +6 -0
  38. package/build/src/cli/commands/akuma-invoke.js +35 -6
  39. package/build/src/cli/invoke.js +1 -0
  40. package/build/src/cli/render/akuma-activity.d.ts +19 -8
  41. package/build/src/cli/render/akuma-activity.js +267 -150
  42. package/build/src/cli/runtime.js +2 -2
  43. package/build/src/index.d.ts +2 -1
  44. package/build/src/index.js +1 -0
  45. package/build/src/kanshi/read.js +32 -6
  46. package/build/src/library/address.d.ts +3 -0
  47. package/build/src/library/address.js +12 -9
  48. package/build/src/library/akuma-creation.d.ts +3 -0
  49. package/build/src/library/akuma-creation.js +111 -46
  50. package/build/src/library/composition.d.ts +3 -1
  51. package/build/src/library/fleet.d.ts +7 -1
  52. package/build/src/library/fleet.js +89 -20
  53. package/build/src/library/keiyaku.d.ts +3 -1
  54. package/build/src/protocol/read/status.d.ts +5 -0
  55. package/build/src/protocol/read/status.js +11 -0
  56. package/build/src/runtime/proc/windows-launch.exe +0 -0
  57. package/package.json +1 -1
@@ -49,6 +49,24 @@ vocabulary, while an explicit empty default permits none. A nested call can use
49
49
  only actions permitted by its direct parent Soul. Use `status <aku/...|@alias>`
50
50
  to inspect the born worker's frozen effective actions.
51
51
 
52
+ ## Answer Schemas
53
+
54
+ For a schema-bearing call or tell through the public API, pass the schema
55
+ directly — `{ schema: z.object({ claim: z.string() }) }` — importing `z` from
56
+ the package root next to `Akuma`. Any Standard Schema v1 value works the same
57
+ way, and the explicit `Schema.zod(...)` and `Schema.json(...)` forms remain
58
+ available for callers who want them.
59
+
60
+ Keep an answer contract inside simple JSON shape vocabulary: objects, arrays,
61
+ strings, numbers, booleans, enums, literals, and optional or nullable fields.
62
+ Do not attach `.max`, `.min`, `.regex`, `.refine`, `.transform`, or other
63
+ constraint methods. The provider must satisfy the contract, and a fragile or
64
+ unrepresentable constraint fails the loop after submission; the seam refuses
65
+ such a schema at submission and names the offending keyword instead. Enforce
66
+ bounds, formats, and cross-field rules in ordinary caller code after the answer
67
+ arrives, and treat a full JSON Schema through `Schema.json(...)` as the explicit
68
+ waiver a caller signs only when it owns that risk.
69
+
52
70
  ## Akuma Names
53
71
 
54
72
  An Akuma name selects a reusable worker configuration, not an individual worker.
@@ -53,14 +53,13 @@ investigation, not as a supposedly fresh judge of its own earlier answer.
53
53
  ## Public Entry And A Single Structured Turn
54
54
 
55
55
  Run an ESM script (`.mjs`) in a project where `@astrosheep/keiyaku` resolves.
56
- The Zod examples also need `zod` available to the script. A globally installed
57
- CLI alone does not establish Node package resolution for an arbitrary script.
56
+ Import `z` from the package root; a globally installed CLI alone does not
57
+ establish Node package resolution for an arbitrary script.
58
58
  Use `keiyaku ls aku/` to select an available Archetype; names and upstream model
59
59
  availability are installation-specific.
60
60
 
61
61
  ```js
62
- import { Akuma, Schema, World } from "@astrosheep/keiyaku";
63
- import { z } from "zod";
62
+ import { Akuma, World, z } from "@astrosheep/keiyaku";
64
63
 
65
64
  const root = await World.at(process.cwd());
66
65
  const archetype = process.env.AKUMA_ARCHETYPE;
@@ -74,11 +73,11 @@ const worker = await Akuma.birth(archetype, {
74
73
  console.error("worker", worker.id); // Keep the complete AkuId.
75
74
  await worker.idle(); // Let the prompt-free birth Body settle before a schema Tell.
76
75
 
77
- const Finding = Schema.zod(z.object({
76
+ const Finding = z.object({
78
77
  claim: z.string(),
79
78
  evidence: z.array(z.object({ path: z.string(), observation: z.string() })),
80
79
  unknowns: z.array(z.string()),
81
- }).strict());
80
+ });
82
81
 
83
82
  const finding = await worker.tell(
84
83
  "Read the repository guidance and relevant owner documents. Read only; " +
@@ -90,9 +89,19 @@ console.log(JSON.stringify(finding, null, 2));
90
89
  ```
91
90
 
92
91
  `birth` does not submit a prompt. Plain `tell` returns answer text; schema
93
- `tell` returns the decoded value, not a JSON string to scrape. For a JSON
94
- Schema and a custom decoder, use `Schema.json(document, decode)` instead of
95
- `Schema.zod(...)`.
92
+ `tell` returns the decoded value, not a JSON string to scrape. Pass the schema
93
+ directly; any Standard Schema v1 value works the same way. The explicit
94
+ `Schema.zod(...)` wrapper still works, and `Schema.json(document, decode)` is
95
+ the escape hatch for a caller-owned JSON Schema and custom decoder.
96
+
97
+ Keep an answer contract inside simple JSON shape vocabulary: objects, arrays,
98
+ strings, numbers, booleans, enums, literals, and optional or nullable fields.
99
+ Do not attach `.max`, `.min`, `.regex`, `.refine`, `.transform`, or other
100
+ constraint methods. The provider must satisfy the contract, and a fragile or
101
+ unrepresentable constraint fails the loop after submission; the seam refuses
102
+ such a schema at submission and names the offending keyword instead. Enforce
103
+ bounds, formats, and cross-field rules in ordinary caller code after the answer
104
+ arrives.
96
105
 
97
106
  Schema makes shape machine-usable, not claims true. Include evidence and
98
107
  unknowns in the requested value; acceptance still needs a suitable judge.
@@ -106,14 +115,14 @@ the prompts, schemas, routing, and selection to the task rather than always
106
115
  running this exact pipeline.
107
116
 
108
117
  ```js
109
- const Claims = Schema.zod(z.object({
110
- claims: z.array(z.object({ id: z.string(), text: z.string() })).max(12),
111
- }).strict());
112
- const Verdict = Schema.zod(z.object({
118
+ const Claims = z.object({
119
+ claims: z.array(z.object({ id: z.string(), text: z.string() })),
120
+ });
121
+ const Verdict = z.object({
113
122
  verdict: z.enum(["supported", "contradicted", "unknown"]),
114
123
  evidence: z.array(z.object({ path: z.string(), observation: z.string() })),
115
124
  reason: z.string(),
116
- }).strict());
125
+ });
117
126
 
118
127
  // Caller-owned concurrency helper, not a Keiyaku API.
119
128
  async function mapSettled(items, concurrency, run) {
@@ -145,6 +154,9 @@ const { claims } = await worker.tell(
145
154
  if (new Set(claims.map(c => c.id)).size !== claims.length) {
146
155
  throw new Error("Duplicate claim ids");
147
156
  }
157
+ if (claims.length > 12) {
158
+ throw new Error("Claim list exceeded the 12-claim budget");
159
+ }
148
160
 
149
161
  async function freshJudge(prompt) {
150
162
  const judge = await Akuma.birth(archetype, { root, cwd: process.cwd(), readonly: true });
@@ -200,6 +212,13 @@ pairwise comparisons, experiment queues, or adaptive sampling instead.
200
212
  - `idle({ timeoutMs })` stops waiting at its timeout, not the worker. A
201
213
  `Promise.race` timeout also does not cancel a Tell. Use explicit lifecycle
202
214
  operations when you intend to interrupt or stop work.
215
+ - `idle()` resolves an `AkumaIdleResult` saying why it returned, so there is
216
+ no need to re-poll `status()` to distinguish the outcomes. A completed wait
217
+ resolves `{ kind: "idle", status, reason }` with `reason` naming the settled
218
+ life (`"asleep"`, `"killed"`, `"hung"`, `"untidy"`, or `"stranded"`); a
219
+ passed deadline resolves `{ kind: "timeout", status, reason }` with `reason`
220
+ naming what was still outstanding as `{ running, pendingTell }`. Both arms
221
+ carry the final observed `status`.
203
222
  - Keep input ids, AkuIds, terminal results, failures, and completed stages in
204
223
  caller-owned artifacts if the run must survive its orchestrator process.
205
224
  On return, `Akuma.select(root, savedId)` reconnects synchronously; `status()`
@@ -2,6 +2,7 @@ import { type TellResult, type TellWakeRuntime } from "./body.js";
2
2
  import { HeldAkumaLeash, type KillEvidence } from "./heart/index.js";
3
3
  import { type AkuId, type AkumaPaths } from "./identity.js";
4
4
  import { type ActivityHistory, type ExactHistory } from "./projection.js";
5
+ import { type WaitReason } from "./akuma-observe.js";
5
6
  import type { AkumaCallExecution, AkumaStatus, ForkReceipt, InterruptReceipt } from "./akuma.js";
6
7
  import type { WorldRoot } from "../world.js";
7
8
  declare const CALL_EXECUTION: unique symbol;
@@ -25,12 +26,21 @@ export declare class AkumaHandle {
25
26
  since?: number;
26
27
  limit?: number;
27
28
  }>): Promise<ActivityHistory | ExactHistory>;
29
+ waitReceipt(predicate?: (status: AkumaStatus) => boolean, options?: Readonly<{
30
+ timeoutMs?: number;
31
+ signal?: AbortSignal;
32
+ }>): Promise<Readonly<{
33
+ reason: WaitReason;
34
+ status: AkumaStatus;
35
+ }>>;
28
36
  wait(predicate?: (status: AkumaStatus) => boolean, options?: Readonly<{
29
37
  timeoutMs?: number;
38
+ signal?: AbortSignal;
30
39
  }>): Promise<AkumaStatus>;
31
40
  tell(body: string, tellId?: string, recordedAt?: string, runtime?: TellWakeRuntime, options?: Readonly<{
32
41
  schemaJson?: string;
33
42
  initiator?: string;
43
+ signal?: AbortSignal;
34
44
  }>): Promise<TellResult>;
35
45
  interrupt(body: string, options?: Readonly<{
36
46
  tellId?: string;
@@ -8,10 +8,8 @@ import { resolveProviderExecution } from "./providers/index.js";
8
8
  import { publishAkuma } from "./publication.js";
9
9
  import { spawnAkumaBody } from "./body.js";
10
10
  import { AkumaNotBornError } from "./akuma-errors.js";
11
- import { bornStatus, defaultWaitComplete, readWaitComplete } from "./akuma-observe.js";
11
+ import { bornStatus, defaultWaitComplete, readWaitComplete, waitForObservation, } from "./akuma-observe.js";
12
12
  const CALL_EXECUTION = Symbol("akuma-call-execution");
13
- const POLL_MS = 100;
14
- const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
15
13
  function diagnostic(error) {
16
14
  return error instanceof Error ? error.message : String(error);
17
15
  }
@@ -33,10 +31,13 @@ async function takeLeashUntilSignal(paths, bodySequence, signal, unbounded = fal
33
31
  export async function settleAkumaKill(paths, signal, retainLeash = false) {
34
32
  const request = await requestStop(paths, new Date().toISOString(), signal);
35
33
  if (request.kind !== "requested") {
34
+ // A witnessed Body was already settled: the kill witness Heart recorded is
35
+ // the kill evidence, not a stop outcome.
36
+ const evidence = request.kind === "witnessed" ? "killed" : request.kind;
36
37
  if (!retainLeash)
37
- return { evidence: request.kind };
38
+ return { evidence };
38
39
  const leash = await acquireLeash(paths, signal === undefined ? {} : { signal });
39
- return leash === null ? { evidence: "unavailable" } : { evidence: request.kind, leash };
40
+ return leash === null ? { evidence: "unavailable" } : { evidence, leash };
40
41
  }
41
42
  const target = request.body;
42
43
  const waited = await takeLeashUntilSignal(paths, target.sequence, signal);
@@ -131,24 +132,26 @@ export class AkumaHandle {
131
132
  limit,
132
133
  });
133
134
  }
134
- async wait(predicate = defaultWaitComplete, options = {}) {
135
+ async waitReceipt(predicate = defaultWaitComplete, options = {}) {
135
136
  if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
136
137
  throw new TypeError("Akuma wait timeoutMs must be a nonnegative finite millisecond duration");
137
138
  }
138
- const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
139
- for (;;) {
140
- if (predicate !== defaultWaitComplete ||
141
- (deadline !== undefined && performance.now() >= deadline) ||
142
- (await readWaitComplete(this.worldPath, this.id))) {
143
- const status = await this.status();
144
- if (predicate(status) || (deadline !== undefined && performance.now() >= deadline))
145
- return status;
146
- }
147
- await wait(deadline === undefined ? POLL_MS : Math.min(POLL_MS, Math.max(0, deadline - performance.now())));
148
- }
139
+ const waited = await waitForObservation({
140
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
141
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
142
+ ...(predicate === defaultWaitComplete
143
+ ? { probe: async () => await readWaitComplete(this.worldPath, this.id) }
144
+ : {}),
145
+ observe: async () => await this.status(),
146
+ complete: predicate,
147
+ });
148
+ return { reason: waited.reason, status: waited.value };
149
+ }
150
+ async wait(predicate = defaultWaitComplete, options = {}) {
151
+ return (await this.waitReceipt(predicate, options)).status;
149
152
  }
150
153
  async tell(body, tellId = randomUUID(), recordedAt = new Date().toISOString(), runtime, options = {}) {
151
- const { schemaJson, initiator } = options;
154
+ const { schemaJson, initiator, signal } = options;
152
155
  const admitted = await recordTell(this.paths, {
153
156
  kind: "tell",
154
157
  id: tellId,
@@ -159,7 +162,7 @@ export class AkumaHandle {
159
162
  });
160
163
  if (admitted.kind === "not-born")
161
164
  throw new AkumaNotBornError(this.id);
162
- return await wakeRecordedTell(this.paths, admitted.tell.id, runtime);
165
+ return await wakeRecordedTell(this.paths, admitted.tell.id, runtime, signal);
163
166
  }
164
167
  async interrupt(body, options = {}) {
165
168
  const request = await requestPause(this.paths, new Date().toISOString(), options.signal);
@@ -209,7 +212,7 @@ export class AkumaHandle {
209
212
  return {
210
213
  kind: "interrupted",
211
214
  putDown,
212
- tell: await wakeRecordedTell(this.paths, recorded.tellId, options.runtime),
215
+ tell: await wakeRecordedTell(this.paths, recorded.tellId, options.runtime, options.signal),
213
216
  };
214
217
  }
215
218
  async fork(input) {
@@ -1,13 +1,19 @@
1
1
  import type { AllowedAction } from "./allowed.js";
2
2
  import type { AkumaStatus } from "./akuma.js";
3
3
  import type { InterruptReceipt, KillEvidence } from "./akuma.js";
4
+ import { type WaitReason } from "./akuma-observe.js";
4
5
  import { type AkuId } from "./identity.js";
5
6
  import { type ActivityHistory } from "./projection.js";
6
7
  import type { Settings } from "../settings.js";
7
8
  import type { WorldRoot } from "../world.js";
8
- import { type Schema } from "./schema.js";
9
+ import { type Schema, type StandardSchemaV1 } from "./schema.js";
9
10
  export type AkumaIdleOptions = Readonly<{
10
11
  timeoutMs?: number;
12
+ signal?: AbortSignal;
13
+ }>;
14
+ export type AkumaIdleResult = Readonly<{
15
+ reason: WaitReason;
16
+ status: AkumaStatus;
11
17
  }>;
12
18
  export type AkumaHistoryOptions = Readonly<{
13
19
  before?: number;
@@ -26,7 +32,7 @@ export type AkumaBirthInput = Readonly<{
26
32
  allowed?: readonly AllowedAction[];
27
33
  }>;
28
34
  export type AkumaTellOptions<T> = Readonly<{
29
- schema: Schema<T>;
35
+ schema: Schema<T> | StandardSchemaV1<T>;
30
36
  interrupt?: boolean;
31
37
  initiator?: string;
32
38
  }>;
@@ -39,13 +45,14 @@ export declare class Akuma {
39
45
  static select(root: WorldRoot, selector: string): Akuma;
40
46
  tell(text: string, options?: Readonly<{
41
47
  initiator?: string;
48
+ signal?: AbortSignal;
42
49
  }>): Promise<string>;
43
- tell<T>(text: string, options: AkumaTellOptions<T>): Promise<T>;
50
+ tell<T>(text: string, options: AkumaTellOptions<T> & AkumaSignalOptions): Promise<T>;
44
51
  status(): Promise<AkumaStatus>;
45
52
  interrupt(text: string, options?: AkumaSignalOptions & Readonly<{
46
53
  initiator?: string;
47
54
  }>): Promise<InterruptReceipt>;
48
- idle(options?: AkumaIdleOptions): Promise<void>;
55
+ idle(options?: AkumaIdleOptions): Promise<AkumaIdleResult>;
49
56
  history(options?: AkumaHistoryOptions): Promise<ActivityHistory>;
50
57
  kill(options?: AkumaSignalOptions): Promise<KillEvidence>;
51
58
  }
@@ -4,15 +4,14 @@ import { spawnAkumaBody } from "./body.js";
4
4
  import { decodeAllowedActions, unionAllowedActions } from "./allowed.js";
5
5
  import { AkumaDecodeError, AkumaProviderError } from "./akuma-errors.js";
6
6
  import { AkumaHandle } from "./akuma-handle.js";
7
- import { POLL_MS, defaultWaitComplete } from "./akuma.js";
8
- import { bornStatus } from "./akuma-observe.js";
7
+ import { bornStatus, defaultWaitComplete, waitForObservation } from "./akuma-observe.js";
9
8
  import { loadArchetype } from "./archetype.js";
10
9
  import { activitySlice, readTell, readTurn } from "./heart/index.js";
11
10
  import { parseAkuId, pathsForAkuId } from "./identity.js";
12
11
  import { birthAkuma, launchAkuma } from "./publication.js";
13
12
  import { projectTurns, selectHistory } from "./projection.js";
14
13
  import { settings as readSettings } from "../settings.js";
15
- import { schemaJsonText } from "./schema.js";
14
+ import { schemaFromStandard, schemaJsonText } from "./schema.js";
16
15
  import { abortable } from "./abort.js";
17
16
  const HISTORY_LIMIT = 12;
18
17
  function signalOption(value) {
@@ -22,25 +21,37 @@ function signalOption(value) {
22
21
  throw new TypeError("signal must be an AbortSignal");
23
22
  return value;
24
23
  }
25
- function wait(milliseconds) {
26
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
27
- }
28
24
  function recordedTell(result) {
29
25
  if (result.wake.kind === "failed")
30
26
  throw new AkumaProviderError(result.wake.diagnostic);
31
27
  return { tellId: result.admission.tellId };
32
28
  }
33
- async function recordPlainTell(id, root, body, tellId, initiator) {
34
- const admitted = await new AkumaHandle(id, root).tell(body, tellId, undefined, undefined, initiator === undefined ? {} : { initiator });
29
+ async function recordPlainTell(input) {
30
+ const { id, root, body, tellId, initiator, signal } = input;
31
+ const admitted = await new AkumaHandle(id, root).tell(body, tellId, undefined, undefined, {
32
+ ...(initiator === undefined ? {} : { initiator }),
33
+ ...(signal === undefined ? {} : { signal }),
34
+ });
35
35
  return recordedTell(admitted);
36
36
  }
37
+ function plainTellInput(input) {
38
+ return {
39
+ id: input.id,
40
+ root: input.root,
41
+ body: input.body,
42
+ tellId: input.tellId,
43
+ ...(input.options?.initiator === undefined ? {} : { initiator: input.options.initiator }),
44
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
45
+ };
46
+ }
37
47
  async function recordSchemaTell(input) {
38
- const { id, body, tellId, options, root } = input;
39
- if (options.interrupt === true) {
48
+ const { id, body, tellId, schema, root } = input;
49
+ if (input.interrupt === true) {
40
50
  const interrupted = await new AkumaHandle(id, root).interrupt(body, {
41
51
  tellId,
42
- schemaJson: schemaJsonText(options.schema),
43
- ...(options.initiator === undefined ? {} : { initiator: options.initiator }),
52
+ schemaJson: schemaJsonText(schema),
53
+ ...(input.initiator === undefined ? {} : { initiator: input.initiator }),
54
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
44
55
  });
45
56
  if (interrupted.kind === "unavailable") {
46
57
  throw new AkumaProviderError(`schema interrupt unavailable: ${interrupted.evidence}`);
@@ -48,8 +59,9 @@ async function recordSchemaTell(input) {
48
59
  return recordedTell(interrupted.tell);
49
60
  }
50
61
  const admitted = await new AkumaHandle(id, root).tell(body, tellId, undefined, undefined, {
51
- schemaJson: schemaJsonText(options.schema),
52
- ...(options.initiator === undefined ? {} : { initiator: options.initiator }),
62
+ schemaJson: schemaJsonText(schema),
63
+ ...(input.initiator === undefined ? {} : { initiator: input.initiator }),
64
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
53
65
  });
54
66
  return recordedTell(admitted);
55
67
  }
@@ -66,19 +78,21 @@ async function boundOutcome(paths, tell) {
66
78
  const turn = await readTurn(paths, tell.binding.turnSequence);
67
79
  return turn?.end?.outcome ?? null;
68
80
  }
69
- async function awaitTellOutcome(paths, tellId) {
70
- for (;;) {
71
- const tell = await readTell(paths, tellId);
72
- if (tell === null)
73
- throw new AkumaProviderError(`recorded Tell ${tellId} is missing from Heart`);
74
- const outcome = await boundOutcome(paths, tell);
75
- if (outcome !== null)
76
- return outcome;
77
- if (tell.state === "told" && tell.binding === undefined) {
78
- throw new AkumaProviderError(`recorded Tell ${tellId} reached a terminal delivery without a Turn binding`);
79
- }
80
- await wait(POLL_MS);
81
- }
81
+ async function awaitTellOutcome(paths, tellId, signal) {
82
+ const waited = await waitForObservation({
83
+ ...(signal === undefined ? {} : { signal }),
84
+ observe: async () => {
85
+ const tell = await readTell(paths, tellId);
86
+ if (tell === null)
87
+ throw new AkumaProviderError(`recorded Tell ${tellId} is missing from Heart`);
88
+ const outcome = await boundOutcome(paths, tell);
89
+ return { outcome, terminalWithoutTurn: tell.state === "told" && tell.binding === undefined };
90
+ },
91
+ complete: (observed) => observed.outcome !== null || observed.terminalWithoutTurn,
92
+ });
93
+ if (waited.value.outcome !== null)
94
+ return waited.value.outcome;
95
+ throw new AkumaProviderError(`recorded Tell ${tellId} reached a terminal delivery without a Turn binding`);
82
96
  }
83
97
  export class Akuma {
84
98
  id;
@@ -134,20 +148,34 @@ export class Akuma {
134
148
  async tell(text, options) {
135
149
  if (typeof text !== "string")
136
150
  throw new TypeError("Akuma tell text must be a string");
151
+ const signal = signalOption(options?.signal);
152
+ signal?.throwIfAborted();
137
153
  const tellId = randomUUID();
138
- const recorded = options === undefined || !("schema" in options)
139
- ? await recordPlainTell(this.id, this.root, text, tellId, options?.initiator)
140
- : await recordSchemaTell({
154
+ const schemaOptions = options !== undefined && "schema" in options ? options : undefined;
155
+ const schema = schemaOptions === undefined ? undefined : schemaFromStandard(schemaOptions.schema);
156
+ const recorded = schemaOptions === undefined || schema === undefined
157
+ ? await recordPlainTell(plainTellInput({
141
158
  id: this.id,
159
+ root: this.root,
142
160
  body: text,
143
161
  tellId,
144
162
  options,
163
+ signal,
164
+ }))
165
+ : await recordSchemaTell({
166
+ id: this.id,
167
+ body: text,
168
+ tellId,
169
+ schema,
145
170
  root: this.root,
171
+ ...(schemaOptions.interrupt === undefined ? {} : { interrupt: schemaOptions.interrupt }),
172
+ ...(schemaOptions.initiator === undefined ? {} : { initiator: schemaOptions.initiator }),
173
+ ...(signal === undefined ? {} : { signal }),
146
174
  });
147
- const outcome = await awaitTellOutcome(this.paths, recorded.tellId);
175
+ const outcome = await awaitTellOutcome(this.paths, recorded.tellId, signal);
148
176
  if (outcome.kind !== "answered")
149
177
  outcomeError(outcome);
150
- if (options === undefined || !("schema" in options))
178
+ if (schema === undefined)
151
179
  return outcome.answer;
152
180
  const raw = outcome.answerJson ?? outcome.answer;
153
181
  let parsed;
@@ -158,7 +186,7 @@ export class Akuma {
158
186
  throw new AkumaDecodeError(error instanceof Error ? error.message : "Answer is not valid JSON", outcome.answer);
159
187
  }
160
188
  try {
161
- return options.schema.decode(parsed);
189
+ return schema.decode(parsed);
162
190
  }
163
191
  catch (error) {
164
192
  throw new AkumaDecodeError(error instanceof Error ? error.message : "Answer failed schema decode", outcome.answer);
@@ -185,19 +213,20 @@ export class Akuma {
185
213
  if (typeof options !== "object" || options === null || Array.isArray(options)) {
186
214
  throw new TypeError("Akuma idle options must be an object");
187
215
  }
188
- const unknown = Object.keys(options).find((key) => key !== "timeoutMs");
216
+ const unknown = Object.keys(options).find((key) => key !== "timeoutMs" && key !== "signal");
189
217
  if (unknown !== undefined)
190
218
  throw new TypeError(`Akuma idle options has unknown field: ${unknown}`);
191
219
  if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
192
220
  throw new TypeError("Akuma idle timeoutMs must be a nonnegative finite millisecond duration");
193
221
  }
194
- const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
195
- for (;;) {
196
- const observed = await bornStatus(this.paths, this.id, { aperture: "monitoring" });
197
- if (defaultWaitComplete(observed.status) || (deadline !== undefined && performance.now() >= deadline))
198
- return;
199
- await wait(deadline === undefined ? POLL_MS : Math.min(POLL_MS, Math.max(0, deadline - performance.now())));
200
- }
222
+ const signal = signalOption(options.signal);
223
+ const waited = await waitForObservation({
224
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
225
+ ...(signal === undefined ? {} : { signal }),
226
+ observe: async () => (await bornStatus(this.paths, this.id, { aperture: "monitoring" })).status,
227
+ complete: defaultWaitComplete,
228
+ });
229
+ return { reason: waited.reason, status: waited.value };
201
230
  }
202
231
  async history(options = {}) {
203
232
  if (typeof options !== "object" || options === null || Array.isArray(options)) {
@@ -1,5 +1,5 @@
1
1
  import { type AkuId, type AkumaPaths } from "./identity.js";
2
- import { selectHistory, type ActivityHistory, type ActivitySnapshot } from "./projection.js";
2
+ import { selectHistory, type ActivityHistory, type ActivityRow, type ActivitySnapshot } from "./projection.js";
3
3
  import type { WorldRoot } from "../world.js";
4
4
  import type { AkumaListRow, AkumaStatus, UnbornAkumaListRow } from "./akuma.js";
5
5
  export declare function fleetListRow(paths: AkumaPaths, expected: AkuId): Promise<AkumaListRow | UnbornAkumaListRow>;
@@ -7,6 +7,14 @@ export type BudgetedStatusObservation = Readonly<{
7
7
  status: AkumaStatus;
8
8
  ordinarySelected: number;
9
9
  }>;
10
+ /**
11
+ * The internal live-observation companion for one status frontier. It is not
12
+ * part of status or any returned facade result: viewers receive it only while
13
+ * an observation loop is still streaming.
14
+ */
15
+ export type LiveStatusObservation = BudgetedStatusObservation & Readonly<{
16
+ rows: readonly ActivityRow[];
17
+ }>;
10
18
  export declare function bornStatus(paths: AkumaPaths, expected: AkuId, input: Readonly<{
11
19
  aperture: "monitoring" | "receipt";
12
20
  ordinaryBudget?: number;
@@ -17,18 +25,43 @@ export declare function readBudgetedStatus(worldPath: WorldRoot, id: AkuId, inpu
17
25
  ordinaryBudget?: number;
18
26
  admittedTellId?: string;
19
27
  }>): Promise<BudgetedStatusObservation>;
28
+ export declare function readLiveStatus(worldPath: WorldRoot, id: AkuId, input: Readonly<{
29
+ aperture: "monitoring" | "receipt";
30
+ ordinaryBudget?: number;
31
+ admittedTellId?: string;
32
+ }>): Promise<LiveStatusObservation>;
20
33
  export declare function defaultWaitComplete(status: AkumaStatus): boolean;
21
34
  export declare function readWaitComplete(worldPath: WorldRoot, id: AkuId): Promise<boolean>;
35
+ export type WaitReason = "completed" | "deadline";
36
+ /**
37
+ * The one deadline-aware observation loop for public Akuma waiting. It always
38
+ * reads once before judging a passed deadline, so a zero-duration wait remains
39
+ * an honest final snapshot and completion wins at the deadline edge.
40
+ */
41
+ export declare function waitForObservation<T>(input: Readonly<{
42
+ timeoutMs?: number;
43
+ signal?: AbortSignal;
44
+ probe?(): Promise<boolean>;
45
+ observe(): Promise<T>;
46
+ complete(value: T): boolean;
47
+ onObserve?(value: T): void | Promise<void>;
48
+ }>): Promise<Readonly<{
49
+ reason: WaitReason;
50
+ value: T;
51
+ }>>;
22
52
  /**
23
53
  * Successive status observations of one already born Akuma until its
24
- * observation window closes. Every status observed while the window is open is
25
- * reported to `observe`; the window's last status is returned. A window that is
26
- * already closed, or an Akuma already at its completion judgment, reports
27
- * nothing and returns the current status.
54
+ * observation window closes. Every acquired observation is reported to
55
+ * `observe` before completion or deadline judgment, including the first
56
+ * already-complete observation; the last status is returned.
28
57
  */
29
58
  export declare function observeAkumaStatus(worldPath: WorldRoot, expected: AkuId, input: Readonly<{
30
59
  timeoutMs: number;
31
- observe: (status: AkumaStatus) => void;
32
- }>): Promise<AkumaStatus>;
60
+ signal?: AbortSignal;
61
+ observe: (status: LiveStatusObservation) => void;
62
+ }>): Promise<Readonly<{
63
+ reason: WaitReason;
64
+ status: AkumaStatus;
65
+ }>>;
33
66
  export declare function readAkumaBirthCwd(worldPath: WorldRoot, id: AkuId): Promise<string>;
34
67
  export { selectHistory, type ActivityHistory, type ActivitySnapshot };