@deepstrike/sdk 0.2.39 → 0.2.40

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 (102) hide show
  1. package/README.md +61 -31
  2. package/dist/collaboration/contract.d.ts +2 -2
  3. package/dist/collaboration/contract.js +2 -2
  4. package/dist/collaboration/handoff.d.ts +2 -14
  5. package/dist/collaboration/handoff.js +1 -17
  6. package/dist/collaboration/harness.d.ts +16 -47
  7. package/dist/collaboration/harness.js +57 -158
  8. package/dist/collaboration/index.d.ts +2 -2
  9. package/dist/collaboration/index.js +1 -2
  10. package/dist/collaboration/modes/creator-verifier.js +42 -6
  11. package/dist/collaboration/pool.d.ts +8 -0
  12. package/dist/collaboration/pool.js +56 -3
  13. package/dist/harness/harness.d.ts +97 -97
  14. package/dist/harness/harness.js +190 -144
  15. package/dist/harness/judge.d.ts +3 -2
  16. package/dist/harness/judge.js +7 -3
  17. package/dist/harness/public.d.ts +5 -3
  18. package/dist/harness/public.js +3 -2
  19. package/dist/index.d.ts +22 -7
  20. package/dist/index.js +8 -1
  21. package/dist/kernel.d.ts +12 -39
  22. package/dist/memory/agent.d.ts +8 -41
  23. package/dist/memory/agent.js +34 -117
  24. package/dist/memory/extraction.d.ts +4 -0
  25. package/dist/memory/extraction.js +79 -0
  26. package/dist/memory/in-memory-store.d.ts +28 -15
  27. package/dist/memory/in-memory-store.js +74 -25
  28. package/dist/memory/protocols.d.ts +56 -56
  29. package/dist/memory/protocols.js +1 -0
  30. package/dist/memory/public.d.ts +3 -1
  31. package/dist/memory/public.js +1 -0
  32. package/dist/memory/ranking.d.ts +33 -0
  33. package/dist/memory/ranking.js +77 -0
  34. package/dist/memory/retention.d.ts +17 -0
  35. package/dist/memory/retention.js +54 -0
  36. package/dist/os/public.d.ts +3 -3
  37. package/dist/os/public.js +1 -1
  38. package/dist/providers/base.d.ts +8 -0
  39. package/dist/providers/base.js +0 -0
  40. package/dist/providers/gemini.js +12 -0
  41. package/dist/providers/ollama.js +3 -1
  42. package/dist/providers/openai-responses.js +9 -2
  43. package/dist/providers/profiles.d.ts +5 -3
  44. package/dist/providers/profiles.js +3 -3
  45. package/dist/runtime/context-policy.d.ts +35 -0
  46. package/dist/runtime/context-policy.js +66 -0
  47. package/dist/runtime/eval.d.ts +6 -2
  48. package/dist/runtime/eval.js +2 -2
  49. package/dist/runtime/event-stream.d.ts +9 -0
  50. package/dist/runtime/event-stream.js +25 -11
  51. package/dist/runtime/execution-plane.d.ts +5 -1
  52. package/dist/runtime/execution-plane.js +9 -3
  53. package/dist/runtime/facade.js +2 -1
  54. package/dist/runtime/kernel-event-log.d.ts +0 -2
  55. package/dist/runtime/kernel-event-log.js +61 -13
  56. package/dist/runtime/kernel-primitives-dashboard.js +1 -1
  57. package/dist/runtime/kernel-rebuild.d.ts +13 -0
  58. package/dist/runtime/kernel-rebuild.js +75 -0
  59. package/dist/runtime/kernel-step.d.ts +157 -8
  60. package/dist/runtime/kernel-step.js +220 -7
  61. package/dist/runtime/kernel-transaction-log.d.ts +61 -0
  62. package/dist/runtime/kernel-transaction-log.js +149 -0
  63. package/dist/runtime/large-result-spool.d.ts +3 -1
  64. package/dist/runtime/large-result-spool.js +24 -5
  65. package/dist/runtime/loop-driver.d.ts +1 -1
  66. package/dist/runtime/loop-driver.js +2 -7
  67. package/dist/runtime/mcp-proxy-plane.d.ts +1 -0
  68. package/dist/runtime/mcp-proxy-plane.js +23 -6
  69. package/dist/runtime/os-profile.d.ts +9 -10
  70. package/dist/runtime/os-profile.js +14 -10
  71. package/dist/runtime/os-snapshot.d.ts +19 -0
  72. package/dist/runtime/os-snapshot.js +33 -3
  73. package/dist/runtime/process-sandbox-plane.js +16 -11
  74. package/dist/runtime/reaction-checkpoint.d.ts +51 -0
  75. package/dist/runtime/reaction-checkpoint.js +83 -0
  76. package/dist/runtime/reactive-session.d.ts +9 -3
  77. package/dist/runtime/reactive-session.js +44 -14
  78. package/dist/runtime/reliability.d.ts +48 -0
  79. package/dist/runtime/reliability.js +86 -0
  80. package/dist/runtime/remote-vpc-plane.js +4 -3
  81. package/dist/runtime/run-group.d.ts +38 -35
  82. package/dist/runtime/run-group.js +97 -54
  83. package/dist/runtime/runner.d.ts +117 -78
  84. package/dist/runtime/runner.js +1516 -1197
  85. package/dist/runtime/session-log.d.ts +71 -12
  86. package/dist/runtime/session-log.js +192 -32
  87. package/dist/runtime/session-repair.d.ts +11 -7
  88. package/dist/runtime/session-repair.js +11 -8
  89. package/dist/runtime/sub-agent-orchestrator.d.ts +6 -2
  90. package/dist/runtime/sub-agent-orchestrator.js +45 -18
  91. package/dist/signals/gateway.d.ts +23 -10
  92. package/dist/signals/gateway.js +81 -17
  93. package/dist/signals/scheduled.js +0 -1
  94. package/dist/signals/types.d.ts +21 -13
  95. package/dist/skills/loader.js +12 -2
  96. package/dist/tools/index.d.ts +2 -0
  97. package/dist/types/agent.d.ts +40 -1
  98. package/dist/types/agent.js +61 -1
  99. package/dist/types.d.ts +6 -1
  100. package/dist/workflow/public.d.ts +2 -3
  101. package/dist/workflow/public.js +0 -1
  102. package/package.json +2 -2
