@cubicecho/agent-core 2.0.8 → 2.1.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/README.md +36 -1
- package/dist/capabilities.d.ts +73 -5
- package/dist/capabilities.js +101 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run-turn.d.ts +19 -7
- package/dist/run-turn.js +8 -6
- package/llms.txt +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ only, Node >=22.
|
|
|
23
23
|
| `schema-compat` | Makes an MCP tool schema something a strict or grammar-constrained server will accept. `sanitizeTools`, `relaxTools`, `isGrammarError`. |
|
|
24
24
|
| `tool-loading` | On-demand tool discovery: a name-only catalogue plus a `load_tools` meta-tool, so a run pays for the schemas it asks for instead of all of them. |
|
|
25
25
|
| `stream` | Reads one streamed turn back into a message: token callbacks, tool-call reassembly, and the idle watchdog that turns a silent endpoint into `EndpointSilent`. |
|
|
26
|
-
| `capabilities` | What an endpoint turned out not to support,
|
|
26
|
+
| `capabilities` | What an endpoint turned out not to support — and, under it, what one model on that endpoint did not — plus the loop that answers either when it says so. `capabilitiesFor`, `modelCapabilitiesFor`, `negotiate`. |
|
|
27
27
|
| `side-task` | One-shot calls that support a run without being one — small prompt, short answer, no tools, never worth failing the run over. |
|
|
28
28
|
| `events` | The in-memory bus a watcher reads while a run happens: `emit`, `watch`, `history`, `fold`. A watcher's backlog is capped and reports its own gaps. |
|
|
29
29
|
| `client` | A pooled `OpenAI` client per endpoint, plus the context-window listing and its cache. |
|
|
@@ -70,6 +70,41 @@ const turn = await negotiate(supports, (supports, produced) =>
|
|
|
70
70
|
);
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## What the model refuses, rather than the server
|
|
74
|
+
|
|
75
|
+
`strictSchemas` and `usageInStream` are facts about a server. Three more arrive through the same
|
|
76
|
+
channel — an error string on a chat completion — and are facts about a *model*: a
|
|
77
|
+
`reasoning_effort` it does not take, a ceiling it spells `max_completion_tokens`, a temperature
|
|
78
|
+
that is not ours to pick. They cannot latch on the endpoint, because one API key reaches every
|
|
79
|
+
model a provider offers: the first turn on `gpt-4o` would stop `gpt-5` ever being asked to reason
|
|
80
|
+
again, with the setting still reading `high` and nothing anywhere saying it had stopped.
|
|
81
|
+
|
|
82
|
+
So they hang off the endpoint under the name the endpoint knows the model by. Pass `model` and
|
|
83
|
+
the same loop answers both levels; leave it out and nothing changes.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const turn = await negotiate(supports, (supports, produced, model) =>
|
|
87
|
+
streamTurn(
|
|
88
|
+
getClient(config),
|
|
89
|
+
{
|
|
90
|
+
model: name, messages, stream: true,
|
|
91
|
+
// Each rebuilt per attempt from what this model has already refused.
|
|
92
|
+
...(model?.reasoningEffort ? { reasoning_effort: effort } : {}),
|
|
93
|
+
...(model?.legacyTokenLimit ? { max_tokens: limit } : { max_completion_tokens: limit }),
|
|
94
|
+
...(model?.chosenTemperature ? { temperature } : {}),
|
|
95
|
+
tools: supports.strictSchemas ? declared : relaxTools(declared),
|
|
96
|
+
},
|
|
97
|
+
{ produced, signal },
|
|
98
|
+
),
|
|
99
|
+
{ model: name },
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`runTurn` takes the same option and hands `request` the same second argument. Keying on
|
|
104
|
+
`(endpoint, model)` rather than the model name alone is the part worth keeping: `gpt-4o` at
|
|
105
|
+
OpenAI and `gpt-4o` behind a proxy need not be the same weights, and one that refused a reasoning
|
|
106
|
+
effort must not speak for the other.
|
|
107
|
+
|
|
73
108
|
`send` takes a callback rather than a body because the body has to be rebuilt from the latched
|
|
74
109
|
flags. `produced` is one box per attempt — `streamTurn` sets it as soon as the server says
|
|
75
110
|
anything, and the re-send reads it — so a caller with its own retry budget passes one in
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ import type { Produced } from "./stream.ts";
|
|
|
8
8
|
* about the server on the other end rather than about this process, which is what makes them
|
|
9
9
|
* this package's to hold.
|
|
10
10
|
*/
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* What one endpoint turned out not to support. Both flags start optimistic and only ever latch
|
|
13
|
+
* off; what is about the model rather than the server hangs off `models`.
|
|
14
|
+
*/
|
|
12
15
|
export interface Capabilities {
|
|
13
16
|
/**
|
|
14
17
|
* llama.cpp-backed servers compile every tool schema into one grammar and reject keywords
|
|
@@ -23,14 +26,63 @@ export interface Capabilities {
|
|
|
23
26
|
* happens: the counts are worth one failed call to find out about, not one per run.
|
|
24
27
|
*/
|
|
25
28
|
usageInStream: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* What each model reached through this endpoint turned out not to support, by the name the
|
|
31
|
+
* endpoint knows it as. A second level rather than two more flags beside these, because one
|
|
32
|
+
* API key reaches every model a provider offers: a flag here would let the first turn on a
|
|
33
|
+
* model that cannot reason latch "no reasoning" for every later turn on one that can, which
|
|
34
|
+
* stops asking for it with the setting still reading `high` and nothing anywhere saying it
|
|
35
|
+
* stopped. Empty until `modelCapabilitiesFor` is asked about a model.
|
|
36
|
+
*/
|
|
37
|
+
models: Map<string, ModelCapabilities>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* What one model on that endpoint turned out not to support. All start optimistic and only ever
|
|
41
|
+
* latch off, the same as the endpoint's own.
|
|
42
|
+
*
|
|
43
|
+
* These arrive through the same channel as the endpoint's — an error string on a chat
|
|
44
|
+
* completion — which is why they are negotiated by the same loop rather than a second one. What
|
|
45
|
+
* makes them the model's is that the answer differs between two models the same key reaches.
|
|
46
|
+
*/
|
|
47
|
+
export interface ModelCapabilities {
|
|
48
|
+
/**
|
|
49
|
+
* Takes a `reasoning_effort` at all. A model that cannot reason refuses the field rather than
|
|
50
|
+
* ignoring it, so the whole request fails over a setting that means nothing to it.
|
|
51
|
+
*/
|
|
52
|
+
reasoningEffort: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Spells its ceiling `max_tokens`. The reasoning models want `max_completion_tokens` instead,
|
|
55
|
+
* and they are exactly the models anyone sets an effort on.
|
|
56
|
+
*/
|
|
57
|
+
legacyTokenLimit: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Takes a temperature we picked, rather than only the one it was built with. A reasoning model
|
|
60
|
+
* refuses any other value, including the one a settings row has been showing all along.
|
|
61
|
+
*/
|
|
62
|
+
chosenTemperature: boolean;
|
|
26
63
|
}
|
|
27
64
|
/**
|
|
28
65
|
* What this endpoint is known not to support. The same object every time, so what `negotiate`
|
|
29
66
|
* latches off stays off.
|
|
30
67
|
*
|
|
31
|
-
* @param baseUrl Identifies the endpoint.
|
|
68
|
+
* @param baseUrl Identifies the endpoint. The two flags on it are per-server; what is
|
|
69
|
+
* per-model hangs off `models`, which `modelCapabilitiesFor` reads.
|
|
32
70
|
*/
|
|
33
71
|
export declare function capabilitiesFor(baseUrl: string): Capabilities;
|
|
72
|
+
/**
|
|
73
|
+
* What this model on this endpoint is known not to support. The same object every time, so what
|
|
74
|
+
* `negotiate` latches off stays off.
|
|
75
|
+
*
|
|
76
|
+
* Nested under the endpoint rather than keyed by name alone, because `gpt-4o` at OpenAI and
|
|
77
|
+
* `gpt-4o` behind a proxy need not be the same weights — and a proxy is free to answer to a name
|
|
78
|
+
* it does not really serve. One that refused a reasoning effort must not speak for the other.
|
|
79
|
+
* Bounded by the models actually asked for on that endpoint, which is what a dropdown holds.
|
|
80
|
+
*
|
|
81
|
+
* @param supports The endpoint's own, as `capabilitiesFor` hands it over.
|
|
82
|
+
* @param model The name the endpoint knows the model as — whatever goes in the request body,
|
|
83
|
+
* since that is the only name the refusal is about.
|
|
84
|
+
*/
|
|
85
|
+
export declare function modelCapabilitiesFor(supports: Capabilities, model: string): ModelCapabilities;
|
|
34
86
|
/** Forgets every endpoint's capabilities. For tests, and for a settings change under test. */
|
|
35
87
|
export declare function resetCapabilities(): void;
|
|
36
88
|
/** What `negotiate` takes besides the request. Both optional, both about telling someone. */
|
|
@@ -45,6 +97,15 @@ export interface NegotiateOptions {
|
|
|
45
97
|
produced?: Produced;
|
|
46
98
|
/** Told what was given up on, for a watcher who would otherwise see an unexplained pause. */
|
|
47
99
|
onNotice?: (message: string) => void;
|
|
100
|
+
/**
|
|
101
|
+
* Which model this request is for, by the name the endpoint knows it as.
|
|
102
|
+
*
|
|
103
|
+
* Given one, the refusals that are about the model rather than the server are answered too,
|
|
104
|
+
* and `send` is handed what that model has already refused. Left out, nothing changes — which
|
|
105
|
+
* is the point of it being here rather than a third positional argument: a caller with one
|
|
106
|
+
* model per endpoint, or one that only ever meets the endpoint's own refusals, needs no edit.
|
|
107
|
+
*/
|
|
108
|
+
model?: string;
|
|
48
109
|
}
|
|
49
110
|
/**
|
|
50
111
|
* Sends a request, re-sending it each time the answer is this endpoint refusing something the
|
|
@@ -57,6 +118,11 @@ export interface NegotiateOptions {
|
|
|
57
118
|
* what the second one starts knowing. It terminates in at most one pass per capability, since
|
|
58
119
|
* each pass either latches one off for good or rethrows.
|
|
59
120
|
*
|
|
121
|
+
* One loop over both levels, because a refusal arrives the same way whichever it is about and
|
|
122
|
+
* one request can meet both. An OpenAI reasoning model has two waiting on its own — the ceiling
|
|
123
|
+
* is spelled the other way, and then the temperature is not ours to pick — so a loop that
|
|
124
|
+
* stopped after the first answer would hand the caller the second.
|
|
125
|
+
*
|
|
60
126
|
* `send` is a thunk rather than a request body because the body has to be rebuilt from the
|
|
61
127
|
* latched flags: `relaxTools` applies to the tools that were just sanitised, and
|
|
62
128
|
* `stream_options` is present or absent rather than adjusted. It is generic over what it
|
|
@@ -70,7 +136,9 @@ export interface NegotiateOptions {
|
|
|
70
136
|
*
|
|
71
137
|
* @param supports What this endpoint has already refused. Latched off further as it refuses more.
|
|
72
138
|
* @param send Builds and sends the request. Called again per downgrade, never once tokens
|
|
73
|
-
* have arrived.
|
|
74
|
-
*
|
|
139
|
+
* have arrived. Its third argument is what the named model has refused, absent when no model
|
|
140
|
+
* was named.
|
|
141
|
+
* @param options `produced` for a caller with its own retry budget, `onNotice` for a watcher,
|
|
142
|
+
* `model` to negotiate the model's refusals alongside the endpoint's.
|
|
75
143
|
*/
|
|
76
|
-
export declare function negotiate<T>(supports: Capabilities, send: (supports: Capabilities, produced: Produced) => Promise<T>, { produced, onNotice }?: NegotiateOptions): Promise<T>;
|
|
144
|
+
export declare function negotiate<T>(supports: Capabilities, send: (supports: Capabilities, produced: Produced, model: ModelCapabilities | undefined) => Promise<T>, { produced, onNotice, model: name }?: NegotiateOptions): Promise<T>;
|
package/dist/capabilities.js
CHANGED
|
@@ -16,24 +16,91 @@ const capabilities = new Map();
|
|
|
16
16
|
* What this endpoint is known not to support. The same object every time, so what `negotiate`
|
|
17
17
|
* latches off stays off.
|
|
18
18
|
*
|
|
19
|
-
* @param baseUrl Identifies the endpoint.
|
|
19
|
+
* @param baseUrl Identifies the endpoint. The two flags on it are per-server; what is
|
|
20
|
+
* per-model hangs off `models`, which `modelCapabilitiesFor` reads.
|
|
20
21
|
*/
|
|
21
22
|
export function capabilitiesFor(baseUrl) {
|
|
22
23
|
let known = capabilities.get(baseUrl);
|
|
23
24
|
if (!known) {
|
|
24
|
-
known = { strictSchemas: true, usageInStream: true };
|
|
25
|
+
known = { strictSchemas: true, usageInStream: true, models: new Map() };
|
|
25
26
|
capabilities.set(baseUrl, known);
|
|
26
27
|
}
|
|
27
28
|
return known;
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* What this model on this endpoint is known not to support. The same object every time, so what
|
|
32
|
+
* `negotiate` latches off stays off.
|
|
33
|
+
*
|
|
34
|
+
* Nested under the endpoint rather than keyed by name alone, because `gpt-4o` at OpenAI and
|
|
35
|
+
* `gpt-4o` behind a proxy need not be the same weights — and a proxy is free to answer to a name
|
|
36
|
+
* it does not really serve. One that refused a reasoning effort must not speak for the other.
|
|
37
|
+
* Bounded by the models actually asked for on that endpoint, which is what a dropdown holds.
|
|
38
|
+
*
|
|
39
|
+
* @param supports The endpoint's own, as `capabilitiesFor` hands it over.
|
|
40
|
+
* @param model The name the endpoint knows the model as — whatever goes in the request body,
|
|
41
|
+
* since that is the only name the refusal is about.
|
|
42
|
+
*/
|
|
43
|
+
export function modelCapabilitiesFor(supports, model) {
|
|
44
|
+
let known = supports.models.get(model);
|
|
45
|
+
if (!known) {
|
|
46
|
+
known = { reasoningEffort: true, legacyTokenLimit: true, chosenTemperature: true };
|
|
47
|
+
supports.models.set(model, known);
|
|
48
|
+
}
|
|
49
|
+
return known;
|
|
50
|
+
}
|
|
29
51
|
/** Forgets every endpoint's capabilities. For tests, and for a settings change under test. */
|
|
30
52
|
export function resetCapabilities() {
|
|
31
53
|
capabilities.clear();
|
|
32
54
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Every latching flag in play on one attempt, endpoint and model together, in a stable order.
|
|
57
|
+
* Read positionally and only against another reading of the same two objects: what it answers is
|
|
58
|
+
* whether anything moved while the request was out, and a flag added to either interface later is
|
|
59
|
+
* compared without an edit here. `models` is not one of them — it is the second level, not a
|
|
60
|
+
* flag, and the map is the same object throughout.
|
|
61
|
+
*/
|
|
62
|
+
const flagsOf = (supports, model) => [
|
|
63
|
+
...Object.values(supports).filter((value) => typeof value === "boolean"),
|
|
64
|
+
...(model ? Object.values(model) : []),
|
|
65
|
+
];
|
|
35
66
|
/** `stream_options` is named in the refusal by every server that has not heard of it. */
|
|
36
67
|
const REJECTS_USAGE = /stream_options/i;
|
|
68
|
+
/**
|
|
69
|
+
* A refusal of the *value* rather than of the field, which names the field either way.
|
|
70
|
+
*
|
|
71
|
+
* `Unsupported value: 'reasoning_effort' does not support 'none' with this model. Supported
|
|
72
|
+
* values are: 'minimal', 'low', 'medium', and 'high'.` A model that answers this reasons
|
|
73
|
+
* perfectly well; it was handed an effort off a list this package does not know. Dropping the
|
|
74
|
+
* field succeeds, at the model's own default effort, which is neither what the caller asked for
|
|
75
|
+
* nor something it can see — and the drop latches, so every later turn on that model reasons at
|
|
76
|
+
* the default with the setting still reading what the operator typed.
|
|
77
|
+
*/
|
|
78
|
+
const REFUSED_VALUE = /unsupported value|invalid value|supported values/i;
|
|
79
|
+
/**
|
|
80
|
+
* A model that cannot reason refuses the field by name — and only the field. See `REFUSED_VALUE`
|
|
81
|
+
* for the refusal that names it too and means the opposite, which is the caller's to see rather
|
|
82
|
+
* than ours to work around. `does not support` is deliberately not the marker: a proxy that
|
|
83
|
+
* words a real field refusal as `this model does not support reasoning_effort` has to keep
|
|
84
|
+
* latching.
|
|
85
|
+
*/
|
|
86
|
+
const rejectsEffort = (detail) => /reasoning_effort/i.test(detail) && !REFUSED_VALUE.test(detail);
|
|
87
|
+
/**
|
|
88
|
+
* Read only alongside the name it is asking for: `'max_tokens' is not supported with this model.
|
|
89
|
+
* Use 'max_completion_tokens' instead.`
|
|
90
|
+
*
|
|
91
|
+
* A bare `max_tokens` complaint is also how a server says the *number* was too large —
|
|
92
|
+
* `max_tokens is too large: 200000. This model supports at most 16384.` — and the answer to that
|
|
93
|
+
* is not to send the same number under a different name. It is to let the error out, where
|
|
94
|
+
* whoever typed the number can see it.
|
|
95
|
+
*/
|
|
96
|
+
const wantsCompletionLimit = (detail) => /max_tokens/i.test(detail) && /max_completion_tokens/i.test(detail);
|
|
97
|
+
/**
|
|
98
|
+
* `'temperature' does not support 0.7 with this model. Only the default (1) is supported.`
|
|
99
|
+
*
|
|
100
|
+
* The qualifier is load-bearing. A temperature out of range is the caller's mistake to see
|
|
101
|
+
* rather than ours to work around, and dropping the field would hide it.
|
|
102
|
+
*/
|
|
103
|
+
const refusesChosenTemperature = (detail) => /temperature/i.test(detail) && /only the default|does not support/i.test(detail);
|
|
37
104
|
/**
|
|
38
105
|
* Sends a request, re-sending it each time the answer is this endpoint refusing something the
|
|
39
106
|
* request can do without. Returns once the endpoint has answered, or throws if the refusal is
|
|
@@ -45,6 +112,11 @@ const REJECTS_USAGE = /stream_options/i;
|
|
|
45
112
|
* what the second one starts knowing. It terminates in at most one pass per capability, since
|
|
46
113
|
* each pass either latches one off for good or rethrows.
|
|
47
114
|
*
|
|
115
|
+
* One loop over both levels, because a refusal arrives the same way whichever it is about and
|
|
116
|
+
* one request can meet both. An OpenAI reasoning model has two waiting on its own — the ceiling
|
|
117
|
+
* is spelled the other way, and then the temperature is not ours to pick — so a loop that
|
|
118
|
+
* stopped after the first answer would hand the caller the second.
|
|
119
|
+
*
|
|
48
120
|
* `send` is a thunk rather than a request body because the body has to be rebuilt from the
|
|
49
121
|
* latched flags: `relaxTools` applies to the tools that were just sanitised, and
|
|
50
122
|
* `stream_options` is present or absent rather than adjusted. It is generic over what it
|
|
@@ -58,17 +130,21 @@ const REJECTS_USAGE = /stream_options/i;
|
|
|
58
130
|
*
|
|
59
131
|
* @param supports What this endpoint has already refused. Latched off further as it refuses more.
|
|
60
132
|
* @param send Builds and sends the request. Called again per downgrade, never once tokens
|
|
61
|
-
* have arrived.
|
|
62
|
-
*
|
|
133
|
+
* have arrived. Its third argument is what the named model has refused, absent when no model
|
|
134
|
+
* was named.
|
|
135
|
+
* @param options `produced` for a caller with its own retry budget, `onNotice` for a watcher,
|
|
136
|
+
* `model` to negotiate the model's refusals alongside the endpoint's.
|
|
63
137
|
*/
|
|
64
|
-
export async function negotiate(supports, send, { produced = { any: false }, onNotice } = {}) {
|
|
138
|
+
export async function negotiate(supports, send, { produced = { any: false }, onNotice, model: name } = {}) {
|
|
139
|
+
const model = name === undefined ? undefined : modelCapabilitiesFor(supports, name);
|
|
65
140
|
for (;;) {
|
|
66
|
-
// What this attempt was built with. `capabilitiesFor`
|
|
67
|
-
// everyone on
|
|
68
|
-
// is in flight — and the branches below are guarded
|
|
69
|
-
|
|
141
|
+
// What this attempt was built with. `capabilitiesFor` and `modelCapabilitiesFor` hand one
|
|
142
|
+
// object per endpoint and per model to everyone on them, so a run starting alongside this
|
|
143
|
+
// one may latch a flag off while this call is in flight — and the branches below are guarded
|
|
144
|
+
// on the flag still being set.
|
|
145
|
+
const sent = flagsOf(supports, model);
|
|
70
146
|
try {
|
|
71
|
-
return await send(supports, produced);
|
|
147
|
+
return await send(supports, produced, model);
|
|
72
148
|
}
|
|
73
149
|
catch (error) {
|
|
74
150
|
if (produced.any)
|
|
@@ -82,7 +158,19 @@ export async function negotiate(supports, send, { produced = { any: false }, onN
|
|
|
82
158
|
supports.usageInStream = false;
|
|
83
159
|
onNotice?.("server rejected stream_options; token counts unavailable");
|
|
84
160
|
}
|
|
85
|
-
else if (
|
|
161
|
+
else if (model?.reasoningEffort && rejectsEffort(detail)) {
|
|
162
|
+
model.reasoningEffort = false;
|
|
163
|
+
onNotice?.("model does not take a reasoning effort; retrying without one");
|
|
164
|
+
}
|
|
165
|
+
else if (model?.legacyTokenLimit && wantsCompletionLimit(detail)) {
|
|
166
|
+
model.legacyTokenLimit = false;
|
|
167
|
+
onNotice?.("model wants max_completion_tokens; retrying with the limit spelled that way");
|
|
168
|
+
}
|
|
169
|
+
else if (model?.chosenTemperature && refusesChosenTemperature(detail)) {
|
|
170
|
+
model.chosenTemperature = false;
|
|
171
|
+
onNotice?.("model takes only its own temperature; retrying without ours");
|
|
172
|
+
}
|
|
173
|
+
else if (flagsOf(supports, model).every((flag, index) => flag === sent[index])) {
|
|
86
174
|
throw error;
|
|
87
175
|
}
|
|
88
176
|
// Otherwise the refusal was answered by whoever got there first, and this attempt was
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
|
-
export { type Capabilities, capabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
12
|
+
export { type Capabilities, capabilitiesFor, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
13
13
|
export type { CatalogServer } from "./catalog.ts";
|
|
14
14
|
export { contextLimitFor, getClient, listModels, type ModelInfo, NO_KEY, resetClients, timeoutMs, } from "./client.ts";
|
|
15
15
|
export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
|
-
export { capabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
12
|
+
export { capabilitiesFor, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
13
13
|
export { contextLimitFor, getClient, listModels, NO_KEY, resetClients, timeoutMs, } from "./client.js";
|
|
14
14
|
export { errorMessage } from "./errors.js";
|
|
15
15
|
export { emit, endRun, fold, history, resetEvents, watch, } from "./events.js";
|
package/dist/run-turn.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
|
-
import { type Capabilities } from "./capabilities.ts";
|
|
2
|
+
import { type Capabilities, type ModelCapabilities } from "./capabilities.ts";
|
|
3
3
|
import { type StreamTurnOptions, type Turn } from "./stream.ts";
|
|
4
4
|
/**
|
|
5
5
|
* One turn, given as many attempts as the caller allows.
|
|
6
6
|
*
|
|
7
7
|
* Two different things are being recovered from here, and they nest. The inner one is a
|
|
8
|
-
* capability the endpoint turns out not to have — `stream_options`, a grammar keyword —
|
|
9
|
-
* is a refusal: it is answered by sending a
|
|
10
|
-
*
|
|
8
|
+
* capability the endpoint turns out not to have — `stream_options`, a grammar keyword — or that
|
|
9
|
+
* the model does not, given `model` below. Either is a refusal: it is answered by sending a
|
|
10
|
+
* lesser request, and it latches for the life of the process against the endpoint or against
|
|
11
|
+
* that one model on it, so it costs one failed call rather than one a run.
|
|
11
12
|
* The outer one is the endpoint being unreachable, busy or silent, which is not about this
|
|
12
13
|
* request at all and is worth simply waiting out.
|
|
13
14
|
*
|
|
@@ -40,6 +41,15 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
40
41
|
* run over one would be the guard failing exactly the callers it was meant to help.
|
|
41
42
|
*/
|
|
42
43
|
contextLimit?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Which model the body names, so the refusals that are about the model rather than the server
|
|
46
|
+
* are negotiated too — a reasoning effort it does not take, a token ceiling it spells the
|
|
47
|
+
* other way, a temperature that is not ours to pick. Left out, only the endpoint's own are.
|
|
48
|
+
*
|
|
49
|
+
* It is given here rather than read off the body because the body is built from the answer:
|
|
50
|
+
* `request` has to know what this model refused before it can build one that avoids it.
|
|
51
|
+
*/
|
|
52
|
+
model?: string;
|
|
43
53
|
}
|
|
44
54
|
/**
|
|
45
55
|
* `request` is a callback rather than a body because the body has to be rebuilt from whatever
|
|
@@ -49,7 +59,9 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
49
59
|
*
|
|
50
60
|
* @param client The pooled client for this endpoint.
|
|
51
61
|
* @param supports What the endpoint has already refused, threaded through the negotiation.
|
|
52
|
-
* @param request Builds the body. Called again per attempt, since a downgrade changes it.
|
|
53
|
-
*
|
|
62
|
+
* @param request Builds the body. Called again per attempt, since a downgrade changes it. Its
|
|
63
|
+
* second argument is what the model named in `options.model` has refused, absent when none was.
|
|
64
|
+
* @param options Retry budget, context limit, the model to negotiate for, notices, and the
|
|
65
|
+
* stream's own callbacks.
|
|
54
66
|
*/
|
|
55
|
-
export declare function runTurn(client: OpenAI, supports: Capabilities, request: (supports: Capabilities) => OpenAI.ChatCompletionCreateParamsStreaming, { maxRetries, onNotice, contextLimit, ...stream }?: RunTurnOptions): Promise<Turn>;
|
|
67
|
+
export declare function runTurn(client: OpenAI, supports: Capabilities, request: (supports: Capabilities, model: ModelCapabilities | undefined) => OpenAI.ChatCompletionCreateParamsStreaming, { maxRetries, onNotice, contextLimit, model, ...stream }?: RunTurnOptions): Promise<Turn>;
|
package/dist/run-turn.js
CHANGED
|
@@ -10,17 +10,19 @@ import { streamTurn } from "./stream.js";
|
|
|
10
10
|
*
|
|
11
11
|
* @param client The pooled client for this endpoint.
|
|
12
12
|
* @param supports What the endpoint has already refused, threaded through the negotiation.
|
|
13
|
-
* @param request Builds the body. Called again per attempt, since a downgrade changes it.
|
|
14
|
-
*
|
|
13
|
+
* @param request Builds the body. Called again per attempt, since a downgrade changes it. Its
|
|
14
|
+
* second argument is what the model named in `options.model` has refused, absent when none was.
|
|
15
|
+
* @param options Retry budget, context limit, the model to negotiate for, notices, and the
|
|
16
|
+
* stream's own callbacks.
|
|
15
17
|
*/
|
|
16
|
-
export async function runTurn(client, supports, request, { maxRetries = 0, onNotice, contextLimit = 0, ...stream } = {}) {
|
|
18
|
+
export async function runTurn(client, supports, request, { maxRetries = 0, onNotice, contextLimit = 0, model, ...stream } = {}) {
|
|
17
19
|
// Sized once rather than per build. `request` is called again for every downgrade and every
|
|
18
20
|
// retry, but a downgraded body is strictly smaller than the one before it and the transcript
|
|
19
21
|
// does not change between attempts — so the first body is the one worth measuring, and
|
|
20
22
|
// measuring the rest would only spend the walk again to reach the same answer.
|
|
21
23
|
let sized = false;
|
|
22
|
-
const measured = (capabilities) => {
|
|
23
|
-
const body = request(capabilities);
|
|
24
|
+
const measured = (capabilities, forModel) => {
|
|
25
|
+
const body = request(capabilities, forModel);
|
|
24
26
|
if (!sized && contextLimit >= SMALLEST_LIKELY_WINDOW) {
|
|
25
27
|
sized = true;
|
|
26
28
|
const needed = requestTokens(body);
|
|
@@ -36,7 +38,7 @@ export async function runTurn(client, supports, request, { maxRetries = 0, onNot
|
|
|
36
38
|
for (let attempt = 0;; attempt++) {
|
|
37
39
|
const produced = { any: false };
|
|
38
40
|
try {
|
|
39
|
-
return await negotiate(supports, (capabilities, box) => streamTurn(client, measured(capabilities), { ...stream, produced: box }), { produced, onNotice });
|
|
41
|
+
return await negotiate(supports, (capabilities, box, forModel) => streamTurn(client, measured(capabilities, forModel), { ...stream, produced: box }), { produced, onNotice, model });
|
|
40
42
|
}
|
|
41
43
|
catch (error) {
|
|
42
44
|
// The abort is read before the classification, not after. A run stopped by its operator
|
package/llms.txt
CHANGED
|
@@ -15,6 +15,8 @@ What an endpoint turned out not to support, and answering it when it says so.
|
|
|
15
15
|
|
|
16
16
|
- `Capabilities` (type) — What one endpoint turned out not to support.
|
|
17
17
|
- `capabilitiesFor` — What this endpoint is known not to support.
|
|
18
|
+
- `ModelCapabilities` (type) — What one model on that endpoint turned out not to support.
|
|
19
|
+
- `modelCapabilitiesFor` — What this model on this endpoint is known not to support.
|
|
18
20
|
- `NegotiateOptions` (type) — What `negotiate` takes besides the request.
|
|
19
21
|
- `negotiate` — Sends a request, re-sending it each time the answer is this endpoint refusing something the request can do without.
|
|
20
22
|
- `resetCapabilities` — Forgets every endpoint's capabilities.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|