@cubicecho/agent-core 2.0.2 → 2.0.4
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/dist/capabilities.js +14 -1
- package/dist/events.js +37 -3
- package/package.json +1 -1
package/dist/capabilities.js
CHANGED
|
@@ -30,6 +30,8 @@ export function capabilitiesFor(baseUrl) {
|
|
|
30
30
|
export function resetCapabilities() {
|
|
31
31
|
capabilities.clear();
|
|
32
32
|
}
|
|
33
|
+
/** Every flag on a `Capabilities`, typed, so a third one is compared without an edit here. */
|
|
34
|
+
const keys = (of) => Object.keys(of);
|
|
33
35
|
/** `stream_options` is named in the refusal by every server that has not heard of it. */
|
|
34
36
|
const REJECTS_USAGE = /stream_options/i;
|
|
35
37
|
/**
|
|
@@ -61,6 +63,10 @@ const REJECTS_USAGE = /stream_options/i;
|
|
|
61
63
|
*/
|
|
62
64
|
export async function negotiate(supports, send, { produced = { any: false }, onNotice } = {}) {
|
|
63
65
|
for (;;) {
|
|
66
|
+
// What this attempt was built with. `capabilitiesFor` hands one object per endpoint to
|
|
67
|
+
// everyone on it, so a run starting alongside this one may latch a flag off while this call
|
|
68
|
+
// is in flight — and the branches below are guarded on the flag still being set.
|
|
69
|
+
const sent = { ...supports };
|
|
64
70
|
try {
|
|
65
71
|
return await send(supports, produced);
|
|
66
72
|
}
|
|
@@ -76,9 +82,16 @@ export async function negotiate(supports, send, { produced = { any: false }, onN
|
|
|
76
82
|
supports.usageInStream = false;
|
|
77
83
|
onNotice?.("server rejected stream_options; token counts unavailable");
|
|
78
84
|
}
|
|
79
|
-
else {
|
|
85
|
+
else if (keys(sent).every((flag) => sent[flag] === supports[flag])) {
|
|
80
86
|
throw error;
|
|
81
87
|
}
|
|
88
|
+
// Otherwise the refusal was answered by whoever got there first, and this attempt was
|
|
89
|
+
// built before the answer existed. Two runs opening on a fresh llama.cpp box both get the
|
|
90
|
+
// grammar error; the first latches it off and re-sends, and the second used to find the
|
|
91
|
+
// flag already clear, fall through to the throw and die on an error the process had just
|
|
92
|
+
// learned to fix — `isTransient` refuses a 400, so `runTurn` would not send it again
|
|
93
|
+
// either. Sending it again is the whole of the fix: flags only ever latch off, so this
|
|
94
|
+
// gives up after one pass per flag.
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
}
|
package/dist/events.js
CHANGED
|
@@ -22,6 +22,22 @@ const MAX_EVENTS = 1000;
|
|
|
22
22
|
const TRIM_SLACK = 256;
|
|
23
23
|
/** How long a finished run stays readable, for a watcher that arrives just after the end. */
|
|
24
24
|
const RETAIN_MS = 60_000;
|
|
25
|
+
/**
|
|
26
|
+
* The same for a run that has not said `done`, which is a far more dangerous thing to drop.
|
|
27
|
+
*
|
|
28
|
+
* A finished run has nothing more to say, so forgetting it a minute later costs a late watcher
|
|
29
|
+
* a backlog and nothing else. An unfinished one is still writing: `touched` only moves on
|
|
30
|
+
* `emit`, so a live run that spends a minute inside one slow tool call looked exactly like an
|
|
31
|
+
* abandoned one and was reaped out from under itself. What made that more than a lost backlog
|
|
32
|
+
* is that the next `emit` builds a fresh stream with `seq` back at zero — and `seq` is the
|
|
33
|
+
* field `RunEvent` documents for ordering and de-duplication, so a client that reconnects
|
|
34
|
+
* across the gap discards the new events as ones it has already seen.
|
|
35
|
+
*
|
|
36
|
+
* The sweep still has to reap them, because a run killed by a signal or simply forgotten
|
|
37
|
+
* reaches no `done` either. This is the backstop for a caller that never says so; `endRun` is
|
|
38
|
+
* for one that knows.
|
|
39
|
+
*/
|
|
40
|
+
const RETAIN_UNENDED_MS = 30 * 60_000;
|
|
25
41
|
const streams = new Map();
|
|
26
42
|
const streamFor = (runId) => {
|
|
27
43
|
const existing = streams.get(runId);
|
|
@@ -52,9 +68,11 @@ const streamFor = (runId) => {
|
|
|
52
68
|
let sweeping = null;
|
|
53
69
|
function sweep() {
|
|
54
70
|
sweeping = null;
|
|
55
|
-
const
|
|
71
|
+
const now = Date.now();
|
|
56
72
|
for (const [runId, stream] of streams) {
|
|
57
|
-
if (stream.listeners.size
|
|
73
|
+
if (stream.listeners.size)
|
|
74
|
+
continue;
|
|
75
|
+
if (stream.touched <= now - (stream.ended ? RETAIN_MS : RETAIN_UNENDED_MS))
|
|
58
76
|
streams.delete(runId);
|
|
59
77
|
}
|
|
60
78
|
scheduleSweep();
|
|
@@ -73,6 +91,17 @@ function scheduleSweep() {
|
|
|
73
91
|
* @param runId The run to forget. An id nothing was emitted under is ignored.
|
|
74
92
|
*/
|
|
75
93
|
export function endRun(runId) {
|
|
94
|
+
const stream = streams.get(runId);
|
|
95
|
+
if (!stream)
|
|
96
|
+
return;
|
|
97
|
+
// Watchers are parked on a promise that only an `emit` to *this* stream can resolve, and the
|
|
98
|
+
// delete below puts it beyond the reach of every later one — the next `emit` builds a fresh
|
|
99
|
+
// stream and wakes nobody. So they are told the run is over first: a watcher that is handed
|
|
100
|
+
// `done` completes, runs its `finally` and lets its consumer go, where one left parked holds
|
|
101
|
+
// an open subscription that can never say anything again. An SSE client on the other end of
|
|
102
|
+
// that is a connection that never closes.
|
|
103
|
+
if (stream.listeners.size)
|
|
104
|
+
emit(runId, { kind: "done", ok: false, text: "run ended" });
|
|
76
105
|
streams.delete(runId);
|
|
77
106
|
}
|
|
78
107
|
/**
|
|
@@ -209,7 +238,12 @@ export async function* watch(runId) {
|
|
|
209
238
|
// A watcher can name a run that has not started, or will never start. Nothing was recorded
|
|
210
239
|
// under it, so nothing is left behind either — and a run that has ended has nothing more to
|
|
211
240
|
// say to anyone, so the last watcher leaving takes the backlog with it.
|
|
212
|
-
|
|
241
|
+
// Against `stream` rather than the id: this generator may have outlived its own entry — a
|
|
242
|
+
// sweep or an `endRun` drops it and a later `emit` files the same run under a new one — and
|
|
243
|
+
// the last watcher of the old stream has no business deleting the new one.
|
|
244
|
+
if (streams.get(runId) === stream &&
|
|
245
|
+
stream.listeners.size === 0 &&
|
|
246
|
+
(stream.ended || stream.events.length === 0)) {
|
|
213
247
|
streams.delete(runId);
|
|
214
248
|
}
|
|
215
249
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai",
|