@gajae-code/agent-core 0.12.0 → 0.12.2
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/CHANGELOG.md +10 -1
- package/dist/types/agent.d.ts +8 -1
- package/dist/types/compaction/pruning.d.ts +29 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/run-resource-ledger.d.ts +2 -0
- package/dist/types/types.d.ts +34 -0
- package/package.json +4 -4
- package/src/agent-loop.ts +128 -30
- package/src/agent.ts +37 -3
- package/src/compaction/pruning.ts +325 -131
- package/src/index.ts +1 -0
- package/src/run-resource-ledger.ts +213 -0
- package/src/types.ts +30 -0
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,213 @@
|
|
|
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
|
+
// Sealing only freezes admission of *new* work; it does not mean the run's
|
|
141
|
+
// resources have all been registered yet. `agent_end` is published before
|
|
142
|
+
// seal(), and its handlers register their own post-prompt work while the
|
|
143
|
+
// event is still draining, so this late registration is the normal
|
|
144
|
+
// lifecycle rather than an escaped resource. Admit it into ordinary
|
|
145
|
+
// settlement accounting so the run stays unsettled until it completes;
|
|
146
|
+
// quarantining here would make every cancel unfenced forever.
|
|
147
|
+
state.resources.set(id, { entry });
|
|
148
|
+
observeSettlement(settled, () => settleTracked(state!, id));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
state.resources.set(id, { entry });
|
|
153
|
+
observeSettlement(settled, () => settleTracked(state!, id));
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
pending(resourceRunId) {
|
|
157
|
+
const state = runs.get(resourceRunId);
|
|
158
|
+
return state ? snapshot(state) : [];
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
seal(resourceRunId) {
|
|
162
|
+
const state = runs.get(resourceRunId);
|
|
163
|
+
if (state?.lifecycle !== "open") return;
|
|
164
|
+
state.lifecycle = "sealed";
|
|
165
|
+
notify(state);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
waitForSettlement(resourceRunId, { graceMs }) {
|
|
169
|
+
const state = runs.get(resourceRunId);
|
|
170
|
+
if (!state) {
|
|
171
|
+
return Promise.resolve({ status: "unfenced", pending: [] });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const immediate = settlementProof(state);
|
|
175
|
+
if (immediate) return Promise.resolve(immediate);
|
|
176
|
+
|
|
177
|
+
const { promise, resolve } = Promise.withResolvers<RunSettlementProof>();
|
|
178
|
+
let waiter!: SettlementWaiter;
|
|
179
|
+
waiter = {
|
|
180
|
+
resolve,
|
|
181
|
+
timer: setTimeout(
|
|
182
|
+
() => {
|
|
183
|
+
state.waiters.delete(waiter);
|
|
184
|
+
const settled = settlementProof(state);
|
|
185
|
+
resolve(
|
|
186
|
+
settled ?? {
|
|
187
|
+
status: "unfenced",
|
|
188
|
+
pending: snapshot(state),
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
Math.max(0, graceMs),
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
state.waiters.add(waiter);
|
|
196
|
+
return promise;
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
quarantine(resourceRunId) {
|
|
200
|
+
let state = runs.get(resourceRunId);
|
|
201
|
+
if (!state) {
|
|
202
|
+
state = {
|
|
203
|
+
lifecycle: "quarantined",
|
|
204
|
+
resources: new Map<string, TrackedResource>(),
|
|
205
|
+
tombstone: [],
|
|
206
|
+
waiters: new Set<SettlementWaiter>(),
|
|
207
|
+
};
|
|
208
|
+
runs.set(resourceRunId, state);
|
|
209
|
+
}
|
|
210
|
+
return quarantineState(state);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
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
|
/**
|