@gajae-code/agent-core 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from "./image-placeholder-guard";
12
12
  export * from "./proxy";
13
13
  // Run-level telemetry collector + aggregators
14
14
  export * from "./run-collector";
15
+ export * from "./run-resource-ledger";
15
16
  // Telemetry
16
17
  export * from "./telemetry";
17
18
  // Thinking selectors
@@ -0,0 +1,209 @@
1
+ import type { RunResourceEntry, RunResourceLedger, RunSettlementProof } from "./types";
2
+
3
+ const MAX_TOMBSTONE_ENTRIES = 256;
4
+
5
+ type RunLifecycle = "open" | "sealed" | "quarantined";
6
+
7
+ interface TrackedResource {
8
+ entry: RunResourceEntry;
9
+ }
10
+
11
+ interface RunState {
12
+ lifecycle: RunLifecycle;
13
+ resources: Map<string, TrackedResource>;
14
+ /** Bounded public snapshot retained after quarantine. */
15
+ tombstone: RunResourceEntry[];
16
+ waiters: Set<SettlementWaiter>;
17
+ }
18
+
19
+ interface SettlementWaiter {
20
+ resolve: (proof: RunSettlementProof) => void;
21
+ timer: NodeJS.Timeout;
22
+ }
23
+
24
+ function copyEntries(entries: readonly RunResourceEntry[]): RunResourceEntry[] {
25
+ return entries.map(entry => ({ ...entry }));
26
+ }
27
+
28
+ export function createRunResourceLedger(): RunResourceLedger {
29
+ const runs = new Map<string, RunState>();
30
+ let sequence = 0;
31
+
32
+ const snapshot = (state: RunState): RunResourceEntry[] => {
33
+ if (state.lifecycle === "quarantined") return copyEntries(state.tombstone);
34
+ return [...state.resources.values()].map(resource => ({ ...resource.entry }));
35
+ };
36
+
37
+ const settlementProof = (state: RunState): RunSettlementProof | undefined => {
38
+ if (state.lifecycle === "quarantined") {
39
+ return { status: "unfenced", pending: copyEntries(state.tombstone) };
40
+ }
41
+ if (state.lifecycle === "sealed" && state.resources.size === 0) {
42
+ return { status: "settled" };
43
+ }
44
+ return undefined;
45
+ };
46
+
47
+ const removeWaiter = (state: RunState, waiter: SettlementWaiter): void => {
48
+ clearTimeout(waiter.timer);
49
+ state.waiters.delete(waiter);
50
+ };
51
+
52
+ const notify = (state: RunState): void => {
53
+ const proof = settlementProof(state);
54
+ if (!proof) return;
55
+ for (const waiter of state.waiters) {
56
+ removeWaiter(state, waiter);
57
+ waiter.resolve(
58
+ proof.status === "unfenced"
59
+ ? { status: "unfenced", pending: copyEntries(proof.pending) }
60
+ : { status: "settled" },
61
+ );
62
+ }
63
+ };
64
+
65
+ const settleTracked = (state: RunState, id: string): void => {
66
+ if (!state.resources.delete(id)) return;
67
+ notify(state);
68
+ };
69
+
70
+ const observeSettlement = (settled: PromiseLike<unknown>, onSettled: () => void): void => {
71
+ // Assimilate the settlement promise once and consume both outcomes so a
72
+ // rejected resource cannot become an unhandled rejection.
73
+ let promise: Promise<unknown>;
74
+ try {
75
+ promise = Promise.resolve(settled);
76
+ } catch {
77
+ onSettled();
78
+ return;
79
+ }
80
+ void promise.then(onSettled, onSettled);
81
+ };
82
+
83
+ const appendTombstone = (state: RunState, entry: RunResourceEntry): void => {
84
+ state.tombstone.push({ ...entry });
85
+ if (state.tombstone.length > MAX_TOMBSTONE_ENTRIES) {
86
+ state.tombstone.splice(0, state.tombstone.length - MAX_TOMBSTONE_ENTRIES);
87
+ }
88
+ };
89
+
90
+ const quarantineState = (state: RunState): RunResourceEntry[] => {
91
+ if (state.lifecycle !== "quarantined") {
92
+ state.lifecycle = "quarantined";
93
+ state.tombstone = [];
94
+ for (const resource of state.resources.values()) appendTombstone(state, resource.entry);
95
+ state.resources.clear();
96
+ }
97
+ notify(state);
98
+ return copyEntries(state.tombstone);
99
+ };
100
+
101
+ return {
102
+ open(resourceRunId) {
103
+ const existing = runs.get(resourceRunId);
104
+ if (existing) return;
105
+ runs.set(resourceRunId, {
106
+ lifecycle: "open",
107
+ resources: new Map<string, TrackedResource>(),
108
+ tombstone: [],
109
+ waiters: new Set<SettlementWaiter>(),
110
+ });
111
+ },
112
+
113
+ track(resourceRunId, kind, label, settled) {
114
+ let state = runs.get(resourceRunId);
115
+ if (!state) {
116
+ // Keep track() usable for low-level callers while making the lifecycle
117
+ // explicit for settlement: an implicitly-created run is still open and
118
+ // therefore cannot settle until seal() is called.
119
+ state = {
120
+ lifecycle: "open",
121
+ resources: new Map<string, TrackedResource>(),
122
+ tombstone: [],
123
+ waiters: new Set<SettlementWaiter>(),
124
+ };
125
+ runs.set(resourceRunId, state);
126
+ }
127
+
128
+ const id = `${++sequence}`;
129
+ const entry: RunResourceEntry = { id, kind, label, registeredAt: Date.now() };
130
+
131
+ if (state.lifecycle === "quarantined") {
132
+ // Quarantine is terminal: late work is retained only in the bounded
133
+ // tombstone and never re-enters normal settlement accounting.
134
+ appendTombstone(state, entry);
135
+ observeSettlement(settled, () => {});
136
+ return;
137
+ }
138
+
139
+ if (state.lifecycle === "sealed") {
140
+ // A sealed run cannot be reopened. Late registration is fenced as
141
+ // quarantine rather than creating a false settled run.
142
+ quarantineState(state);
143
+ appendTombstone(state, entry);
144
+ observeSettlement(settled, () => {});
145
+ return;
146
+ }
147
+
148
+ state.resources.set(id, { entry });
149
+ observeSettlement(settled, () => settleTracked(state!, id));
150
+ },
151
+
152
+ pending(resourceRunId) {
153
+ const state = runs.get(resourceRunId);
154
+ return state ? snapshot(state) : [];
155
+ },
156
+
157
+ seal(resourceRunId) {
158
+ const state = runs.get(resourceRunId);
159
+ if (state?.lifecycle !== "open") return;
160
+ state.lifecycle = "sealed";
161
+ notify(state);
162
+ },
163
+
164
+ waitForSettlement(resourceRunId, { graceMs }) {
165
+ const state = runs.get(resourceRunId);
166
+ if (!state) {
167
+ return Promise.resolve({ status: "unfenced", pending: [] });
168
+ }
169
+
170
+ const immediate = settlementProof(state);
171
+ if (immediate) return Promise.resolve(immediate);
172
+
173
+ const { promise, resolve } = Promise.withResolvers<RunSettlementProof>();
174
+ let waiter!: SettlementWaiter;
175
+ waiter = {
176
+ resolve,
177
+ timer: setTimeout(
178
+ () => {
179
+ state.waiters.delete(waiter);
180
+ const settled = settlementProof(state);
181
+ resolve(
182
+ settled ?? {
183
+ status: "unfenced",
184
+ pending: snapshot(state),
185
+ },
186
+ );
187
+ },
188
+ Math.max(0, graceMs),
189
+ ),
190
+ };
191
+ state.waiters.add(waiter);
192
+ return promise;
193
+ },
194
+
195
+ quarantine(resourceRunId) {
196
+ let state = runs.get(resourceRunId);
197
+ if (!state) {
198
+ state = {
199
+ lifecycle: "quarantined",
200
+ resources: new Map<string, TrackedResource>(),
201
+ tombstone: [],
202
+ waiters: new Set<SettlementWaiter>(),
203
+ };
204
+ runs.set(resourceRunId, state);
205
+ }
206
+ return quarantineState(state);
207
+ },
208
+ };
209
+ }
package/src/types.ts CHANGED
@@ -28,6 +28,29 @@ export type StreamFn = (
28
28
 
29
29
  /** Stable identifier for a managed logical run, shared by all of its retry attempts. */
30
30
  export type ManagedLogicalRunId = number;
31
+ /** A resource owned by a prompt run until its promise settles. */
32
+ export type RunResourceKind = "provider_factory" | "provider_iterator" | "tool" | "post_prompt";
33
+
34
+ export interface RunResourceEntry {
35
+ id: string;
36
+ kind: RunResourceKind;
37
+ label: string;
38
+ registeredAt: number;
39
+ }
40
+
41
+ export type RunSettlementProof = { status: "settled" } | { status: "unfenced"; pending: RunResourceEntry[] };
42
+
43
+ export interface RunResourceLedger {
44
+ /** Reserve a run handle before publishing its `agent_start` event. */
45
+ open(resourceRunId: string): void;
46
+ track(resourceRunId: string, kind: RunResourceKind, label: string, settled: PromiseLike<unknown>): void;
47
+ pending(resourceRunId: string): RunResourceEntry[];
48
+ /** Seal a run after terminal event publication; only sealed empty runs settle. */
49
+ seal(resourceRunId: string): void;
50
+ waitForSettlement(resourceRunId: string, options: { graceMs: number }): Promise<RunSettlementProof>;
51
+ /** Terminally detach a run; its bounded tombstone remains unfenced forever. */
52
+ quarantine(resourceRunId: string): RunResourceEntry[];
53
+ }
31
54
 
32
55
  /** Terminal completion requested for a logical run. */
33
56
  export interface RunTerminalRequest {
@@ -353,6 +376,13 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
353
376
  * capture, cost estimator, agent identity).
354
377
  */
355
378
  telemetry?: AgentTelemetryConfig;
379
+ /**
380
+ * Optional prompt-run resource ownership ledger. Provider and scheduler-level tool
381
+ * work is tracked until its owned lifecycle promise settles.
382
+ */
383
+ resourceLedger?: RunResourceLedger;
384
+ /** Stable resource ownership identifier for this prompt run. */
385
+ resourceRunId?: string;
356
386
  }
357
387
 
358
388
  /**