@cubicecho/agent-core 2.2.2 → 2.2.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/README.md +13 -4
- package/dist/capabilities.js +27 -3
- package/dist/run-turn.d.ts +4 -2
- package/dist/stream.d.ts +8 -2
- package/dist/stream.js +11 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ the exports rather than a second description of them; CI fails if the committed
|
|
|
46
46
|
|
|
47
47
|
`negotiate` wrapping `streamTurn` is the whole of one turn against an endpoint: the request is
|
|
48
48
|
re-sent for as long as the answer is this server refusing something the request can do without,
|
|
49
|
-
and nothing is re-sent once
|
|
49
|
+
and nothing is re-sent once the model has started answering.
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
52
|
import {
|
|
@@ -82,6 +82,12 @@ again, with the setting still reading `high` and nothing anywhere saying it had
|
|
|
82
82
|
So they hang off the endpoint under the name the endpoint knows the model by. Pass `model` and
|
|
83
83
|
the same loop answers both levels; leave it out and nothing changes.
|
|
84
84
|
|
|
85
|
+
A refusal of the *value* is not one of these, however alike the two read: an effort off a list
|
|
86
|
+
this package does not know, a `max_tokens` larger than the model's ceiling, a temperature out of
|
|
87
|
+
range. Dropping the field answers those too — at the model's own default, latched for the rest of
|
|
88
|
+
the process, with the settings row still reading what was typed and nothing saying it had stopped
|
|
89
|
+
meaning it. They are passed to the caller instead, where whoever typed the number can see it.
|
|
90
|
+
|
|
85
91
|
```ts
|
|
86
92
|
const turn = await negotiate(supports, (supports, produced, model) =>
|
|
87
93
|
streamTurn(
|
|
@@ -116,9 +122,12 @@ OpenAI and `gpt-4o` behind a proxy need not be the same weights, and one that re
|
|
|
116
122
|
effort must not speak for the other.
|
|
117
123
|
|
|
118
124
|
`send` takes a callback rather than a body because the body has to be rebuilt from the latched
|
|
119
|
-
flags. `produced` is one box per attempt — `streamTurn` sets it
|
|
120
|
-
|
|
121
|
-
(`{ produced }`) and reads it afterwards to decide whether the failure
|
|
125
|
+
flags. `produced` is one box per attempt — `streamTurn` sets it on the first chunk that carries
|
|
126
|
+
text, reasoning or a piece of a tool call, and the re-send reads it — so a caller with its own
|
|
127
|
+
retry budget passes one in (`{ produced }`) and reads it afterwards to decide whether the failure
|
|
128
|
+
is worth another attempt. The content-free `{"role":"assistant"}` most servers open a stream with
|
|
129
|
+
does not set it: nothing has been shown to anybody yet, so an endpoint that primes the stream and
|
|
130
|
+
then wedges is retried like one that never answered at all.
|
|
122
131
|
|
|
123
132
|
`idleMs` is silence, not a deadline: the timer is rearmed on every chunk, so a model that is
|
|
124
133
|
still talking is never cut off however long it takes, and one that has stopped answering raises
|
package/dist/capabilities.js
CHANGED
|
@@ -94,13 +94,37 @@ const rejectsEffort = (detail) => /reasoning_effort/i.test(detail) && !REFUSED_V
|
|
|
94
94
|
* whoever typed the number can see it.
|
|
95
95
|
*/
|
|
96
96
|
const wantsCompletionLimit = (detail) => /max_tokens/i.test(detail) && /max_completion_tokens/i.test(detail);
|
|
97
|
+
/**
|
|
98
|
+
* Temperature named as the field being refused, rather than mentioned in passing.
|
|
99
|
+
*
|
|
100
|
+
* A bare search of the message is not that question. `Unsupported value: 'top_p' does not
|
|
101
|
+
* support 3 with this model. Adjust temperature instead.` refuses another field and merely says
|
|
102
|
+
* the word, and it disabled ours — the word is ordinary English about a model, so any advice,
|
|
103
|
+
* aside or list that reaches for it counted. OpenAI quotes the field it is refusing, which is
|
|
104
|
+
* the anchor; the second half is a server that writes the same clause without the quotes.
|
|
105
|
+
*/
|
|
106
|
+
const NAMES_TEMPERATURE = /(['"`])temperature\1|\btemperature\s+(?:is|does|must|can)\b/i;
|
|
97
107
|
/**
|
|
98
108
|
* `'temperature' does not support 0.7 with this model. Only the default (1) is supported.`
|
|
99
109
|
*
|
|
100
|
-
* The qualifier is load-bearing
|
|
101
|
-
*
|
|
110
|
+
* The qualifier is load-bearing, and `does not support` is not the qualifier: that is how a
|
|
111
|
+
* server words a refusal of the *value* — `Invalid value: 'temperature' does not support 5.
|
|
112
|
+
* Supported values are between 0 and 2.` — which is the caller's mistake to see rather than
|
|
113
|
+
* ours to work around. Dropping the field there succeeds, at the model's own default, so a
|
|
114
|
+
* number the operator typed and can still read on a settings row is silently replaced by one
|
|
115
|
+
* nobody chose, for the life of the process. `only the default` is the whole of what separates
|
|
116
|
+
* the two: it says the field takes one value, where the generic phrasing says only that this
|
|
117
|
+
* value was the wrong one.
|
|
118
|
+
*
|
|
119
|
+
* `REFUSED_VALUE` cannot be borrowed from `rejectsEffort` to draw that line, which is why this
|
|
120
|
+
* has no such guard rather than having lost one — the genuine refusal above opens with
|
|
121
|
+
* `Unsupported value:` too, so the guard would reject the one message that has to latch.
|
|
122
|
+
*
|
|
123
|
+
* It under-matches an endpoint that refuses the field in some third wording, which is the
|
|
124
|
+
* direction this file argues for on `max_tokens` and on `reasoning_effort`: a false negative
|
|
125
|
+
* costs one visible error, and a false positive quietly changes what every later request means.
|
|
102
126
|
*/
|
|
103
|
-
const refusesChosenTemperature = (detail) =>
|
|
127
|
+
const refusesChosenTemperature = (detail) => NAMES_TEMPERATURE.test(detail) && /only the default/i.test(detail);
|
|
104
128
|
/**
|
|
105
129
|
* Sends a request, re-sending it each time the answer is this endpoint refusing something the
|
|
106
130
|
* request can do without. Returns once the endpoint has answered, or throws if the refusal is
|
package/dist/run-turn.d.ts
CHANGED
|
@@ -12,9 +12,11 @@ import { type StreamTurnOptions, type Turn } from "./stream.ts";
|
|
|
12
12
|
* The outer one is the endpoint being unreachable, busy or silent, which is not about this
|
|
13
13
|
* request at all and is worth simply waiting out.
|
|
14
14
|
*
|
|
15
|
-
* Both are bounded by the same rule: nothing is sent again once the
|
|
15
|
+
* Both are bounded by the same rule: nothing is sent again once the model has started
|
|
16
16
|
* answering. The tokens are already out and on their way to whoever is watching, and a second
|
|
17
|
-
* attempt would say everything twice. That is what `produced` is, one box per attempt
|
|
17
|
+
* attempt would say everything twice. That is what `produced` is, one box per attempt — set by
|
|
18
|
+
* a chunk that carried something rather than by a chunk arriving, so the empty opening chunk
|
|
19
|
+
* most servers send does not cost the retry.
|
|
18
20
|
*/
|
|
19
21
|
/** A retry is not the same event as a downgrade, but a watcher wants to be told about both. */
|
|
20
22
|
export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
package/dist/stream.d.ts
CHANGED
|
@@ -22,13 +22,19 @@ export interface Turn {
|
|
|
22
22
|
usage: TurnUsage;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Whether the
|
|
25
|
+
* Whether the model has said anything a second attempt would say twice.
|
|
26
26
|
*
|
|
27
27
|
* A box rather than a return value because it has to be readable *while* the request is in
|
|
28
28
|
* flight: the rules in `retry.ts` are built on the premise that a stream which has already
|
|
29
29
|
* emitted tokens must never be replayed, and by the time a rejected promise is in hand the turn
|
|
30
30
|
* is over. There is one of these per attempt, shared by everything that has a say in whether
|
|
31
31
|
* the attempt is repeated. See `negotiate`.
|
|
32
|
+
*
|
|
33
|
+
* What sets it is a chunk that carried something — text, reasoning, or a piece of a tool call —
|
|
34
|
+
* rather than a chunk arriving. Those read as the same sentence until a server puts an empty
|
|
35
|
+
* chunk between them, and most of them do: a stream usually opens with a content-free
|
|
36
|
+
* `{"role":"assistant"}` that shows nobody anything, and a turn that latched on it could never
|
|
37
|
+
* be retried however early it then died.
|
|
32
38
|
*/
|
|
33
39
|
export interface Produced {
|
|
34
40
|
any: boolean;
|
|
@@ -43,7 +49,7 @@ export interface StreamTurnOptions {
|
|
|
43
49
|
* needs.
|
|
44
50
|
*/
|
|
45
51
|
idleMs?: number;
|
|
46
|
-
/** Set
|
|
52
|
+
/** Set by the first chunk that carries anything, so a failed call knows if it can be retried. */
|
|
47
53
|
produced?: Produced;
|
|
48
54
|
/** The model's scratchpad, as it arrives. */
|
|
49
55
|
onThinking?: (delta: string) => void;
|
package/dist/stream.js
CHANGED
|
@@ -52,8 +52,8 @@ export async function streamTurn(client, body, { signal, idleMs, produced, onThi
|
|
|
52
52
|
const calls = new Map();
|
|
53
53
|
const usage = { prompt: 0, completion: 0, total: 0 };
|
|
54
54
|
for await (const chunk of stream) {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Rearmed on every chunk, latched below on only some: a priming chunk is the endpoint
|
|
56
|
+
// being alive, which is all the watchdog is asking about.
|
|
57
57
|
rearm();
|
|
58
58
|
// Assigned rather than accumulated. `stream_options.include_usage` sends one final chunk
|
|
59
59
|
// and the two agree there, but a server that reports cumulatively per chunk makes a sum
|
|
@@ -68,6 +68,15 @@ export async function streamTurn(client, body, { signal, idleMs, produced, onThi
|
|
|
68
68
|
if (!delta)
|
|
69
69
|
continue;
|
|
70
70
|
const thinking = delta.reasoning_content || delta.reasoning || "";
|
|
71
|
+
// Latched on what the chunk carried, not on its having arrived. Most OpenAI-compatible
|
|
72
|
+
// servers open a stream with a content-free `{"role":"assistant"}` before the first
|
|
73
|
+
// token; latching on that made an endpoint that primes and then wedges unrepeatable,
|
|
74
|
+
// which is exactly the case the watchdog raises `EndpointSilent` for. Tool-call
|
|
75
|
+
// fragments count even though no callback reports them: a partial call is state the turn
|
|
76
|
+
// has accumulated, and losing a retry is the safer half of that trade. Set before the
|
|
77
|
+
// callbacks, so a watcher that throws mid-token cannot be told the same token twice.
|
|
78
|
+
if (produced && (thinking || delta.content || delta.tool_calls?.length))
|
|
79
|
+
produced.any = true;
|
|
71
80
|
if (thinking)
|
|
72
81
|
onThinking?.(thinking);
|
|
73
82
|
if (delta.content) {
|
package/llms.txt
CHANGED
|
@@ -115,7 +115,7 @@ One-shot calls that support a run without being one: picking tools, naming a ses
|
|
|
115
115
|
|
|
116
116
|
Reading one streamed turn back into a message.
|
|
117
117
|
|
|
118
|
-
- `Produced` (type) — Whether the
|
|
118
|
+
- `Produced` (type) — Whether the model has said anything a second attempt would say twice.
|
|
119
119
|
- `StreamTurnOptions` (type) — What `streamTurn` takes besides the request body.
|
|
120
120
|
- `streamTurn` — Runs one turn as a stream, reporting tokens as they arrive and assembling them back into a message.
|
|
121
121
|
- `Turn` (type) — One streamed turn, put back together into the shape a loop and a transcript work with.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.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",
|