@cubicecho/agent-core 2.0.3 → 2.0.5

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.
@@ -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
  }
@@ -88,6 +88,7 @@ export declare function expandNames(requested: string[], catalog: CatalogServer[
88
88
  name: string;
89
89
  hits: string[];
90
90
  }[];
91
+ deferred: string[];
91
92
  };
92
93
  /**
93
94
  * What `load_tools` reports back: the descriptions, now that they are worth their tokens.
@@ -95,7 +96,7 @@ export declare function expandNames(requested: string[], catalog: CatalogServer[
95
96
  * @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
96
97
  * @param catalog The servers, read for the descriptions now worth their tokens.
97
98
  */
98
- export declare function loadResult({ matched, unknown, overBroad }: ReturnType<typeof expandNames>, catalog: CatalogServer[]): string;
99
+ export declare function loadResult({ matched, unknown, overBroad, deferred }: ReturnType<typeof expandNames>, catalog: CatalogServer[]): string;
99
100
  /**
100
101
  * Whether the catalogue holds a tool by this name.
101
102
  *
@@ -137,6 +137,7 @@ export function expandNames(requested, catalog) {
137
137
  const matched = new Set();
138
138
  const unknown = [];
139
139
  const overBroad = [];
140
+ const deferred = [];
140
141
  const known = new Set(all.map((tool) => tool.name));
141
142
  const resolve = (name) => {
142
143
  if (name.endsWith("*")) {
@@ -172,13 +173,21 @@ export function expandNames(requested, catalog) {
172
173
  // model that is meant to be choosing from twelve. Already-matched names are free, so a
173
174
  // name that only repeats an earlier one never spends budget.
174
175
  const fresh = hits.filter((hit) => !matched.has(hit));
175
- if (matched.size + fresh.length > MAX_PER_LOAD)
176
+ // Two different refusals, and answering both with "narrow it down" made one of them
177
+ // impossible to act on. A name that would not fit an empty call is over-broad, and the
178
+ // names are what the model needs. A name that only does not fit *this* call is precise
179
+ // enough already — telling a model that asked for one exact tool that it matches one tool,
180
+ // "more than the twelve one call may load", and to choose from a list holding just that
181
+ // name, leaves it nothing to do but send the identical call again.
182
+ if (fresh.length > MAX_PER_LOAD)
176
183
  overBroad.push({ name, hits });
184
+ else if (matched.size + fresh.length > MAX_PER_LOAD)
185
+ deferred.push(name);
177
186
  else
178
187
  for (const hit of hits)
179
188
  matched.add(hit);
180
189
  }
181
- return { matched: [...matched], unknown, overBroad };
190
+ return { matched: [...matched], unknown, overBroad, deferred };
182
191
  }
183
192
  /**
184
193
  * What `load_tools` reports back: the descriptions, now that they are worth their tokens.
@@ -186,7 +195,7 @@ export function expandNames(requested, catalog) {
186
195
  * @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
187
196
  * @param catalog The servers, read for the descriptions now worth their tokens.
188
197
  */
189
- export function loadResult({ matched, unknown, overBroad }, catalog) {
198
+ export function loadResult({ matched, unknown, overBroad, deferred }, catalog) {
190
199
  const byName = new Map(flatten(catalog).map((tool) => [tool.name, tool.description]));
191
200
  const lines = [];
192
201
  if (matched.length) {
@@ -199,6 +208,11 @@ export function loadResult({ matched, unknown, overBroad }, catalog) {
199
208
  lines.push("");
200
209
  lines.push(`\`${name}\` matches ${hits.length} tools, more than the ${MAX_PER_LOAD} one call may load.`, "Name the ones you need from:", ...hits.map((hit) => ` ${hit}`));
201
210
  }
211
+ if (deferred.length) {
212
+ if (lines.length)
213
+ lines.push("");
214
+ lines.push(`This call is full at ${MAX_PER_LOAD} tools, so these were not loaded: ${deferred.join(", ")}.`, "Ask for them on your next step.");
215
+ }
202
216
  if (unknown.length) {
203
217
  if (lines.length)
204
218
  lines.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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",