@@ -0,0 +1,83 @@
1
+ /** Process-local reference implementation; durable stores implement the same atomic contract. */
2
+ export class InMemoryReactionCheckpointStore {
3
+ opts;
4
+ states = new Map();
5
+ leaseSeq = 0;
6
+ constructor(opts = {}) {
7
+ this.opts = opts;
8
+ }
9
+ async claim(checkpointKey, leaseMs = this.opts.defaultLeaseMs ?? 900_000) {
10
+ if (!Number.isFinite(leaseMs) || leaseMs <= 0)
11
+ throw new RangeError("leaseMs must be positive");
12
+ const now = this.opts.now?.() ?? Date.now();
13
+ const state = this.states.get(checkpointKey) ?? {
14
+ outputs: new Map(),
15
+ completed: false,
16
+ };
17
+ this.states.set(checkpointKey, state);
18
+ if (state.completed)
19
+ return { status: "completed", reactions: this.reactions(state) };
20
+ if (state.lease && state.lease.expiresAtMs > now)
21
+ return { status: "busy" };
22
+ const token = `${checkpointKey}:lease-${++this.leaseSeq}`;
23
+ const expiresAtMs = now + leaseMs;
24
+ state.lease = { token, expiresAtMs };
25
+ return {
26
+ status: "claimed",
27
+ claim: {
28
+ checkpointKey,
29
+ leaseToken: token,
30
+ leaseExpiresAtMs: expiresAtMs,
31
+ plan: state.plan ? [...state.plan] : undefined,
32
+ outputs: Object.fromEntries(state.outputs),
33
+ },
34
+ };
35
+ }
36
+ async savePlan(receipt, personaIds) {
37
+ const state = this.current(receipt);
38
+ if (!state)
39
+ return null;
40
+ state.plan ??= [...new Set(personaIds)];
41
+ return [...state.plan];
42
+ }
43
+ async record(receipt, reaction) {
44
+ const state = this.current(receipt);
45
+ if (!state)
46
+ return false;
47
+ state.outputs.set(reaction.personaId, reaction.output);
48
+ return true;
49
+ }
50
+ async complete(receipt) {
51
+ const state = this.current(receipt);
52
+ if (!state)
53
+ return false;
54
+ if (!state.plan || state.plan.some(personaId => !state.outputs.has(personaId))) {
55
+ throw new Error("cannot complete a reaction checkpoint with unfinished personas");
56
+ }
57
+ state.completed = true;
58
+ delete state.lease;
59
+ return true;
60
+ }
61
+ async release(receipt) {
62
+ const state = this.current(receipt);
63
+ if (!state)
64
+ return false;
65
+ delete state.lease;
66
+ return true;
67
+ }
68
+ current(receipt) {
69
+ const state = this.states.get(receipt.checkpointKey);
70
+ return state?.lease?.token === receipt.leaseToken ? state : undefined;
71
+ }
72
+ reactions(state) {
73
+ return (state.plan ?? []).map(personaId => ({ personaId, output: state.outputs.get(personaId) }));
74
+ }
75
+ }
76
+ export class ReactionInProgressError extends Error {
77
+ checkpointKey;
78
+ constructor(checkpointKey) {
79
+ super(`reaction checkpoint is already in progress: ${checkpointKey}`);
80
+ this.checkpointKey = checkpointKey;
81
+ this.name = "ReactionInProgressError";
82
+ }
83
+ }
@@ -17,6 +17,7 @@ import { SignalGateway } from "../os/public.js";
17
17
  import type { BlackboardEvent, EventStream, EventViewer } from "./event-stream.js";
