@cubicecho/agent-core 1.2.0 → 1.3.0

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 CHANGED
@@ -29,6 +29,7 @@ only, Node >=22.
29
29
  | `client` | A pooled `OpenAI` client per endpoint, plus the context-window listing and its cache. |
30
30
  | `retry` | What to do when a request is lost, refused or too big: `isTransient`, `backoffMs`, `ContextOverflow`, `EndpointSilent`. |
31
31
  | `config` | The structural interfaces every function here asks for. |
32
+ | `run-turn` | `runTurn`: one turn with the retry loop around the negotiation around the stream. The whole loop, for a caller that wants it rather than its parts. |
32
33
  | `reset` | `resetAll`: drops every cache and latch in one call, so a teardown cannot forget one. |
33
34
  | `errors` | `errorMessage`: a caught `unknown` turned into something a run row can hold. |
34
35
  | `catalog` | `CatalogServer`: the name-only shape `tool-loading` reads a connected server as. |
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { errorMessage } from "./errors.ts";
17
17
  export { emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunUsage, reset, watch, } from "./events.ts";
18
18
  export { resetAll } from "./reset.ts";
19
19
  export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.ts";
20
+ export { type RunTurnOptions, runTurn } from "./run-turn.ts";
20
21
  export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
21
22
  export { ask, clean, listLines, parseJson, resetHints, type SideTaskOptions, tryAsk, } from "./side-task.ts";
22
23
  export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from "./stream.ts";
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export { errorMessage } from "./errors.js";
15
15
  export { emit, endRun, fold, history, reset, watch, } from "./events.js";
16
16
  export { resetAll } from "./reset.js";
17
17
  export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
18
+ export { runTurn } from "./run-turn.js";
18
19
  export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
19
20
  export { ask, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
20
21
  export { streamTurn, } from "./stream.js";
@@ -0,0 +1,38 @@
1
+ import type OpenAI from "openai";
2
+ import { type Capabilities } from "./capabilities.ts";
3
+ import { type StreamTurnOptions, type Turn } from "./stream.ts";
4
+ /**
5
+ * One turn, given as many attempts as the caller allows.
6
+ *
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 — which
9
+ * is a refusal: it is answered by sending a lesser request, and it latches against that
10
+ * endpoint for the life of the process, so it costs one failed call rather than one a run.
11
+ * The outer one is the endpoint being unreachable, busy or silent, which is not about this
12
+ * request at all and is worth simply waiting out.
13
+ *
14
+ * Both are bounded by the same rule: nothing is sent again once the server has started
15
+ * answering. The tokens are already out and on their way to whoever is watching, and a second
16
+ * attempt would say everything twice. That is what `produced` is, one box per attempt.
17
+ */
18
+ /** A retry is not the same event as a downgrade, but a watcher wants to be told about both. */
19
+ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
20
+ /**
21
+ * How many times a lost request is worth sending again. Zero is one attempt, which is the
22
+ * default because a caller with no retry budget in its settings should not inherit one.
23
+ * A downgrade does not spend an attempt: it is a different request, not the same one again.
24
+ */
25
+ maxRetries?: number;
26
+ /**
27
+ * Told what was given up on and what is being waited out, for a watcher who would otherwise
28
+ * see an unexplained pause. Carries both the capability notices and the retry notices.
29
+ */
30
+ onNotice?: (message: string) => void;
31
+ }
32
+ /**
33
+ * `request` is a callback rather than a body because the body has to be rebuilt from whatever
34
+ * the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools`
35
+ * has to apply to the schemas that were just sanitised. It is handed the same `Capabilities`
36
+ * object throughout, and a caller that reads those from its own closure can ignore the argument.
37
+ */
38
+ export declare function runTurn(client: OpenAI, supports: Capabilities, request: (supports: Capabilities) => OpenAI.ChatCompletionCreateParamsStreaming, { maxRetries, onNotice, ...stream }?: RunTurnOptions): Promise<Turn>;
@@ -0,0 +1,33 @@
1
+ import { negotiate } from "./capabilities.js";
2
+ import { errorMessage } from "./errors.js";
3
+ import { backoffMs, isTransient, sleep } from "./retry.js";
4
+ import { streamTurn } from "./stream.js";
5
+ /**
6
+ * `request` is a callback rather than a body because the body has to be rebuilt from whatever
7
+ * the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools`
8
+ * has to apply to the schemas that were just sanitised. It is handed the same `Capabilities`
9
+ * object throughout, and a caller that reads those from its own closure can ignore the argument.
10
+ */
11
+ export async function runTurn(client, supports, request, { maxRetries = 0, onNotice, ...stream } = {}) {
12
+ for (let attempt = 0;; attempt++) {
13
+ const produced = { any: false };
14
+ try {
15
+ return await negotiate(supports, (capabilities, box) => streamTurn(client, request(capabilities), { ...stream, produced: box }), { produced, onNotice });
16
+ }
17
+ catch (error) {
18
+ // The abort is read before the classification, not after. A run stopped by its operator
19
+ // can trip the idle watchdog on the way out, and `EndpointSilent` is transient by the
20
+ // rules in `retry.ts` — so classifying first brings a cancelled run back from the dead.
21
+ if (produced.any || stream.signal?.aborted)
22
+ throw error;
23
+ if (attempt >= maxRetries || !isTransient(error))
24
+ throw error;
25
+ const wait = backoffMs(attempt);
26
+ // Reported in whatever unit reads as a number: the first backoff is under a second, and
27
+ // "retrying in 0s" is what rounding it to seconds says.
28
+ const delay = wait < 1000 ? `${Math.round(wait)}ms` : `${Math.round(wait / 1000)}s`;
29
+ onNotice?.(`${errorMessage(error)} — retrying in ${delay} (${attempt + 1}/${maxRetries})`);
30
+ await sleep(wait, stream.signal);
31
+ }
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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",