@driftengine/ai 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +103 -0
  4. package/dist/adapters/local.d.ts +29 -0
  5. package/dist/adapters/local.js +24 -0
  6. package/dist/adapters/proxy.d.ts +28 -0
  7. package/dist/adapters/proxy.js +138 -0
  8. package/dist/bridges/authority.d.ts +153 -0
  9. package/dist/bridges/authority.js +179 -0
  10. package/dist/bridges/navigation.d.ts +100 -0
  11. package/dist/bridges/navigation.js +139 -0
  12. package/dist/budget/budget.d.ts +34 -0
  13. package/dist/budget/budget.js +57 -0
  14. package/dist/command/apply.d.ts +24 -0
  15. package/dist/command/apply.js +40 -0
  16. package/dist/command/log.d.ts +55 -0
  17. package/dist/command/log.js +50 -0
  18. package/dist/context/assemble.d.ts +48 -0
  19. package/dist/context/assemble.js +55 -0
  20. package/dist/context/continuation.d.ts +14 -0
  21. package/dist/context/continuation.js +36 -0
  22. package/dist/describe/manifest.d.ts +70 -0
  23. package/dist/describe/manifest.js +99 -0
  24. package/dist/entities/context.d.ts +52 -0
  25. package/dist/entities/context.js +83 -0
  26. package/dist/index.d.ts +61 -0
  27. package/dist/index.js +40 -0
  28. package/dist/policy/types.d.ts +55 -0
  29. package/dist/policy/types.js +26 -0
  30. package/dist/policy/utility.d.ts +18 -0
  31. package/dist/policy/utility.js +47 -0
  32. package/dist/provider/create.d.ts +16 -0
  33. package/dist/provider/create.js +57 -0
  34. package/dist/provider/latency.d.ts +27 -0
  35. package/dist/provider/latency.js +52 -0
  36. package/dist/provider/types.d.ts +90 -0
  37. package/dist/provider/types.js +8 -0
  38. package/dist/realtime/session.d.ts +35 -0
  39. package/dist/realtime/session.js +34 -0
  40. package/dist/session/agent.d.ts +217 -0
  41. package/dist/session/agent.js +506 -0
  42. package/dist/session/replay.d.ts +32 -0
  43. package/dist/session/replay.js +81 -0
  44. package/dist/session/states.d.ts +28 -0
  45. package/dist/session/states.js +33 -0
  46. package/dist/session/usage.d.ts +43 -0
  47. package/dist/session/usage.js +38 -0
  48. package/dist/testing/deterministic.d.ts +65 -0
  49. package/dist/testing/deterministic.js +150 -0
  50. package/dist/tools/policy.d.ts +47 -0
  51. package/dist/tools/policy.js +84 -0
  52. package/dist/tools/registry.d.ts +69 -0
  53. package/dist/tools/registry.js +75 -0
  54. package/dist/tools/validate.d.ts +24 -0
  55. package/dist/tools/validate.js +80 -0
  56. package/package.json +59 -0
  57. package/src/adapters/local.ts +64 -0
  58. package/src/adapters/proxy.ts +187 -0
  59. package/src/bridges/authority.ts +244 -0
  60. package/src/bridges/navigation.ts +207 -0
  61. package/src/budget/budget.ts +73 -0
  62. package/src/command/apply.ts +52 -0
  63. package/src/command/log.ts +81 -0
  64. package/src/context/assemble.ts +104 -0
  65. package/src/context/continuation.ts +39 -0
  66. package/src/describe/manifest.ts +148 -0
  67. package/src/entities/context.ts +112 -0
  68. package/src/index.ts +94 -0
  69. package/src/policy/types.ts +70 -0
  70. package/src/policy/utility.ts +53 -0
  71. package/src/provider/create.ts +70 -0
  72. package/src/provider/latency.ts +57 -0
  73. package/src/provider/types.ts +96 -0
  74. package/src/realtime/session.ts +63 -0
  75. package/src/session/agent.ts +622 -0
  76. package/src/session/replay.ts +96 -0
  77. package/src/session/states.ts +63 -0
  78. package/src/session/usage.ts +66 -0
  79. package/src/testing/deterministic.ts +204 -0
  80. package/src/tools/policy.ts +114 -0
  81. package/src/tools/registry.ts +122 -0
  82. package/src/tools/validate.ts +92 -0