18
18
  import type { TurnPolicy } from "./turn-policy.js";
19
19
  import type { RegisteredTool } from "../tools/index.js";
20
+ import type { ReactionCheckpointStore, ReactionRecord } from "./reaction-checkpoint.js";
20
21
  /**
21
22
  * How a persona executes one reactive turn. The default body is a single `runner.run(...)` agent turn;
22
23
  * override it to make a persona's turn a *different orchestration form* — e.g. drive a DAG via
@@ -47,6 +48,8 @@ export interface ReactivePeerSpec {
47
48
  /** What the caller appends to the blackboard via `emit`. */
48
49
  export interface EmitEvent {
49
50
  payload: unknown;
51
+ /** Stable external request/event key used for append and reaction idempotency. */
52
+ idempotencyKey?: string;
50
53
  source?: string;
51
54
  channel?: string;
52
55
  audience?: string[];
@@ -58,6 +61,10 @@ export interface ReactiveSessionOptions {
58
61
  turnPolicy: TurnPolicy;
59
62
  /** Shared blackboard. Defaults to a process-local `InMemoryEventStream`. */
60
63
  eventStream?: EventStream;
64
+ /** Persisted event-level reaction plan/output checkpoints. Defaults to process-local memory. */
65
+ checkpointStore?: ReactionCheckpointStore;
66
+ /** Lease covering one event's reaction plan. Default: 15 minutes. */
67
+ reactionLeaseMs?: number;
61
68
  /** Shared signal gateway for targeted interrupt / broadcast (L0). Defaults to a fresh one. */
62
69
  signalGateway?: SignalGateway;
63
70
  /**
@@ -79,9 +86,7 @@ export interface ReactiveSessionOptions {
79
86
  reactWith?: ReactorTurn;
80
87
  }
81
88
  /** A persona's reaction to an emitted event. */
82
- export interface Reaction {
83
- personaId: string;
84
- output: string;
89
+ export interface Reaction extends ReactionRecord {
85
90
  }
86
91
  export declare class ReactiveSession {
87
92
  private readonly opts;
@@ -90,6 +95,7 @@ export declare class ReactiveSession {
90
95
  private readonly policyState;
91
96
  private readonly eventStream;
92
97
  private readonly gateway;
98
+ private readonly checkpointStore;
93
99
  constructor(opts: ReactiveSessionOptions);
94
100
  /** Register a peer persona and record it in the group membership (lineage). */
95
101
  addPeer(personaId: string, spec?: ReactivePeerSpec): void;
@@ -2,6 +2,7 @@ import { collectText } from "./runner.js";
2
2
  import { SignalGateway } from "../os/public.js";
3
3
  import { InMemoryEventStream, isVisibleTo } from "./event-stream.js";
4
4
  import { tool } from "../tools/index.js";
5
+ import { InMemoryReactionCheckpointStore, ReactionInProgressError, } from "./reaction-checkpoint.js";
5
6
  export class ReactiveSession {
6
7
  opts;
7
8
  peerSpecs = new Map();
@@ -9,10 +10,12 @@ export class ReactiveSession {
9
10
  policyState = {};
10
11
  eventStream;
11
12
  gateway;
13
+ checkpointStore;
12
14
  constructor(opts) {
13
15
  this.opts = opts;
14
16
  this.eventStream = opts.eventStream ?? new InMemoryEventStream();
15
17
  this.gateway = opts.signalGateway ?? new SignalGateway();
18
+ this.checkpointStore = opts.checkpointStore ?? new InMemoryReactionCheckpointStore();
16
19
  }
17
20
  /** Register a peer persona and record it in the group membership (lineage). */
18
21
  addPeer(personaId, spec = {}) {
@@ -37,19 +40,47 @@ export class ReactiveSession {
37
40
  */
38
41
  async emit(event) {
39
42
  const bbEvent = await this.eventStream.append(event);
40
- const candidates = [...this.peerSpecs.entries()]
41
- .map(([personaId, spec]) => ({ personaId, role: spec.role, channels: spec.channels }))
42
- // Only personas that can actually see the event are eligible to react.
43
- .filter(p => isVisibleTo(bbEvent, p));
44
- const chosen = await this.opts.turnPolicy(bbEvent, candidates, this.policyState);
45
- const eligible = new Set(candidates.map(p => p.personaId));
46
- const reactions = [];
47
- for (const personaId of chosen) {
48
- if (!eligible.has(personaId))
49
- continue;
50
- reactions.push({ personaId, output: await this.driveTurn(personaId, bbEvent) });
43
+ const eventIdentity = bbEvent.idempotencyKey ?? `seq:${bbEvent.seq}`;
44
+ const checkpointKey = JSON.stringify([this.opts.runGroup.id, eventIdentity]);
45
+ const checkpoint = await this.checkpointStore.claim(checkpointKey, this.opts.reactionLeaseMs);
46
+ if (checkpoint.status === "completed")
47
+ return checkpoint.reactions;
48
+ if (checkpoint.status === "busy")
49
+ throw new ReactionInProgressError(checkpointKey);
50
+ const receipt = checkpoint.claim;
51
+ try {
52
+ const candidates = [...this.peerSpecs.entries()]
53
+ .map(([personaId, spec]) => ({ personaId, role: spec.role, channels: spec.channels }))
54
+ .filter(p => isVisibleTo(bbEvent, p));
55
+ const eligible = new Set(candidates.map(p => p.personaId));
56
+ const selected = checkpoint.claim.plan
57
+ ?? (await this.opts.turnPolicy(bbEvent, candidates, this.policyState)).filter(id => eligible.has(id));
58
+ const plan = await this.checkpointStore.savePlan(receipt, selected);
59
+ if (!plan)
60
+ throw new Error("reaction checkpoint lease was lost before saving the plan");
61
+ const outputs = new Map(Object.entries(checkpoint.claim.outputs));
62
+ const reactions = [];
63
+ for (const personaId of plan) {
64
+ let output = outputs.get(personaId);
65
+ if (output === undefined) {
66
+ if (!eligible.has(personaId))
67
+ throw new Error(`reaction persona is unavailable: ${personaId}`);
68
+ output = await this.driveTurn(personaId, bbEvent);
69
+ if (!await this.checkpointStore.record(receipt, { personaId, output })) {
70
+ throw new Error("reaction checkpoint lease was lost before recording output");
71
+ }
72
+ }
73
+ reactions.push({ personaId, output });
74
+ }
75
+ if (!await this.checkpointStore.complete(receipt)) {
76
+ throw new Error("reaction checkpoint lease was lost before completion");
77
+ }
78
+ return reactions;
79
+ }
80
+ catch (cause) {
81
+ await this.checkpointStore.release(receipt);
82
+ throw cause;
51
83
  }
52
- return reactions;
53
84
  }
54
85
  /** Targeted preemption: deliver a critical signal to one persona's loop only (L0 recipient routing). */
55
86
  async interrupt(personaId, signal) {
@@ -64,13 +95,12 @@ export class ReactiveSession {
64
95
  }
65
96
  /** Broadcast a signal to every persona (each sees it on its next turn). */
66
97
  async broadcast(signal) {
67
- this.gateway.ingest({
98
+ this.gateway.broadcast(this.peers(), {
68
99
  source: "gateway",
69
100
  signalType: "event",
70
101
  urgency: "normal",
71
102
  payload: signal.payload ?? {},
72
103
  ...signal,
73
- recipient: undefined,
74
104
  });
75
105
  }
76
106
  getRunner(personaId) {
@@ -0,0 +1,48 @@
1
+ /** A failure raised by an observer after the owning mutation has already committed. */
2
+ export interface ObserverFailure {
3
+ component: string;
4
+ operation: string;
5
+ cause: unknown;
6
+ committed: true;
7
+ }
8
+ export type ObserverErrorHandler = (failure: ObserverFailure) => void;
9
+ /** Immutable identity and cancellation boundary for one runtime operation. */
10
+ export interface OperationContext {
11
+ readonly runId: string;
12
+ readonly sessionId: string;
13
+ readonly agentId?: string;
14
+ readonly signal: AbortSignal;
15
+ readonly deadlineMs?: number;
16
+ readonly provenance?: Readonly<Record<string, string>>;
17
+ }
18
+ export interface BackgroundTaskFailure {
19
+ readonly label: string;
20
+ readonly operation: OperationContext;
21
+ readonly cause: unknown;
22
+ }
23
+ export type BackgroundTaskErrorHandler = (failure: BackgroundTaskFailure) => void;
24
+ /** Compose an adapter timeout with the operation cancellation/deadline boundary. */
25
+ export declare function operationAbortSignal(operation: OperationContext | undefined, timeoutMs: number, now?: () => number): AbortSignal;
26
+ /** Report an observer failure without allowing the reporter to change the committed result. */
27
+ export declare function reportObserverFailure(handler: ObserverErrorHandler | undefined, failure: Omit<ObserverFailure, "committed">): void;
28
+ /**
29
+ * Serialize async mutations by owner key while allowing unrelated keys to proceed concurrently.
30
+ * The queued tail is always resolved, so one failed mutation cannot poison later operations.
31
+ */
32
+ export declare class KeyedSerialExecutor {
33
+ private readonly tails;
34
+ run<T>(key: string, operation: () => Promise<T>): Promise<T>;
35
+ }
36
+ /** Owns best-effort asynchronous work for exactly one operation. */
37
+ export declare class ManagedTaskScope {
38
+ private readonly onTaskError?;
39
+ private readonly tasks;
40
+ private readonly controller;
41
+ private closed;
42
+ readonly operation: OperationContext;
43
+ constructor(operation: OperationContext, onTaskError?: BackgroundTaskErrorHandler | undefined);
44
+ get pending(): number;
45
+ spawn(label: string, work: (operation: OperationContext) => Promise<void> | void): void;
46
+ drain(): Promise<void>;
47
+ cancel(reason?: unknown): Promise<void>;
48
+ }
@@ -0,0 +1,86 @@
1
+ /** Compose an adapter timeout with the operation cancellation/deadline boundary. */
2
+ export function operationAbortSignal(operation, timeoutMs, now = Date.now) {
3
+ const remaining = operation?.deadlineMs === undefined
4
+ ? timeoutMs
5
+ : Math.min(timeoutMs, operation.deadlineMs - now());
6
+ const timeout = remaining <= 0
7
+ ? AbortSignal.abort(new Error("operation deadline exceeded"))
8
+ : AbortSignal.timeout(remaining);
9
+ return operation ? AbortSignal.any([operation.signal, timeout]) : timeout;
10
+ }
11
+ /** Report an observer failure without allowing the reporter to change the committed result. */
12
+ export function reportObserverFailure(handler, failure) {
13
+ try {
14
+ handler?.({ ...failure, committed: true });
15
+ }
16
+ catch {
17
+ // The reporter is itself an observer. There is no additional semantic owner to fail here.
18
+ }
19
+ }
20
+ /**
21
+ * Serialize async mutations by owner key while allowing unrelated keys to proceed concurrently.
22
+ * The queued tail is always resolved, so one failed mutation cannot poison later operations.
23
+ */
24
+ export class KeyedSerialExecutor {
25
+ tails = new Map();
26
+ async run(key, operation) {
27
+ const previous = this.tails.get(key) ?? Promise.resolve();
28
+ let release;
29
+ const gate = new Promise(resolve => { release = resolve; });
30
+ const tail = previous.then(() => gate);
31
+ this.tails.set(key, tail);
32
+ await previous;
33
+ try {
34
+ return await operation();
35
+ }
36
+ finally {
37
+ release();
38
+ void tail.then(() => {
39
+ if (this.tails.get(key) === tail)
40
+ this.tails.delete(key);
41
+ });
42
+ }
43
+ }
44
+ }
45
+ /** Owns best-effort asynchronous work for exactly one operation. */
46
+ export class ManagedTaskScope {
47
+ onTaskError;
48
+ tasks = new Set();
49
+ controller = new AbortController();
50
+ closed = false;
51
+ operation;
52
+ constructor(operation, onTaskError) {
53
+ this.onTaskError = onTaskError;
54
+ this.operation = {
55
+ ...operation,
56
+ signal: AbortSignal.any([operation.signal, this.controller.signal]),
57
+ };
58
+ }
59
+ get pending() {
60
+ return this.tasks.size;
61
+ }
62
+ spawn(label, work) {
63
+ if (this.closed)
64
+ throw new Error("task scope is closed");
65
+ const task = Promise.resolve()
66
+ .then(() => work(this.operation))
67
+ .catch(cause => {
68
+ try {
69
+ this.onTaskError?.({ label, operation: this.operation, cause });
70
+ }
71
+ catch {
72
+ // Failure reporting is observational and cannot own the task result.
73
+ }
74
+ })
75
+ .finally(() => { this.tasks.delete(task); });
76
+ this.tasks.add(task);
77
+ }
78
+ async drain() {
79
+ this.closed = true;
80
+ await Promise.all([...this.tasks]);
81
+ }
82
+ async cancel(reason) {
83
+ this.controller.abort(reason);
84
+ await this.drain();
85
+ }
86
+ }
@@ -1,5 +1,6 @@
1
1
  import { LocalExecutionPlane } from "./execution-plane.js";
2
2
  import { formatToolError } from "../tools/errors.js";
3
+ import { operationAbortSignal } from "./reliability.js";
3
4
  /**
4
5
  * ExecutionPlane that forwards tool calls over HTTP to a worker inside a customer VPC.
5
6
  *
@@ -48,7 +49,7 @@ export class RemoteVpcPlane {
48
49
  if (auth)
49
50
  headers["Authorization"] = auth;
50
51
  // Fire all remote calls concurrently; yield results in dispatch order
51
- const pending = remoteCalls.map(call => this.callRemote(call, headers).then(result => ({ call, result })));
52
+ const pending = remoteCalls.map(call => this.callRemote(call, headers, ctx.operation).then(result => ({ call, result })));
52
53
  for (const p of pending) {
53
54
  const { call, result } = await p;
54
55
  yield {
@@ -60,14 +61,14 @@ export class RemoteVpcPlane {
60
61
  };
61
62
  }
62
63
  }
63
- async callRemote(call, headers) {
64
+ async callRemote(call, headers, operation) {
64
65
  try {
65
66
  const args = JSON.parse(call.arguments || "{}");
66
67
  const response = await fetch(`${this.opts.baseUrl}/execute`, {
67
68
  method: "POST",
68
69
  headers,
69
70
  body: JSON.stringify({ name: call.name, arguments: args }),
70
- signal: AbortSignal.timeout(this.opts.timeoutMs ?? 30_000),
71
+ signal: operationAbortSignal(operation, this.opts.timeoutMs ?? 30_000),
71
72
  });
72
73
  if (!response.ok) {
73
74
  const body = await response.text().catch(() => "");
@@ -1,21 +1,3 @@
1
- /**
2
- * L1 (RunGroup) — a governance domain shared by N peer agent sessions of one logical run.
3
- *
4
- * The kernel (execution vehicle) is ephemeral and torn down between stateless turns, so the
5
- * cumulative budget + membership that must span the whole group live outside any vehicle: in a
6
- * `GroupBudgetStore`. Each member's run is seeded at boot with the group's accumulated spend (tokens
7
- * + sub-agent spawns) so the run-level token cap and the cumulative spawn cap are enforced across all
8
- * members, registers itself as a member (lineage), and charges its own consumption back when it ends.
9
- * Per spec §2.5, only *cumulative* budget is shared this way; instantaneous concurrency stays
10
- * vehicle-scoped.
11
- *
12
- * Two built-in stores:
13
- * - `InMemoryGroupBudgetStore` — process-local; fine for a single replica / tests.
14
- * - `SessionLogGroupBudgetStore` — persists the ledger + membership to any `SessionLog` (fold-on-read
15
- * under a group-anchor key), so a logical run's governance + lineage survive process boundaries and
16
- * span replicas when backed by a durable `SessionLog`.
17
- */
18
- import type { SessionLog } from "./session-log.js";
19
1
  /** Cumulative resources spent across a run group. */
20
2
  export interface GroupLedger {
21
3
  /** ③ loop-agent rounds completed across the group (seeds the pacing trap's max_rounds). */
@@ -32,6 +14,19 @@ export interface GroupCharge {
32
14
  /** ③ loop-agent: completed rounds to add to the group's round count. */
33
15
  rounds?: number;
34
16
  }
17
+ export interface GroupBudgetRequest {
18
+ /** Group-wide hard limits. An omitted axis is unbounded for this admission. */
19
+ limits: GroupCharge;
20
+ /** Maximum capacity this member wants to hold for its lifetime. */
21
+ requested: GroupCharge;
22
+ }
23
+ export interface GroupBudgetReservation {
24
+ id: string;
25
+ groupId: string;
26
+ memberId: string;
27
+ /** Capacity granted to this member. May be lower than requested when the group is nearly full. */
28
+ granted: GroupCharge;
29
+ }
35
30
  /** A persona session that participated in the logical run (process-table lineage). */
36
31
  export interface GroupMember {
37
32
  sessionId: string;
@@ -43,36 +38,44 @@ export interface GroupMember {
43
38
  kind?: "peer" | "vehicle";
44
39
  }
45
40
  export interface GroupBudgetStore {
46
- /** Cumulative spend across the group so far. */
47
- read(groupId: string): GroupLedger | Promise<GroupLedger>;
48
- /** Add a member's spend to the group's cumulative totals. */
49
- charge(groupId: string, delta: GroupCharge): void | Promise<void>;
50
41
  /** Register a persona session as a member of the group (idempotent by sessionId). */
51
42
  join(groupId: string, member: GroupMember): void | Promise<void>;
52
43
  /** All persona sessions of the logical run — the cross-invocation lineage (R2). */
53
44
  members(groupId: string): GroupMember[] | Promise<GroupMember[]>;
45
+ reserve(groupId: string, request: GroupBudgetRequest & {
46
+ memberId: string;
47
+ }): GroupBudgetReservation | Promise<GroupBudgetReservation>;
48
+ /** Idempotently replace a reservation with actual usage. */
49
+ settle(groupId: string, reservationId: string, actual: GroupCharge): void | Promise<void>;
50
+ /** Idempotently discard an unused reservation. */
51
+ release(groupId: string, reservationId: string): void | Promise<void>;
54
52
  }
55
53
  /** Process-local default store. One ledger + member set per group id. */
56
54
  export declare class InMemoryGroupBudgetStore implements GroupBudgetStore {
57
55
  private readonly ledgers;
58
56
  private readonly memberships;
57
+ private readonly reservations;
59
58
  read(groupId: string): GroupLedger;
60
- charge(groupId: string, delta: GroupCharge): void;
59
+ private applyCharge;
61
60
  join(groupId: string, member: GroupMember): void;
62
61
  members(groupId: string): GroupMember[];
62
+ reserve(groupId: string, request: GroupBudgetRequest & {
63
+ memberId: string;
64
+ }): GroupBudgetReservation;
65
+ settle(groupId: string, reservationId: string, actual: GroupCharge): void;
66
+ release(groupId: string, reservationId: string): void;
63
67
  }
64
- /**
65
- * Persists the group ledger + membership to a `SessionLog`, keyed by a group-anchor session whose id
66
- * is the group id. Budget/membership rebuild by folding `group_budget_charged` / `group_member_joined`
67
- * events on read (spec §2.4). Durable + replica-spanning when the underlying `SessionLog` is.
68
- */
69
- export declare class SessionLogGroupBudgetStore implements GroupBudgetStore {
70
- private readonly log;
71
- constructor(log: SessionLog);
72
- read(groupId: string): Promise<GroupLedger>;
73
- charge(groupId: string, delta: GroupCharge): Promise<void>;
74
- join(groupId: string, member: GroupMember): Promise<void>;
75
- members(groupId: string): Promise<GroupMember[]>;
68
+ /** One member's reservation lifecycle. */
69
+ export declare class GroupBudgetScope {
70
+ private readonly group;
71
+ readonly granted: GroupCharge;
72
+ readonly reservationId: string;
73
+ private closed;
74
+ private constructor();
75
+ static open(group: RunGroup, member: GroupMember, request: GroupBudgetRequest): Promise<GroupBudgetScope>;
76
+ settle(actual: GroupCharge): Promise<void>;
77
+ get isClosed(): boolean;
78
+ release(): Promise<void>;
76
79
  }
77
80
  /** Binds a runner to a governance domain: a stable group id + the store its members share. */
78
81
  export interface RunGroup {