@astrosheep/keiyaku 4.5.22 → 4.5.24

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 (32) 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.js +5 -2
  4. package/build/src/akuma/akuma-instance.d.ts +15 -3
  5. package/build/src/akuma/akuma-instance.js +27 -13
  6. package/build/src/akuma/akuma.d.ts +1 -0
  7. package/build/src/akuma/akuma.js +1 -0
  8. package/build/src/akuma/fleet-execution.d.ts +2 -0
  9. package/build/src/akuma/fleet-execution.js +22 -5
  10. package/build/src/akuma/heart/index.d.ts +3 -0
  11. package/build/src/akuma/heart/index.js +12 -2
  12. package/build/src/akuma/index.d.ts +2 -2
  13. package/build/src/akuma/projection-read.d.ts +2 -0
  14. package/build/src/akuma/projection-read.js +4 -0
  15. package/build/src/akuma/projection.d.ts +1 -1
  16. package/build/src/akuma/projection.js +1 -1
  17. package/build/src/akuma/schema.d.ts +40 -0
  18. package/build/src/akuma/schema.js +114 -0
  19. package/build/src/cli/commands/akuma-invoke.d.ts +5 -0
  20. package/build/src/cli/commands/akuma-invoke.js +19 -3
  21. package/build/src/cli/render/akuma-activity.d.ts +6 -3
  22. package/build/src/cli/render/akuma-activity.js +168 -51
  23. package/build/src/cli/runtime.js +2 -2
  24. package/build/src/index.d.ts +2 -1
  25. package/build/src/index.js +1 -0
  26. package/build/src/kanshi/read.js +32 -6
  27. package/build/src/library/address.d.ts +3 -0
  28. package/build/src/library/address.js +2 -1
  29. package/build/src/library/fleet.js +54 -11
  30. package/build/src/protocol/read/status.d.ts +5 -0
  31. package/build/src/protocol/read/status.js +11 -0
  32. 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()`
@@ -33,10 +33,13 @@ async function takeLeashUntilSignal(paths, bodySequence, signal, unbounded = fal
33
33
  export async function settleAkumaKill(paths, signal, retainLeash = false) {
34
34
  const request = await requestStop(paths, new Date().toISOString(), signal);
35
35
  if (request.kind !== "requested") {
36
+ // A witnessed Body was already settled: the kill witness Heart recorded is
37
+ // the kill evidence, not a stop outcome.
38
+ const evidence = request.kind === "witnessed" ? "killed" : request.kind;
36
39
  if (!retainLeash)
37
- return { evidence: request.kind };
40
+ return { evidence };
38
41
  const leash = await acquireLeash(paths, signal === undefined ? {} : { signal });
39
- return leash === null ? { evidence: "unavailable" } : { evidence: request.kind, leash };
42
+ return leash === null ? { evidence: "unavailable" } : { evidence, leash };
40
43
  }
41
44
  const target = request.body;
42
45
  const waited = await takeLeashUntilSignal(paths, target.sequence, signal);
@@ -5,10 +5,22 @@ import { type AkuId } from "./identity.js";
5
5
  import { type ActivityHistory } from "./projection.js";
6
6
  import type { Settings } from "../settings.js";
7
7
  import type { WorldRoot } from "../world.js";
8
- import { type Schema } from "./schema.js";
8
+ import { type Schema, type StandardSchemaV1 } from "./schema.js";
9
9
  export type AkumaIdleOptions = Readonly<{
10
10
  timeoutMs?: number;
11
11
  }>;
12
+ export type AkumaIdleResult = Readonly<{
13
+ kind: "idle";
14
+ status: AkumaStatus;
15
+ reason: Exclude<AkumaStatus["life"], "running">;
16
+ }> | Readonly<{
17
+ kind: "timeout";
18
+ status: AkumaStatus;
19
+ reason: Readonly<{
20
+ running: boolean;
21
+ pendingTell: boolean;
22
+ }>;
23
+ }>;
12
24
  export type AkumaHistoryOptions = Readonly<{
13
25
  before?: number;
14
26
  since?: number;
@@ -26,7 +38,7 @@ export type AkumaBirthInput = Readonly<{
26
38
  allowed?: readonly AllowedAction[];
27
39
  }>;
28
40
  export type AkumaTellOptions<T> = Readonly<{
29
- schema: Schema<T>;
41
+ schema: Schema<T> | StandardSchemaV1<T>;
30
42
  interrupt?: boolean;
31
43
  initiator?: string;
32
44
  }>;
@@ -45,7 +57,7 @@ export declare class Akuma {
45
57
  interrupt(text: string, options?: AkumaSignalOptions & Readonly<{
46
58
  initiator?: string;
47
59
  }>): Promise<InterruptReceipt>;
48
- idle(options?: AkumaIdleOptions): Promise<void>;
60
+ idle(options?: AkumaIdleOptions): Promise<AkumaIdleResult>;
49
61
  history(options?: AkumaHistoryOptions): Promise<ActivityHistory>;
50
62
  kill(options?: AkumaSignalOptions): Promise<KillEvidence>;
51
63
  }
@@ -12,7 +12,7 @@ import { parseAkuId, pathsForAkuId } from "./identity.js";
12
12
  import { birthAkuma, launchAkuma } from "./publication.js";
13
13
  import { projectTurns, selectHistory } from "./projection.js";
14
14
  import { settings as readSettings } from "../settings.js";
15
- import { schemaJsonText } from "./schema.js";
15
+ import { schemaFromStandard, schemaJsonText } from "./schema.js";
16
16
  import { abortable } from "./abort.js";
17
17
  const HISTORY_LIMIT = 12;
18
18
  function signalOption(value) {
@@ -25,6 +25,15 @@ function signalOption(value) {
25
25
  function wait(milliseconds) {
26
26
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
27
27
  }
28
+ function pendingTellOf(status) {
29
+ return status.timeline.entries.some((entry) => entry.kind === "row" && entry.row.kind === "tell" && entry.row.state === "pending");
30
+ }
31
+ function idleResult(status) {
32
+ const pendingTell = pendingTellOf(status);
33
+ if (status.life !== "running" && !pendingTell)
34
+ return { kind: "idle", status, reason: status.life };
35
+ return { kind: "timeout", status, reason: { running: status.life === "running", pendingTell } };
36
+ }
28
37
  function recordedTell(result) {
29
38
  if (result.wake.kind === "failed")
30
39
  throw new AkumaProviderError(result.wake.diagnostic);
@@ -35,12 +44,12 @@ async function recordPlainTell(id, root, body, tellId, initiator) {
35
44
  return recordedTell(admitted);
36
45
  }
37
46
  async function recordSchemaTell(input) {
38
- const { id, body, tellId, options, root } = input;
39
- if (options.interrupt === true) {
47
+ const { id, body, tellId, schema, root } = input;
48
+ if (input.interrupt === true) {
40
49
  const interrupted = await new AkumaHandle(id, root).interrupt(body, {
41
50
  tellId,
42
- schemaJson: schemaJsonText(options.schema),
43
- ...(options.initiator === undefined ? {} : { initiator: options.initiator }),
51
+ schemaJson: schemaJsonText(schema),
52
+ ...(input.initiator === undefined ? {} : { initiator: input.initiator }),
44
53
  });
45
54
  if (interrupted.kind === "unavailable") {
46
55
  throw new AkumaProviderError(`schema interrupt unavailable: ${interrupted.evidence}`);
@@ -48,8 +57,8 @@ async function recordSchemaTell(input) {
48
57
  return recordedTell(interrupted.tell);
49
58
  }
50
59
  const admitted = await new AkumaHandle(id, root).tell(body, tellId, undefined, undefined, {
51
- schemaJson: schemaJsonText(options.schema),
52
- ...(options.initiator === undefined ? {} : { initiator: options.initiator }),
60
+ schemaJson: schemaJsonText(schema),
61
+ ...(input.initiator === undefined ? {} : { initiator: input.initiator }),
53
62
  });
54
63
  return recordedTell(admitted);
55
64
  }
@@ -135,19 +144,23 @@ export class Akuma {
135
144
  if (typeof text !== "string")
136
145
  throw new TypeError("Akuma tell text must be a string");
137
146
  const tellId = randomUUID();
138
- const recorded = options === undefined || !("schema" in options)
147
+ const schemaOptions = options !== undefined && "schema" in options ? options : undefined;
148
+ const schema = schemaOptions === undefined ? undefined : schemaFromStandard(schemaOptions.schema);
149
+ const recorded = schemaOptions === undefined || schema === undefined
139
150
  ? await recordPlainTell(this.id, this.root, text, tellId, options?.initiator)
140
151
  : await recordSchemaTell({
141
152
  id: this.id,
142
153
  body: text,
143
154
  tellId,
144
- options,
155
+ schema,
145
156
  root: this.root,
157
+ ...(schemaOptions.interrupt === undefined ? {} : { interrupt: schemaOptions.interrupt }),
158
+ ...(schemaOptions.initiator === undefined ? {} : { initiator: schemaOptions.initiator }),
146
159
  });
147
160
  const outcome = await awaitTellOutcome(this.paths, recorded.tellId);
148
161
  if (outcome.kind !== "answered")
149
162
  outcomeError(outcome);
150
- if (options === undefined || !("schema" in options))
163
+ if (schema === undefined)
151
164
  return outcome.answer;
152
165
  const raw = outcome.answerJson ?? outcome.answer;
153
166
  let parsed;
@@ -158,7 +171,7 @@ export class Akuma {
158
171
  throw new AkumaDecodeError(error instanceof Error ? error.message : "Answer is not valid JSON", outcome.answer);
159
172
  }
160
173
  try {
161
- return options.schema.decode(parsed);
174
+ return schema.decode(parsed);
162
175
  }
163
176
  catch (error) {
164
177
  throw new AkumaDecodeError(error instanceof Error ? error.message : "Answer failed schema decode", outcome.answer);
@@ -194,8 +207,9 @@ export class Akuma {
194
207
  const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
195
208
  for (;;) {
196
209
  const observed = await bornStatus(this.paths, this.id, { aperture: "monitoring" });
197
- if (defaultWaitComplete(observed.status) || (deadline !== undefined && performance.now() >= deadline))
198
- return;
210
+ if (defaultWaitComplete(observed.status) || (deadline !== undefined && performance.now() >= deadline)) {
211
+ return idleResult(observed.status);
212
+ }
199
213
  await wait(deadline === undefined ? POLL_MS : Math.min(POLL_MS, Math.max(0, deadline - performance.now())));
200
214
  }
201
215
  }
@@ -427,6 +427,7 @@ export declare const akumaStatusSchema: z.ZodObject<{
427
427
  export type AkumaStatus = z.infer<typeof akumaStatusSchema>;
428
428
  export declare function parseAkumaStatus(value: unknown): AkumaStatus;
429
429
  export { defaultWaitComplete } from "./akuma-observe.js";
430
+ export { withoutReportedChanges } from "./projection.js";
430
431
  export type { ReadonlyRestraint } from "./provider-recipe.js";
431
432
  export type * from "./projection.js";
432
433
  export type UnbornAkumaListRow = Readonly<{
@@ -36,6 +36,7 @@ export function parseAkumaStatus(value) {
36
36
  return akumaStatusSchema.parse(value);
37
37
  }
38
38
  export { defaultWaitComplete } from "./akuma-observe.js";
39
+ export { withoutReportedChanges } from "./projection.js";
39
40
  export { AkumaNotBornError } from "./akuma-errors.js";
40
41
  export function wait(milliseconds) {
41
42
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
@@ -30,6 +30,8 @@ export type WaitExecutionInput = Readonly<{
30
30
  signal?: AbortSignal;
31
31
  /** Resolves one Akuma's identity facts; consulted once per Akuma a viewer first sees. */
32
32
  identity?: (id: AkumaStatus["id"]) => Promise<WaitIdentityFacts>;
33
+ /** The selected set in the caller's order; a viewer's head and scoreboard follow it. */
34
+ selectionOrder?: readonly AkumaStatus["id"][];
33
35
  /** Reports the frozen selected set before the first round, so a viewer can fix its layout. */
34
36
  onSelected?: (selected: readonly WaitSelectedAkuma[]) => void;
35
37
  /** Reports every observation round to a live viewer; absent keeps the cheap completion probe. */
@@ -87,20 +87,37 @@ function delay(milliseconds, signal) {
87
87
  signal?.addEventListener("abort", abort, { once: true });
88
88
  });
89
89
  }
90
+ /** Canonical ids in the caller's selection order, appending any id the caller never named. */
91
+ function selectionOrderedIds(ids, order) {
92
+ const present = new Set(ids);
93
+ const seen = new Set();
94
+ const ordered = [];
95
+ for (const id of order) {
96
+ if (present.has(id) && !seen.has(id)) {
97
+ seen.add(id);
98
+ ordered.push(id);
99
+ }
100
+ }
101
+ for (const id of ids) {
102
+ if (!seen.has(id)) {
103
+ seen.add(id);
104
+ ordered.push(id);
105
+ }
106
+ }
107
+ return ordered;
108
+ }
90
109
  export async function executeWaitAkuma(input) {
91
110
  const deadline = input.timeoutMs === undefined ? undefined : performance.now() + input.timeoutMs;
92
111
  // Identity facts are read once per observed Akuma, not once per 100ms round.
93
112
  const facts = new Map();
94
113
  if (input.onSelected !== undefined) {
95
- const selected = [];
96
114
  for (const id of input.ids) {
97
115
  input.signal?.throwIfAborted();
98
116
  const known = input.identity === undefined ? undefined : await input.identity(id);
99
- const resolved = known ?? { contract: NO_DISPATCH_ASSOCIATION };
100
- facts.set(id, resolved);
101
- selected.push({ id, ...resolved });
117
+ facts.set(id, known ?? { contract: NO_DISPATCH_ASSOCIATION });
102
118
  }
103
- input.onSelected(selected);
119
+ const ordered = input.selectionOrder === undefined ? input.ids : selectionOrderedIds(input.ids, input.selectionOrder);
120
+ input.onSelected(ordered.map((id) => ({ id, ...facts.get(id) })));
104
121
  }
105
122
  const observeRound = async (statuses) => {
106
123
  if (input.observe === undefined)
@@ -55,6 +55,9 @@ export declare function readKill(paths: AkumaPaths, bodySequence: number): Promi
55
55
  export declare function requestStop(paths: AkumaPaths, at: string, signal?: AbortSignal): Promise<Readonly<{
56
56
  kind: "requested";
57
57
  body: BodyFact;
58
+ }> | Readonly<{
59
+ kind: "witnessed";
60
+ body: BodyFact;
58
61
  }> | Readonly<{
59
62
  kind: "already-killed" | "already-stopped";
60
63
  body: BodyFact;
@@ -1,5 +1,5 @@
1
1
  import { parsePublicHistoryId } from "../identity.js";
2
- import { answeredTurnFact, endBodyFact, finishBodyFact, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertPauseControl, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, latestKillFact, latestSessionFact, lastAnsweredTurnFact, pauseFact, sessionFactForCoordinate, stopFact, turnFact, } from "./rows.js";
2
+ import { answeredTurnFact, endBodyFact, finishBodyFact, deleteStopControl, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertPauseControl, insertKillFact, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, latestKillFact, latestSessionFact, lastAnsweredTurnFact, pauseFact, sessionFactForCoordinate, stopFact, turnFact, } from "./rows.js";
3
3
  import { insertTellDeliveryFact, dispositionSnapshotProven, insertTellBindingFact, insertTellDispositionSnapshot, insertTellFact, insertTellReceiptFact, insertUndeliveredTellReceipts, latestOpenTellDisposition, openBoundTurns, openTellDispositionIds, pendingTellFacts, hasPendingTell, resolveTellDispositionSnapshot, tellDispositionResolved, tellFact, tellIdsForFence, } from "./tells.js";
4
4
  import { activityFactSlice, statusFacts, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
5
5
  import { isHeartAbsent, readSealFromLeash, withHeartTransaction, withReadOnlyHeart } from "./storage.js";
@@ -180,8 +180,18 @@ export async function requestStop(paths, at, signal) {
180
180
  throw new Error("Akuma has no Body to kill");
181
181
  if (latestKillFact(heart)?.bodySequence === body.sequence)
182
182
  return { kind: "already-killed", body };
183
- if (body.end !== undefined)
183
+ if (body.end !== undefined) {
184
+ // A stranded Body is already explicitly settled: kill witnesses it in
185
+ // place rather than requesting a stop from a dead process. A normally
186
+ // exited Body remains already-stopped, and hung custody is untouched.
187
+ if (body.hung === undefined && body.end !== "exited") {
188
+ insertKillFact(heart, body.sequence, at);
189
+ if (stopFact(heart)?.bodySequence === body.sequence)
190
+ deleteStopControl(heart);
191
+ return { kind: "witnessed", body };
192
+ }
184
193
  return { kind: "already-stopped", body };
194
+ }
185
195
  const existing = stopFact(heart);
186
196
  if (existing !== null && existing.bodySequence !== body.sequence) {
187
197
  throw new Error("Akuma stop target is not the latest Body");
@@ -1,8 +1,8 @@
1
1
  export { Akuma } from "./akuma-instance.js";
2
- export type { AkumaBirthInput, AkumaHistoryOptions, AkumaIdleOptions, AkumaSignalOptions, AkumaTellOptions, } from "./akuma-instance.js";
2
+ export type { AkumaBirthInput, AkumaHistoryOptions, AkumaIdleOptions, AkumaIdleResult, AkumaSignalOptions, AkumaTellOptions, } from "./akuma-instance.js";
3
3
  export type { InterruptReceipt, KillEvidence } from "./akuma.js";
4
4
  export { Schema } from "./schema.js";
5
- export type { JsonSchema, JsonSchemaDocument } from "./schema.js";
5
+ export type { JsonSchema, JsonSchemaDocument, StandardSchemaV1, SchemaLike } from "./schema.js";
6
6
  export type { AkuId } from "./identity.js";
7
7
  export type { AkumaStatus } from "./akuma.js";
8
8
  export type { ActivityHistory, ActivityRow } from "./projection.js";
@@ -1,4 +1,6 @@
1
1
  import type { ActivitySnapshot, TurnLedger } from "./projection.js";
2
+ /** Present the same snapshot without its reported-change summary. */
3
+ export declare function withoutReportedChanges(snapshot: ActivitySnapshot): ActivitySnapshot;
2
4
  import type { HistoryCursor, HistoryPage } from "./projection.js";
3
5
  export declare function ordinarySnapshotBudget(ordinaryBudget?: number): Readonly<{
4
6
  tail: number;
@@ -2,6 +2,10 @@ const DEFAULT_TAIL = 3;
2
2
  const DEFAULT_VOICE = 3;
3
3
  const REPORTED_CHANGE_LIMIT = 5;
4
4
  const EMPTY_REPORTED = { reportedChanges: [], reportedChangesOmitted: 0 };
5
+ /** Present the same snapshot without its reported-change summary. */
6
+ export function withoutReportedChanges(snapshot) {
7
+ return { ...snapshot, reportedChanges: [], reportedChangesOmitted: 0 };
8
+ }
5
9
  function assembleEntries(ledger, window, selected) {
6
10
  const windowRows = new Set(window);
7
11
  const result = [];
@@ -958,4 +958,4 @@ export type TurnLedger = Readonly<{
958
958
  }>;
959
959
  /** Fold the retained fact timeline into Turn-owned rows without inventing provider facts. */
960
960
  export declare function projectTurns(facts: readonly TimelineFact[], retained?: RetainedWindow): TurnLedger;
961
- export { ordinarySnapshotBudget, selectHistory, selectSnapshot } from "./projection-read.js";
961
+ export { ordinarySnapshotBudget, selectHistory, selectSnapshot, withoutReportedChanges } from "./projection-read.js";
@@ -406,4 +406,4 @@ export function projectTurns(facts, retained = {
406
406
  }
407
407
  return finishLedger(state, retained);
408
408
  }
409
- export { ordinarySnapshotBudget, selectHistory, selectSnapshot } from "./projection-read.js";
409
+ export { ordinarySnapshotBudget, selectHistory, selectSnapshot, withoutReportedChanges } from "./projection-read.js";
@@ -3,12 +3,52 @@ export type JsonSchemaDocument = Readonly<{
3
3
  readonly [key: string]: unknown;
4
4
  }>;
5
5
  export type JsonSchema = JsonSchemaDocument;
6
+ export type StandardResult<Output> = Readonly<{
7
+ readonly value: Output;
8
+ readonly issues?: undefined;
9
+ }> | Readonly<{
10
+ readonly issues: readonly Readonly<{
11
+ readonly message: string;
12
+ }>[];
13
+ }>;
14
+ /** Structural Standard Schema v1 marker; no runtime dependency on a library. */
15
+ export type StandardSchemaV1<Output = unknown> = Readonly<{
16
+ readonly "~standard": Readonly<{
17
+ readonly version: 1;
18
+ readonly vendor: string;
19
+ readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
20
+ readonly types?: Readonly<{
21
+ readonly input: unknown;
22
+ readonly output: Output;
23
+ }> | undefined;
24
+ readonly jsonSchema?: Readonly<{
25
+ readonly output: (options: Readonly<{
26
+ readonly target: string;
27
+ }>) => unknown;
28
+ }> | undefined;
29
+ }>;
30
+ }>;
31
+ /** The schema forms a Tell answer contract accepts. */
32
+ export type SchemaLike<T> = Schema<T> | StandardSchemaV1<T>;
6
33
  export declare class Schema<T> {
7
34
  readonly jsonSchema: JsonSchemaDocument;
8
35
  readonly decode: (value: unknown) => T;
9
36
  private constructor();
10
37
  static zod<Output>(schema: ZodType<Output>): Schema<Output>;
11
38
  static json<Output>(schema: JsonSchemaDocument, decode: (value: unknown) => Output): Schema<Output>;
39
+ /**
40
+ * Bless a Standard Schema v1 value as a real Schema: validate the marker,
41
+ * project a JSON Schema document, refuse shapes outside the simple
42
+ * vocabulary, and decode at the boundary. The class owns this path so every
43
+ * normalized schema is born through the constructor rather than around it.
44
+ */
45
+ static standard<Output>(value: StandardSchemaV1<Output>): Schema<Output>;
12
46
  }
47
+ /**
48
+ * Normalize a Tell answer contract to the internal Schema. The package's own
49
+ * Schema passes through; anything else delegates to the class's own blessing
50
+ * path, so inferred output types keep flowing from a genuine Schema instance.
51
+ */
52
+ export declare function schemaFromStandard<T>(value: SchemaLike<T>): Schema<T>;
13
53
  /** Internal neutral serialization for Heart/provider forwarding. */
14
54
  export declare function schemaJsonText(schema: Schema<unknown>): string;
@@ -1,5 +1,25 @@
1
1
  import { toJSONSchema } from "zod";
2
2
  const SCHEMA_JSON_MAX_BYTES = 65_536;
3
+ /**
4
+ * Keywords a provider answer contract may carry. Provider failures from
5
+ * fragile constraints are expensive to diagnose, so the seam refuses any
6
+ * projected document that steps outside simple JSON shape vocabulary.
7
+ */
8
+ const SIMPLE_SCHEMA_KEYWORDS = new Set([
9
+ "type",
10
+ "properties",
11
+ "required",
12
+ "items",
13
+ "enum",
14
+ "const",
15
+ "anyOf",
16
+ "additionalProperties",
17
+ "description",
18
+ "title",
19
+ "$schema",
20
+ "$defs",
21
+ "$ref",
22
+ ]);
3
23
  function isPlainObject(value) {
4
24
  if (typeof value !== "object" || value === null || Array.isArray(value))
5
25
  return false;
@@ -26,6 +46,38 @@ function assertJsonValue(value, path) {
26
46
  }
27
47
  throw new TypeError(`${path} contains a non-JSON value`);
28
48
  }
49
+ /**
50
+ * Walk every subschema position and refuse a keyword outside the blessed shape
51
+ * vocabulary. Property names and enum/const data are not keywords, so their
52
+ * keys are never judged.
53
+ */
54
+ function assertSimpleSchema(value, path) {
55
+ if (!isPlainObject(value))
56
+ throw new TypeError(`${path} must be a JSON Schema object`);
57
+ for (const [key, entry] of Object.entries(value)) {
58
+ if (!SIMPLE_SCHEMA_KEYWORDS.has(key)) {
59
+ throw new TypeError(`provider answer contract uses unsupported JSON Schema keyword "${key}" at ${path}; ` +
60
+ "provider answer contracts carry simple shapes only");
61
+ }
62
+ if (key === "properties" || key === "$defs") {
63
+ for (const [name, subschema] of Object.entries(entry)) {
64
+ assertSimpleSchema(subschema, `${path}.${key}.${name}`);
65
+ }
66
+ }
67
+ else if (key === "items") {
68
+ if (Array.isArray(entry))
69
+ entry.forEach((subschema, index) => assertSimpleSchema(subschema, `${path}.items[${index}]`));
70
+ else
71
+ assertSimpleSchema(entry, `${path}.items`);
72
+ }
73
+ else if (key === "anyOf") {
74
+ entry.forEach((subschema, index) => assertSimpleSchema(subschema, `${path}.anyOf[${index}]`));
75
+ }
76
+ else if (key === "additionalProperties" && typeof entry !== "boolean") {
77
+ assertSimpleSchema(entry, `${path}.additionalProperties`);
78
+ }
79
+ }
80
+ }
29
81
  function sortValue(value) {
30
82
  if (Array.isArray(value))
31
83
  return value.map(sortValue);
@@ -62,6 +114,44 @@ function canonicalDocument(value, label) {
62
114
  }
63
115
  return { json: freezeValue(JSON.parse(jsonText)), jsonText };
64
116
  }
117
+ function standardMarker(value) {
118
+ const marker = typeof value === "object" && value !== null ? value["~standard"] : undefined;
119
+ if (typeof marker !== "object" ||
120
+ marker === null ||
121
+ marker.version !== 1 ||
122
+ typeof marker.vendor !== "string" ||
123
+ typeof marker.validate !== "function") {
124
+ throw new TypeError("schema must be a Schema or a Standard Schema v1 value");
125
+ }
126
+ return marker;
127
+ }
128
+ /** Project a Standard Schema value to a JSON Schema document, or refuse honestly. */
129
+ function projectStandardSchema(value, marker) {
130
+ if (marker.vendor === "zod") {
131
+ return toJSONSchema(value, { target: "draft-07", unrepresentable: "throw", cycles: "throw" });
132
+ }
133
+ const converter = marker.jsonSchema;
134
+ if (converter !== undefined && typeof converter.output === "function") {
135
+ return converter.output({ target: "draft-07" });
136
+ }
137
+ const method = value.toJSONSchema;
138
+ if (typeof method === "function") {
139
+ return method.call(value, { target: "draft-07" });
140
+ }
141
+ throw new TypeError(`schema vendor "${marker.vendor}" does not carry a JSON Schema projection; ` +
142
+ "pass a JSON Schema document and decoder to Schema.json instead");
143
+ }
144
+ function decodeStandard(value, marker) {
145
+ const result = marker.validate(value);
146
+ if (result instanceof Promise || (typeof result === "object" && result !== null && "then" in result)) {
147
+ throw new TypeError(`schema vendor "${marker.vendor}" validates asynchronously`);
148
+ }
149
+ if ("issues" in result && result.issues !== undefined) {
150
+ const detail = result.issues.map((issue) => issue.message).join("; ");
151
+ throw new Error(detail.length === 0 ? "Standard Schema validation failed" : detail);
152
+ }
153
+ return result.value;
154
+ }
65
155
  export class Schema {
66
156
  jsonSchema;
67
157
  decode;
@@ -72,6 +162,7 @@ export class Schema {
72
162
  }
73
163
  static zod(schema) {
74
164
  const payload = toJSONSchema(schema, { target: "draft-07", unrepresentable: "throw", cycles: "throw" });
165
+ assertSimpleSchema(payload, "$");
75
166
  const canonical = canonicalDocument(payload, "Zod JSON Schema");
76
167
  return new Schema(canonical.json, (value) => schema.parse(value));
77
168
  }
@@ -82,6 +173,29 @@ export class Schema {
82
173
  const canonical = canonicalDocument(document, "JSON Schema");
83
174
  return new Schema(canonical.json, decode);
84
175
  }
176
+ /**
177
+ * Bless a Standard Schema v1 value as a real Schema: validate the marker,
178
+ * project a JSON Schema document, refuse shapes outside the simple
179
+ * vocabulary, and decode at the boundary. The class owns this path so every
180
+ * normalized schema is born through the constructor rather than around it.
181
+ */
182
+ static standard(value) {
183
+ const marker = standardMarker(value);
184
+ const payload = projectStandardSchema(value, marker);
185
+ assertSimpleSchema(payload, "$");
186
+ const canonical = canonicalDocument(payload, `${marker.vendor} JSON Schema`);
187
+ return new Schema(canonical.json, (input) => decodeStandard(input, marker));
188
+ }
189
+ }
190
+ /**
191
+ * Normalize a Tell answer contract to the internal Schema. The package's own
192
+ * Schema passes through; anything else delegates to the class's own blessing
193
+ * path, so inferred output types keep flowing from a genuine Schema instance.
194
+ */
195
+ export function schemaFromStandard(value) {
196
+ if (value instanceof Schema)
197
+ return value;
198
+ return Schema.standard(value);
85
199
  }
86
200
  /** Internal neutral serialization for Heart/provider forwarding. */
87
201
  export function schemaJsonText(schema) {