@@ -0,0 +1,24 @@
1
+ import type { ToolRegistry } from '../tools/registry.ts';
2
+ import type { AiCommand } from './log.ts';
3
+ export type ApplyOutcome = {
4
+ readonly ok: true;
5
+ readonly result: unknown;
6
+ } | {
7
+ readonly ok: false;
8
+ readonly reason: string;
9
+ };
10
+ /**
11
+ * Run an accepted command at a tick boundary, revalidating on the way in.
12
+ *
13
+ * The tool's own guard ran once already, when the buffered intent drained. It runs
14
+ * **again** here, because those are two different moments and a snapshot is never
15
+ * authority — the buffer widens the gap on purpose, and admission proves the plan was
16
+ * true when it was taken up rather than that it is true now.
17
+ *
18
+ * Not the execution policy, which ran at acceptance. Running that twice would charge
19
+ * the rate limit twice for one call.
20
+ *
21
+ * **Returns on every path.** A caller inside `fixedUpdate` never needs a `try`, and a
22
+ * consumer's tool throwing must not become an exception halfway through a tick.
23
+ */
24
+ export declare function applyCommand<W>(registry: ToolRegistry<W>, world: W, command: AiCommand, _tick: number): ApplyOutcome;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Run an accepted command at a tick boundary, revalidating on the way in.
3
+ *
4
+ * The tool's own guard ran once already, when the buffered intent drained. It runs
5
+ * **again** here, because those are two different moments and a snapshot is never
6
+ * authority — the buffer widens the gap on purpose, and admission proves the plan was
7
+ * true when it was taken up rather than that it is true now.
8
+ *
9
+ * Not the execution policy, which ran at acceptance. Running that twice would charge
10
+ * the rate limit twice for one call.
11
+ *
12
+ * **Returns on every path.** A caller inside `fixedUpdate` never needs a `try`, and a
13
+ * consumer's tool throwing must not become an exception halfway through a tick.
14
+ */
15
+ export function applyCommand(registry, world, command, _tick) {
16
+ const tool = registry.get(command.toolId);
17
+ if (tool === undefined) {
18
+ return { ok: false, reason: `unknown tool "${command.toolId}" — it is not registered` };
19
+ }
20
+ let admitted;
21
+ try {
22
+ admitted = tool.admits(command.args, world);
23
+ }
24
+ catch {
25
+ return { ok: false, reason: `the guard for "${command.toolId}" threw, so the call is refused` };
26
+ }
27
+ if (!admitted) {
28
+ return {
29
+ ok: false,
30
+ reason: `"${command.toolId}" no longer admits: the world changed between acceptance and application`,
31
+ };
32
+ }
33
+ try {
34
+ return { ok: true, result: tool.execute(command.args, world) };
35
+ }
36
+ catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ return { ok: false, reason: `"${command.toolId}" failed while running: ${message}` };
39
+ }
40
+ }
@@ -0,0 +1,55 @@
1
+ export interface AiCommand {
2
+ readonly kind: 'command';
3
+ readonly toolId: string;
4
+ readonly args: unknown;
5
+ readonly agentId: string;
6
+ /** The tick the model was asked. */
7
+ readonly issuedAtTick: number;
8
+ /** The tick the command crossed into the simulation. */
9
+ readonly acceptedAtTick: number;
10
+ }
11
+ /**
12
+ * An observation that ended the current intent early.
13
+ *
14
+ * Recorded for the same reason a command is: it cannot be recomputed. The floor is
15
+ * deterministic and replays by rerunning, but an observation is *external input* —
16
+ * the same class of thing as a keypress — and nothing in the simulation derives it.
17
+ * Without this, a replay reruns the floor past the moment it was interrupted and
18
+ * diverges from there on.
19
+ */
20
+ export interface AiPreemption {
21
+ readonly kind: 'preemption';
22
+ readonly agentId: string;
23
+ readonly acceptedAtTick: number;
24
+ }
25
+ export type LogEntry = AiCommand | AiPreemption;
26
+ /**
27
+ * What was accepted, and when — beside `TickTrace` rather than inside it.
28
+ *
29
+ * `TickTrace` is `Float32Array` channels plus a `Uint8Array` of flags, sampled per
30
+ * tick. A command carries a string id and structured arguments, and boxing those into
31
+ * float channels is not a design, it is a defeat. This borrows the ring-buffer
32
+ * discipline and the tick indexing and shares nothing else, which answers the question
33
+ * §33 of the parent design left open for AI-2.
34
+ *
35
+ * *What it costs:* two ring buffers where a reader might expect one place to look.
36
+ * *What would make it wrong:* if a consumer needed one ordered stream of both, this
37
+ * would need a merge read rather than a merged store — the storage shapes have no
38
+ * common representation worth finding.
39
+ */
40
+ export declare class CommandLog {
41
+ private readonly entries;
42
+ private cursor;
43
+ private filled;
44
+ constructor(capacity?: number);
45
+ /** How many commands are still retained, not how many were ever recorded. */
46
+ get length(): number;
47
+ record(entry: LogEntry): void;
48
+ /**
49
+ * Commands accepted at exactly this tick, in acceptance order, written into `out`.
50
+ *
51
+ * Returns the count written. Reading a tick allocates nothing, because a replay
52
+ * reads every tick and a per-tick array would put an allocation on that path.
53
+ */
54
+ at(tick: number, out: LogEntry[]): number;
55
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What was accepted, and when — beside `TickTrace` rather than inside it.
3
+ *
4
+ * `TickTrace` is `Float32Array` channels plus a `Uint8Array` of flags, sampled per
5
+ * tick. A command carries a string id and structured arguments, and boxing those into
6
+ * float channels is not a design, it is a defeat. This borrows the ring-buffer
7
+ * discipline and the tick indexing and shares nothing else, which answers the question
8
+ * §33 of the parent design left open for AI-2.
9
+ *
10
+ * *What it costs:* two ring buffers where a reader might expect one place to look.
11
+ * *What would make it wrong:* if a consumer needed one ordered stream of both, this
12
+ * would need a merge read rather than a merged store — the storage shapes have no
13
+ * common representation worth finding.
14
+ */
15
+ export class CommandLog {
16
+ entries;
17
+ cursor = 0;
18
+ filled = 0;
19
+ constructor(capacity = 256) {
20
+ this.entries = new Array(Math.max(1, capacity | 0));
21
+ }
22
+ /** How many commands are still retained, not how many were ever recorded. */
23
+ get length() {
24
+ return this.filled;
25
+ }
26
+ record(entry) {
27
+ this.entries[this.cursor] = entry;
28
+ this.cursor = (this.cursor + 1) % this.entries.length;
29
+ if (this.filled < this.entries.length)
30
+ this.filled++;
31
+ }
32
+ /**
33
+ * Commands accepted at exactly this tick, in acceptance order, written into `out`.
34
+ *
35
+ * Returns the count written. Reading a tick allocates nothing, because a replay
36
+ * reads every tick and a per-tick array would put an allocation on that path.
37
+ */
38
+ at(tick, out) {
39
+ let written = 0;
40
+ const size = this.entries.length;
41
+ const start = this.filled < size ? 0 : this.cursor;
42
+ for (let i = 0; i < this.filled; i++) {
43
+ const entry = this.entries[(start + i) % size];
44
+ if (entry === undefined || entry.acceptedAtTick !== tick)
45
+ continue;
46
+ out[written++] = entry;
47
+ }
48
+ return written;
49
+ }
50
+ }
@@ -0,0 +1,48 @@
1
+ import type { ToolSchema } from '../tools/registry.ts';
2
+ /**
3
+ * A read-only snapshot the model is told about.
4
+ *
5
+ * Context and tools are separate surfaces and the separation improves behaviour:
6
+ * nearby objects are context, inspecting one in detail is a tool, moving is a tool.
7
+ */
8
+ export interface ContextProvider<T> {
9
+ /** Stable and versioned, like a tool id: `project.visible@5`. */
10
+ readonly id: string;
11
+ readonly priority: number;
12
+ readonly maxItems?: number;
13
+ describe(): {
14
+ readonly title: string;
15
+ readonly schema: ToolSchema;
16
+ };
17
+ sample(subject: string): T;
18
+ }
19
+ export interface ContextSection {
20
+ readonly id: string;
21
+ readonly title: string;
22
+ readonly items: unknown;
23
+ readonly estimatedTokens: number;
24
+ /** True when `maxItems` cut the sample, so a reader can tell short from truncated. */
25
+ readonly truncated: boolean;
26
+ }
27
+ export interface AssembledContext {
28
+ readonly capturedAtTick: number;
29
+ readonly sections: readonly ContextSection[];
30
+ readonly estimatedTokens: number;
31
+ /**
32
+ * Section ids left out, and why they are named rather than merely absent.
33
+ *
34
+ * A budget that silently truncates reads as coverage. A model told nothing about
35
+ * nearby objects behaves as though there are none, and a consumer reading a trace
36
+ * cannot tell that from a world that was empty.
37
+ */
38
+ readonly dropped: readonly string[];
39
+ }
40
+ /**
41
+ * Never send the scene graph.
42
+ *
43
+ * Sections are taken in descending priority until the budget is spent, and what did
44
+ * not fit is named. The estimate is deliberately crude — a provider-specific tokenizer
45
+ * may sharpen it, but requiring one would make the core abstraction depend on whichever
46
+ * provider happened to be configured.
47
+ */
48
+ export declare function assembleContext(providers: readonly ContextProvider<unknown>[], subject: string, tick: number, tokenBudget: number): AssembledContext;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Never send the scene graph.
3
+ *
4
+ * Sections are taken in descending priority until the budget is spent, and what did
5
+ * not fit is named. The estimate is deliberately crude — a provider-specific tokenizer
6
+ * may sharpen it, but requiring one would make the core abstraction depend on whichever
7
+ * provider happened to be configured.
8
+ */
9
+ export function assembleContext(providers, subject, tick, tokenBudget) {
10
+ const ordered = [...providers].sort((a, b) => b.priority - a.priority);
11
+ const sections = [];
12
+ const dropped = [];
13
+ let spent = 0;
14
+ for (const provider of ordered) {
15
+ let items;
16
+ let truncated = false;
17
+ try {
18
+ items = provider.sample(subject);
19
+ }
20
+ catch {
21
+ /* One bad context provider must not silence an agent. It is named in `dropped`,
22
+ which is the difference between a section that failed and one that was empty. */
23
+ dropped.push(provider.id);
24
+ continue;
25
+ }
26
+ const limit = provider.maxItems;
27
+ if (limit !== undefined && Array.isArray(items) && items.length > limit) {
28
+ items = items.slice(0, limit);
29
+ truncated = true;
30
+ }
31
+ const cost = estimateTokens(items);
32
+ if (spent + cost > tokenBudget) {
33
+ dropped.push(provider.id);
34
+ continue;
35
+ }
36
+ spent += cost;
37
+ sections.push({
38
+ id: provider.id,
39
+ title: provider.describe().title,
40
+ items,
41
+ estimatedTokens: cost,
42
+ truncated,
43
+ });
44
+ }
45
+ return { capturedAtTick: tick, sections, estimatedTokens: spent, dropped };
46
+ }
47
+ /** Four characters to a token, which is close enough to budget with and cheap to compute. */
48
+ function estimateTokens(value) {
49
+ try {
50
+ return Math.ceil(JSON.stringify(value ?? null).length / 4);
51
+ }
52
+ catch {
53
+ return 0;
54
+ }
55
+ }
@@ -0,0 +1,14 @@
1
+ import type { Intent } from '../policy/types.ts';
2
+ /**
3
+ * Tell the model when its answer will be used.
4
+ *
5
+ * Three lines. Without them the model answers as though acting immediately, proposes
6
+ * what is right now and wrong in a second and a half, and the lookahead becomes lag
7
+ * that is still paid for. The whole value of asking early is lost to not saying so.
8
+ *
9
+ * *What it costs:* three lines of every continuation's context budget. *What would
10
+ * make it wrong:* a provider that ignores framing of this kind entirely, at which
11
+ * point the lead should shrink toward zero rather than the preamble being dropped —
12
+ * the fix for a model that cannot reason about the future is to stop asking it to.
13
+ */
14
+ export declare function continuationPreamble(current: Intent, remainingMs: number): string;
@@ -0,0 +1,36 @@
1
+ import { hasKnownExtent } from '../policy/types.js';
2
+ /**
3
+ * Tell the model when its answer will be used.
4
+ *
5
+ * Three lines. Without them the model answers as though acting immediately, proposes
6
+ * what is right now and wrong in a second and a half, and the lookahead becomes lag
7
+ * that is still paid for. The whole value of asking early is lost to not saying so.
8
+ *
9
+ * *What it costs:* three lines of every continuation's context budget. *What would
10
+ * make it wrong:* a provider that ignores framing of this kind entirely, at which
11
+ * point the lead should shrink toward zero rather than the preamble being dropped —
12
+ * the fix for a model that cannot reason about the future is to stop asking it to.
13
+ */
14
+ export function continuationPreamble(current, remainingMs) {
15
+ const doing = describeIntent(current);
16
+ const when = hasKnownExtent(current)
17
+ ? `~${(Math.max(0, remainingMs) / 1000).toFixed(1)}s after this snapshot`
18
+ : 'unknown — this action has no predictable duration';
19
+ return [
20
+ `You are currently: ${doing}`,
21
+ `Expected to finish: ${when}`,
22
+ `Your answer applies: when that finishes, not now`,
23
+ ].join('\n');
24
+ }
25
+ /**
26
+ * What the agent is doing, in the model's terms.
27
+ *
28
+ * A floor intent is described exactly like a model intent. Telling the model the
29
+ * engine improvised would invite it to treat the current behaviour as provisional,
30
+ * and the floor's choice is as real as any other — it is what the agent is doing.
31
+ */
32
+ function describeIntent(intent) {
33
+ if (intent.toolIds.length === 0)
34
+ return `${intent.id} (no tool calls)`;
35
+ return `${intent.id} (${intent.toolIds.join(', ')})`;
36
+ }
@@ -0,0 +1,70 @@
1
+ import type { AiProvider } from '../provider/types.ts';
2
+ import type { ContextProvider } from '../context/assemble.ts';
3
+ import type { ToolRegistry, ToolSchema } from '../tools/registry.ts';
4
+ /**
5
+ * What an agent is, written down.
6
+ *
7
+ * Describes the *agent* rather than its connection: the tools and context are the
8
+ * agent's, and `providerId` is null when nothing is attached. An agent with no provider
9
+ * is still a fully described agent — it runs on its floor — and a manifest that
10
+ * required a connection would have nothing to say about the case this package was
11
+ * built to make ordinary.
12
+ */
13
+ export interface AiManifest {
14
+ readonly tools: readonly {
15
+ id: string;
16
+ description: string;
17
+ schema: ToolSchema;
18
+ }[];
19
+ readonly context: readonly {
20
+ id: string;
21
+ title: string;
22
+ schema: ToolSchema;
23
+ }[];
24
+ readonly providerId: string | null;
25
+ /** Effects a `@deterministic` function may still have, for a reader of a trace. */
26
+ readonly determinismBoundary: readonly string[];
27
+ /** Surfaces this engine refuses in writing, so a reader learns the shape of the hole. */
28
+ readonly refused: readonly {
29
+ id: string;
30
+ waitsOn: string;
31
+ }[];
32
+ }
33
+ export declare function describeAgent<W>(tools: ToolRegistry<W>, context: readonly ContextProvider<unknown>[], provider: AiProvider | null): AiManifest;
34
+ export type StructuredResult = {
35
+ readonly ok: true;
36
+ readonly value: unknown;
37
+ } | {
38
+ readonly ok: false;
39
+ readonly reason: string;
40
+ };
41
+ /**
42
+ * Ask for output shaped like a schema, or refuse before spending anything.
43
+ *
44
+ * A provider declaring no structured output is refused **before** the request. Sending
45
+ * it anyway and hoping the text parses is the silent-downgrade failure wearing a
46
+ * different hat: it works often enough to ship and fails on the input nobody tested.
47
+ */
48
+ export declare function requireStructuredOutput(provider: AiProvider | null, schema: ToolSchema, value: unknown): StructuredResult;
49
+ export interface DevelopmentManifest extends AiManifest {
50
+ /** What this package will not do, in the words a reader needs to stop asking. */
51
+ readonly refusals: readonly {
52
+ id: string;
53
+ waitsOn: string;
54
+ }[];
55
+ /** The properties everything else here exists to make cheap. */
56
+ readonly guarantees: readonly string[];
57
+ }
58
+ /**
59
+ * The manifest a coding assistant reads.
60
+ *
61
+ * Carries what the runtime manifest carries **plus the refusals**, because a
62
+ * description listing only what works teaches a reader to ask for the rest — and the
63
+ * answer arrives as a failure rather than as a sentence. The parent design's §43 asks
64
+ * for exactly this, and gives that reason.
65
+ *
66
+ * The refusal ids are the sentinel names in `docs/CAPABILITIES.md`, and a test asserts
67
+ * the two agree. Two descriptions of one thing will drift, and this is the pair that
68
+ * would.
69
+ */
70
+ export declare function describeForDevelopment<W>(tools: ToolRegistry<W>, context: readonly ContextProvider<unknown>[], provider: AiProvider | null): DevelopmentManifest;
@@ -0,0 +1,99 @@
1
+ import { validateArgs } from '../tools/validate.js';
2
+ const DETERMINISM_BOUNDARY = [
3
+ 'pure',
4
+ 'clock.read',
5
+ 'scene.read',
6
+ 'scene.write',
7
+ 'ecs.read',
8
+ 'ecs.write',
9
+ 'physics.read',
10
+ 'physics.write',
11
+ ];
12
+ /**
13
+ * The two bridges Track O refuses, named so a coding assistant learns them.
14
+ *
15
+ * A manifest that listed only what works teaches a reader to ask for the rest, and the
16
+ * answer arrives as a failure rather than as a sentence. `docs/CAPABILITIES.md` carries
17
+ * a sentinel for each, so the day either is built is the day this list is wrong and the
18
+ * suite says so.
19
+ */
20
+ /*
21
+ * **Empty since 2026-09-05, and the emptiness is the point rather than an oversight.**
22
+ *
23
+ * Both entries that were here — the navigation bridge and the network authority — were refused for
24
+ * reasons that stopped being true on 2026-09-03, and neither this list nor `CAPABILITIES.md` had
25
+ * re-read them. `navigationBridge` and `AuthoritativeAgent` are built, so a refusal naming either
26
+ * would be this file telling a model a capability is absent while a consumer registers it.
27
+ *
28
+ * An empty list is a supported state and not a broken one: `describeAgent` simply says nothing is
29
+ * refused. What must not happen is an entry outliving the thing it waited on, which is what the
30
+ * gate above catches and what both of these did for two days.
31
+ */
32
+ const REFUSED = [];
33
+ export function describeAgent(tools, context, provider) {
34
+ return {
35
+ tools: tools.ids().map((id) => {
36
+ const tool = tools.get(id);
37
+ return {
38
+ id,
39
+ description: tool?.description ?? '',
40
+ schema: tool?.schema ?? { kind: 'object', fields: {} },
41
+ };
42
+ }),
43
+ context: context.map((entry) => {
44
+ const described = entry.describe();
45
+ return { id: entry.id, title: described.title, schema: described.schema };
46
+ }),
47
+ providerId: provider?.id ?? null,
48
+ determinismBoundary: DETERMINISM_BOUNDARY,
49
+ refused: REFUSED,
50
+ };
51
+ }
52
+ /**
53
+ * Ask for output shaped like a schema, or refuse before spending anything.
54
+ *
55
+ * A provider declaring no structured output is refused **before** the request. Sending
56
+ * it anyway and hoping the text parses is the silent-downgrade failure wearing a
57
+ * different hat: it works often enough to ship and fails on the input nobody tested.
58
+ */
59
+ export function requireStructuredOutput(provider, schema, value) {
60
+ if (provider === null) {
61
+ return { ok: false, reason: 'no provider is attached, so nothing can be asked for' };
62
+ }
63
+ if (!provider.capabilities.structuredOutput) {
64
+ return {
65
+ ok: false,
66
+ reason: `provider "${provider.id}" declares no structured output — asking anyway and parsing the text is a downgrade, not a fallback`,
67
+ };
68
+ }
69
+ const validation = validateArgs(schema, value);
70
+ if (validation.ok)
71
+ return { ok: true, value: validation.value };
72
+ return { ok: false, reason: `${validation.path}: ${validation.reason}` };
73
+ }
74
+ /**
75
+ * The manifest a coding assistant reads.
76
+ *
77
+ * Carries what the runtime manifest carries **plus the refusals**, because a
78
+ * description listing only what works teaches a reader to ask for the rest — and the
79
+ * answer arrives as a failure rather than as a sentence. The parent design's §43 asks
80
+ * for exactly this, and gives that reason.
81
+ *
82
+ * The refusal ids are the sentinel names in `docs/CAPABILITIES.md`, and a test asserts
83
+ * the two agree. Two descriptions of one thing will drift, and this is the pair that
84
+ * would.
85
+ */
86
+ export function describeForDevelopment(tools, context, provider) {
87
+ const manifest = describeAgent(tools, context, provider);
88
+ return {
89
+ ...manifest,
90
+ refusals: REFUSED,
91
+ guarantees: [
92
+ 'an agent is never without a current intent, whether or not a provider is attached',
93
+ 'exactly one provider request is in flight per agent',
94
+ 'a buffered intent is revalidated at the drain and discarded, never deferred',
95
+ 'budget exhaustion degrades to the policy floor and throws nothing',
96
+ 'accepted commands and preemptions are recorded, so a run replays exactly',
97
+ ],
98
+ };
99
+ }
@@ -0,0 +1,52 @@
1
+ import { type Entity, type World } from '@driftengine/entities';
2
+ import type { ComponentType } from '@driftengine/entities';
3
+ import type { ContextProvider } from '../context/assemble.ts';
4
+ import type { ToolDefinition } from '../tools/registry.ts';
5
+ /**
6
+ * Context and tools over an entity world.
7
+ *
8
+ * This is the half of AI-6 that links. The other half — a bridge that moves something
9
+ * along a path — is refused in writing, because nothing pathfinds and `drift/navigation`
10
+ * waits on no track at all. A seam with no implementation behind it is what R1 withdrew.
11
+ */
12
+ /** A stable, printable name for an entity, and the only form a model ever sees. */
13
+ export declare function entityRef(entity: Entity): string;
14
+ /**
15
+ * Read a reference back, or `null` when it is not one.
16
+ *
17
+ * A model inventing an identifier is ordinary rather than exceptional — the parent
18
+ * design's identity constraints say so in words — so this returns rather than throws,
19
+ * and the caller's guard turns it into a refusal.
20
+ */
21
+ export declare function parseEntityRef(ref: string, world: World): Entity | null;
22
+ export interface EntityContextOptions {
23
+ readonly id: string;
24
+ readonly title: string;
25
+ readonly priority: number;
26
+ readonly maxItems?: number;
27
+ readonly components: readonly ComponentType[];
28
+ }
29
+ /**
30
+ * A context provider that samples a query into a list of references.
31
+ *
32
+ * **This allocates, and that is correct here.** A reference is a string and a sample is
33
+ * a list, so neither can be free. What matters is where it runs: context is assembled
34
+ * once per *request*, not once per tick, and a request already costs a network round
35
+ * trip. The floor is the per-tick path and it is the one with the allocation floor.
36
+ *
37
+ * What wrapping the cursor must not do is make *iteration* allocate — that property is
38
+ * `@driftengine/entities`' and is floored in its own suite — so the loop reads the
39
+ * cursor directly rather than materialising it first.
40
+ */
41
+ export declare function entityContext(world: World, options: EntityContextOptions): ContextProvider<readonly string[]>;
42
+ /**
43
+ * A tool addressing an entity, whose guard checks the **generation** and not only the index.
44
+ *
45
+ * The staleness case the buffer makes likely rather than rare: an entity is destroyed
46
+ * and its slot is reused, so an index that still resolves resolves to something else
47
+ * entirely. A guard checking only the index would let a plan made about one thing run
48
+ * against another, which is worse than the plan failing.
49
+ */
50
+ export declare function entityTool<R>(world: World, id: string, description: string, execute: (entity: Entity, world: World) => R): ToolDefinition<{
51
+ target: string;
52
+ }, R | null, World>;
@@ -0,0 +1,83 @@
1
+ import { entityGeneration, entityIndex, packEntity, } from '@driftengine/entities';
2
+ /**
3
+ * Context and tools over an entity world.
4
+ *
5
+ * This is the half of AI-6 that links. The other half — a bridge that moves something
6
+ * along a path — is refused in writing, because nothing pathfinds and `drift/navigation`
7
+ * waits on no track at all. A seam with no implementation behind it is what R1 withdrew.
8
+ */
9
+ /** A stable, printable name for an entity, and the only form a model ever sees. */
10
+ export function entityRef(entity) {
11
+ return `e${entityIndex(entity)}.${entityGeneration(entity)}`;
12
+ }
13
+ /**
14
+ * Read a reference back, or `null` when it is not one.
15
+ *
16
+ * A model inventing an identifier is ordinary rather than exceptional — the parent
17
+ * design's identity constraints say so in words — so this returns rather than throws,
18
+ * and the caller's guard turns it into a refusal.
19
+ */
20
+ export function parseEntityRef(ref, world) {
21
+ const match = /^e(\d+)\.(\d+)$/.exec(ref);
22
+ if (match === null)
23
+ return null;
24
+ /* Rebuilt and asked, rather than searched for. `alive` compares the generation, so a
25
+ handle whose slot has been reused answers false — which is the whole reason a
26
+ reference carries one. */
27
+ const entity = packEntity(Number(match[1]), Number(match[2]));
28
+ return world.alive(entity) ? entity : null;
29
+ }
30
+ /**
31
+ * A context provider that samples a query into a list of references.
32
+ *
33
+ * **This allocates, and that is correct here.** A reference is a string and a sample is
34
+ * a list, so neither can be free. What matters is where it runs: context is assembled
35
+ * once per *request*, not once per tick, and a request already costs a network round
36
+ * trip. The floor is the per-tick path and it is the one with the allocation floor.
37
+ *
38
+ * What wrapping the cursor must not do is make *iteration* allocate — that property is
39
+ * `@driftengine/entities`' and is floored in its own suite — so the loop reads the
40
+ * cursor directly rather than materialising it first.
41
+ */
42
+ export function entityContext(world, options) {
43
+ const schema = { kind: 'array', of: { kind: 'string' } };
44
+ const limit = options.maxItems ?? Number.POSITIVE_INFINITY;
45
+ const [a, b, c, d] = options.components;
46
+ return {
47
+ id: options.id,
48
+ priority: options.priority,
49
+ maxItems: options.maxItems,
50
+ describe: () => ({ title: options.title, schema }),
51
+ sample: () => {
52
+ const out = [];
53
+ if (a === undefined)
54
+ return out;
55
+ for (const entity of world.query(a, b, c, d)) {
56
+ if (out.length >= limit)
57
+ break;
58
+ out.push(entityRef(entity));
59
+ }
60
+ return out;
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * A tool addressing an entity, whose guard checks the **generation** and not only the index.
66
+ *
67
+ * The staleness case the buffer makes likely rather than rare: an entity is destroyed
68
+ * and its slot is reused, so an index that still resolves resolves to something else
69
+ * entirely. A guard checking only the index would let a plan made about one thing run
70
+ * against another, which is worse than the plan failing.
71
+ */
72
+ export function entityTool(world, id, description, execute) {
73
+ return {
74
+ id,
75
+ description,
76
+ schema: { kind: 'object', fields: { target: { kind: 'string' } } },
77
+ admits: (args) => parseEntityRef(args.target, world) !== null,
78
+ execute: (args, w) => {
79
+ const entity = parseEntityRef(args.target, world);
80
+ return entity === null ? null : execute(entity, w);
81
+ },
82
+ };
83
+ }