@cubicecho/agent-core 4.17.0 → 4.19.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 +26 -9
- package/dist/index.d.ts +2 -2
- package/dist/run/agent-loop.d.ts +12 -6
- package/dist/run/agent-loop.js +45 -26
- package/dist/run/events.d.ts +55 -7
- package/dist/run/events.js +12 -4
- package/dist/run/run-calls.d.ts +2 -6
- package/dist/run/run-calls.js +31 -10
- package/dist/turn/continuation.d.ts +3 -1
- package/dist/turn/continuation.js +29 -2
- package/dist/wire/errors.d.ts +21 -6
- package/dist/wire/errors.js +8 -0
- package/dist/wire/stream.d.ts +93 -1
- package/dist/wire/stream.js +54 -1
- package/llms.txt +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,6 +213,11 @@ is worth another attempt. The content-free `{"role":"assistant"}` most servers o
|
|
|
213
213
|
does not set it: nothing has been shown to anybody yet, so an endpoint that primes the stream and
|
|
214
214
|
then wedges is retried like one that never answered at all.
|
|
215
215
|
|
|
216
|
+
`inFlight` is the other box `streamTurn` takes, for what an attempt had said once it has thrown:
|
|
217
|
+
each call sets its `read` as it starts, so after a retry it reads the attempt that failed last and
|
|
218
|
+
nothing of the one before. `runTurn` and `continueTurn` pass it through, the second with the
|
|
219
|
+
answer it is continuing joined on, and `runAgentLoop` reads it into a failure's `partial`.
|
|
220
|
+
|
|
216
221
|
`idleMs` is silence, not a deadline: the timer is rearmed on every chunk, so a model that is still
|
|
217
222
|
talking is never cut off however long it takes, and one that has stopped answering raises
|
|
218
223
|
`EndpointSilent` rather than hanging the run. `timeoutMs(config)` returns `undefined` for a
|
|
@@ -821,7 +826,12 @@ failure short of a stop, the cut-off answer is kept with a notice. The cap is on
|
|
|
821
826
|
continue, one to find out.
|
|
822
827
|
|
|
823
828
|
Every turn ends in a `usage` event, whether or not the endpoint reported tokens. Its totals are the
|
|
824
|
-
run's so far
|
|
829
|
+
run's so far — `promptTokens`, `completionTokens`, `totalTokens`, `cachedTokens` and
|
|
830
|
+
`cacheReported`, the `UsageTotals` spelling that `result.usage` and a failed run's `usage` share
|
|
831
|
+
(their older `prompt`, `completion`, `total` and `cached` are still there, deprecated). A
|
|
832
|
+
`cachedTokens` of zero is a miss only where `cacheReported` is true; false is an endpoint that never
|
|
833
|
+
said. Its `turn` is that turn's own `TurnUsage` and `finishReason`, with the `contextLength` the loop
|
|
834
|
+
was told. The loop adds what only
|
|
825
835
|
it can know: `toolsDeclared` and `toolSchemaTokens` for the tool block it sent, and, from the second
|
|
826
836
|
request on, `cacheExpected` (the previous prompt plus its reply) and — where a cache count was
|
|
827
837
|
reported — `cacheBroken`, a hit short of 90% of the previous prompt. A broken cache is given a
|
|
@@ -850,10 +860,12 @@ a lesser body and a continuation with the reply so far on the end, and neither i
|
|
|
850
860
|
|
|
851
861
|
`runMetrics(events)` adds a run up from those events — tokens, cache hit ratio and breaks by
|
|
852
862
|
reason, prefill, decode and tool time, the slowest turn, mean time to first token, draft
|
|
853
|
-
acceptance, the largest prompt against
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
863
|
+
acceptance, the largest prompt against its window, turns cut off, tool errors by name, what
|
|
864
|
+
`load_tools` resolved to (`toolsLoaded`, `redundantLoads`, `unknownToolNames`, off each load's
|
|
865
|
+
result as `loads`), and an `outcome`. The loop returns it as `metrics`. Every turn carries its own
|
|
866
|
+
window and every load its own counts, so a host that runs one loop per step gets the whole run from
|
|
867
|
+
`runMetrics` over all the loops' events, with no per-loop result to merge. It is a sibling of `fold`
|
|
868
|
+
rather than part of it, since `fold`'s blocks are for display and a summary is not one:
|
|
857
869
|
|
|
858
870
|
```ts
|
|
859
871
|
const metrics = runMetrics(history(runId), { contextLength: config.contextLength });
|
|
@@ -946,7 +958,7 @@ try {
|
|
|
946
958
|
store.add(sessionId, turn?.reasoning ? { ...message, reasoning_content: turn.reasoning } : message),
|
|
947
959
|
});
|
|
948
960
|
} catch (error) {
|
|
949
|
-
const run = failedRun(error); // messages, usage, toolCalls, loaded, used, preselected — or undefined
|
|
961
|
+
const run = failedRun(error); // messages, partial, usage, toolCalls, loaded, used, preselected — or undefined
|
|
950
962
|
if (run) await store.keep(sessionId, { usage: run.usage, loaded: run.loaded });
|
|
951
963
|
if (signal.aborted) return; // a stop, told by the signal as it always was
|
|
952
964
|
if (error instanceof ContextOverflow) return compactAndRetry(run?.messages);
|
|
@@ -973,9 +985,14 @@ A failure while a step's tools are running leaves a transcript that can be sent
|
|
|
973
985
|
that came back are kept, a call the run was stopped during is answered `Stopped before this call
|
|
974
986
|
finished.`, and one the loop never reached `Not run: the run stopped first.` — a call with no
|
|
975
987
|
result is a transcript an endpoint refuses. Those are announced through `onMessage` like any other
|
|
976
|
-
result. The reply of a request that was in flight when the run stopped
|
|
977
|
-
|
|
978
|
-
|
|
988
|
+
result. The reply of a request that was in flight when the run stopped or failed — a stop, the
|
|
989
|
+
endpoint going silent, a stream that broke after its first token — is not in `messages`, because it
|
|
990
|
+
is not a turn an endpoint will take back. It is on the failure as `partial`: its `content`, its
|
|
991
|
+
`reasoning` and the `toolCalls` it had begun, assembled as far as they had arrived, with a
|
|
992
|
+
continuation's piece joined onto the answer it was continuing and, after a retry, only the attempt
|
|
993
|
+
that was in flight. A request that had said nothing leaves no `partial`, and neither does a
|
|
994
|
+
failure while the tools run, since that step's reply is already in `messages`. Whether half an
|
|
995
|
+
answer belongs in the history is the host's call.
|
|
979
996
|
|
|
980
997
|
`resolveApiKey` is exported and not applied, because which key an endpoint gets is a rule a
|
|
981
998
|
consumer states and a library guessing it could send one where it was not meant to go. The rule
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { CACHE_PROMPT_FIELD, resetSlots, SESSION_HEADER, SLOT_FIELD, type SlotAl
|
|
|
20
20
|
export { HOOK_EVENTS, HookEvent, INJECT_EVENTS } from './hooks/hook-events.ts';
|
|
21
21
|
export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, type HookContext, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from './hooks/hooks.ts';
|
|
22
22
|
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, type StepWindow, } from './run/agent-loop.ts';
|
|
23
|
-
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, RunEventKind, type RunMetrics, type RunMetricsOptions, RunOutcome, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from './run/events.ts';
|
|
23
|
+
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type LoadCounts, type RunEvent, type RunEventInput, RunEventKind, type RunMetrics, type RunMetricsOptions, RunOutcome, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from './run/events.ts';
|
|
24
24
|
export { buildBody } from './run/request-body.ts';
|
|
25
25
|
export { preview, type ToolDispatch } from './run/run-calls.ts';
|
|
26
26
|
export { resetAll } from './runtime/reset.ts';
|
|
@@ -37,7 +37,7 @@ export { type RunTurnOptions, runTurn } from './turn/run-turn.ts';
|
|
|
37
37
|
export { type AskJsonOptions, ask, askJson, askReply, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, type SideTaskReply, taskCall, tryAsk, } from './turn/side-task.ts';
|
|
38
38
|
export { AgentLoopError, type AgentLoopFailure, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from './wire/errors.ts';
|
|
39
39
|
export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow, isTransient, SMALLEST_LIKELY_WINDOW, } from './wire/retry.ts';
|
|
40
|
-
export { CacheBreakReason, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from './wire/stream.ts';
|
|
40
|
+
export { CacheBreakReason, type InFlight, type LoopUsage, type PartialTurn, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, type UsageTotals, } from './wire/stream.ts';
|
|
41
41
|
export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, SPLIT_OUTPUT, SPLIT_REASONING, type Split, stripThinking, THINK_FENCE, } from './wire/thinking.ts';
|
|
42
42
|
export { type ContextBreakdown, type ContextBreakdownOptions, compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, type TokenEstimateOptions, } from './wire/tokens.ts';
|
|
43
43
|
export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, type EndedBy, parseToolArguments, type ResourceLink, type RunArtifact, recoverToolCalls, type ServerLogEntry, ServerLogLevel, type ToolAnswer, ToolArgumentsError, type ToolCall, type ToolCallOutcome, type ToolCallReport, type ToolCallRequest, type ToolCallResult, } from './wire/tool-calls.ts';
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type ServerInstructionsOptions } from '../tools/instructions.ts';
|
|
|
9
9
|
import { type KeywordPreselectOptions } from '../tools/preselect.ts';
|
|
10
10
|
import { type ToolOrder } from '../tools/tool-loading.ts';
|
|
11
11
|
import { type SideTask } from '../turn/side-task.ts';
|
|
12
|
-
import { type
|
|
12
|
+
import { type LoopUsage, type Turn } from '../wire/stream.ts';
|
|
13
13
|
import { type EndedBy, type RunArtifact, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
|
|
14
14
|
import { type RunEventInput, type RunMetrics } from './events.ts';
|
|
15
15
|
import { type ToolDispatch } from './run-calls.ts';
|
|
@@ -497,8 +497,11 @@ export interface AgentLoopResult {
|
|
|
497
497
|
ended?: EndedBy;
|
|
498
498
|
/** The transcript, with every assistant turn and tool result the run added. No system prompt. */
|
|
499
499
|
messages: OpenAI.ChatCompletionMessageParam[];
|
|
500
|
-
/**
|
|
501
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Summed over every turn of the run, in the spelling the `usage` events use, with whether any
|
|
502
|
+
* turn reported a cache count. The four counts are also under their older names.
|
|
503
|
+
*/
|
|
504
|
+
usage: LoopUsage;
|
|
502
505
|
/**
|
|
503
506
|
* Every call, `load_tools` included, in the order they were made, each under its call id — a
|
|
504
507
|
* proxied preselection's among them, first.
|
|
@@ -550,8 +553,10 @@ export interface AgentLoopResult {
|
|
|
550
553
|
*/
|
|
551
554
|
afterTurn: Promise<HookNote[]>;
|
|
552
555
|
/**
|
|
553
|
-
* The run summed and derived: what `runMetrics` makes of the events this loop emitted,
|
|
554
|
-
* `load_tools`
|
|
556
|
+
* The run summed and derived: what `runMetrics` makes of the events this loop emitted, with the
|
|
557
|
+
* `load_tools` counts at zero rather than absent in a run on demand, `wallMs` from the call to
|
|
558
|
+
* the return, and `outcome`. A host running several loops for one run gets its counts from
|
|
559
|
+
* `runMetrics` over all their events, without these results beside it.
|
|
555
560
|
*/
|
|
556
561
|
metrics: RunMetrics;
|
|
557
562
|
/**
|
|
@@ -577,7 +582,8 @@ export interface AgentLoopResult {
|
|
|
577
582
|
* @remarks
|
|
578
583
|
* What it throws carries the run as it stood — `messages`, `usage`, `toolCalls`, `loaded`, `used`
|
|
579
584
|
* and `preselected` — because the steps before a failure were real and a host that stores its
|
|
580
|
-
* transcript has nothing else to store them from.
|
|
585
|
+
* transcript has nothing else to store them from. What the request in flight had said is on it
|
|
586
|
+
* as `partial`, and not in `messages`. A spent budget is a `ToolIterationLimit`, an
|
|
581
587
|
* overflow is an `AgentLoopOverflow`, which is still a `ContextOverflow`, and anything else is an
|
|
582
588
|
* `AgentLoopError` with what was caught as its `cause` and its message as its own; `failedRun`
|
|
583
589
|
* reads the run off any of the three. A failure while a step's tools run leaves the transcript
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -16,7 +16,7 @@ import { runTurn } from '../turn/run-turn.js';
|
|
|
16
16
|
import { taskCall, tryAsk } from '../turn/side-task.js';
|
|
17
17
|
import { AgentLoopError, AgentLoopOverflow, errorMessage, ToolIterationLimit, } from '../wire/errors.js';
|
|
18
18
|
import { ContextOverflow } from '../wire/retry.js';
|
|
19
|
-
import { addCounts, noUsage } from '../wire/stream.js';
|
|
19
|
+
import { addCounts, loopUsage, noUsage, reportsCache, } from '../wire/stream.js';
|
|
20
20
|
import { contextTokens, toolsChars } from '../wire/tokens.js';
|
|
21
21
|
import { recoverToolCalls, } from '../wire/tool-calls.js';
|
|
22
22
|
import { FinishReason, PartType, Role } from '../wire/wire.js';
|
|
@@ -77,7 +77,8 @@ function cacheDiagnosis(previous, next, usage) {
|
|
|
77
77
|
* @remarks
|
|
78
78
|
* What it throws carries the run as it stood — `messages`, `usage`, `toolCalls`, `loaded`, `used`
|
|
79
79
|
* and `preselected` — because the steps before a failure were real and a host that stores its
|
|
80
|
-
* transcript has nothing else to store them from.
|
|
80
|
+
* transcript has nothing else to store them from. What the request in flight had said is on it
|
|
81
|
+
* as `partial`, and not in `messages`. A spent budget is a `ToolIterationLimit`, an
|
|
81
82
|
* overflow is an `AgentLoopOverflow`, which is still a `ContextOverflow`, and anything else is an
|
|
82
83
|
* `AgentLoopError` with what was caught as its `cause` and its message as its own; `failedRun`
|
|
83
84
|
* reads the run off any of the three. A failure while a step's tools run leaves the transcript
|
|
@@ -126,7 +127,7 @@ export async function runAgentLoop(options) {
|
|
|
126
127
|
const standing = {
|
|
127
128
|
read: () => ({
|
|
128
129
|
messages: [...options.messages],
|
|
129
|
-
usage: noUsage(),
|
|
130
|
+
usage: loopUsage(noUsage(), false),
|
|
130
131
|
toolCalls: [],
|
|
131
132
|
artifacts: [],
|
|
132
133
|
loaded: [],
|
|
@@ -529,19 +530,25 @@ function announcer(onMessage) {
|
|
|
529
530
|
* @param usage - The run's totals, this turn's already added.
|
|
530
531
|
* @param turn - The turn just finished, read for its usage and why it ended.
|
|
531
532
|
* @param ledger - The ledger as it stands. Carried on the event as it is, not copied.
|
|
533
|
+
* @param [contextLength] - The window the turn was served. Absent or zero leaves it off the report.
|
|
532
534
|
* @returns The event, for the caller to emit.
|
|
533
535
|
*/
|
|
534
|
-
const usageEvent = (usage, turn, ledger) =>
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
536
|
+
const usageEvent = (usage, turn, ledger, contextLength) => {
|
|
537
|
+
const { promptTokens, completionTokens, totalTokens, cachedTokens, cacheReported } = usage;
|
|
538
|
+
const window = contextLength ? { contextLength } : {};
|
|
539
|
+
return {
|
|
540
|
+
kind: RunEventKind.Usage,
|
|
541
|
+
usage: {
|
|
542
|
+
promptTokens,
|
|
543
|
+
completionTokens,
|
|
544
|
+
totalTokens,
|
|
545
|
+
cachedTokens,
|
|
546
|
+
cacheReported,
|
|
547
|
+
turn: { ...turn.usage, finishReason: turn.finishReason, ...window },
|
|
548
|
+
ledger,
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
};
|
|
545
552
|
/**
|
|
546
553
|
* The calls a turn made and the answer beside them, with calls the model wrote as text taken out
|
|
547
554
|
* of the answer and counted as calls.
|
|
@@ -628,18 +635,26 @@ async function runSteps(options, standing, pin) {
|
|
|
628
635
|
const byName = (names) => definedAs(definitions, names);
|
|
629
636
|
let messages = [...options.messages];
|
|
630
637
|
let usage = noUsage();
|
|
638
|
+
let cacheReported = false;
|
|
631
639
|
const toolCalls = [];
|
|
632
640
|
const artifacts = new Map();
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
641
|
+
// A box per request, emptied once its turn is in `messages`: a failure while the tools run had
|
|
642
|
+
// no request in flight.
|
|
643
|
+
let inFlight = {};
|
|
644
|
+
standing.read = () => {
|
|
645
|
+
const partial = inFlight.read?.();
|
|
646
|
+
return {
|
|
647
|
+
messages,
|
|
648
|
+
...(partial ? { partial } : {}),
|
|
649
|
+
usage: loopUsage(usage, cacheReported),
|
|
650
|
+
toolCalls,
|
|
651
|
+
artifacts: [...artifacts.values()],
|
|
652
|
+
// As the result would have said it: proxied, nothing is loaded that the history does not hold.
|
|
653
|
+
loaded: proxied ? [] : [...loaded],
|
|
654
|
+
used: [...used],
|
|
655
|
+
preselected: [...preselected],
|
|
656
|
+
};
|
|
657
|
+
};
|
|
643
658
|
const announce = announcer(onMessage);
|
|
644
659
|
// Held by reference rather than by index, so a `beforeStep` that folds the head into a summary
|
|
645
660
|
// moves the question without losing it — and one that summarises the question away takes the
|
|
@@ -764,8 +779,10 @@ async function runSteps(options, standing, pin) {
|
|
|
764
779
|
...withContext(messages, questionAt(), gathered.context, preface),
|
|
765
780
|
];
|
|
766
781
|
const build = (supported, refused) => buildBody(sending, supported, refused, request, declared, toolOrder);
|
|
782
|
+
inFlight = {};
|
|
767
783
|
const turnOptions = {
|
|
768
784
|
model: config.model,
|
|
785
|
+
inFlight,
|
|
769
786
|
droppable: Object.keys(extraBody ?? {}),
|
|
770
787
|
...(pin ? { headers: pin.headers } : {}),
|
|
771
788
|
maxRetries,
|
|
@@ -819,6 +836,7 @@ async function runSteps(options, standing, pin) {
|
|
|
819
836
|
const turn = maxContinuations > 0
|
|
820
837
|
? await continueTurn(client, supports, build, first, { ...turnOptions, maxContinuations })
|
|
821
838
|
: first;
|
|
839
|
+
inFlight = {};
|
|
822
840
|
previous = {
|
|
823
841
|
messages: request,
|
|
824
842
|
tools: names,
|
|
@@ -826,7 +844,8 @@ async function runSteps(options, standing, pin) {
|
|
|
826
844
|
completion: turn.usage.completion,
|
|
827
845
|
};
|
|
828
846
|
usage = addCounts(usage, turn.usage);
|
|
829
|
-
|
|
847
|
+
cacheReported ||= reportsCache(turn.usage);
|
|
848
|
+
onEvent(usageEvent(loopUsage(usage, cacheReported), turn, ledger, config.contextLength));
|
|
830
849
|
if (turn.finishReason === FinishReason.Length) {
|
|
831
850
|
notice(`the model stopped at maxTokens (${config.maxTokens}); this turn is cut short`);
|
|
832
851
|
}
|
|
@@ -862,7 +881,7 @@ async function runSteps(options, standing, pin) {
|
|
|
862
881
|
turn: shown,
|
|
863
882
|
...(ended ? { ended } : {}),
|
|
864
883
|
messages,
|
|
865
|
-
usage,
|
|
884
|
+
usage: loopUsage(usage, cacheReported),
|
|
866
885
|
toolCalls,
|
|
867
886
|
artifacts: [...artifacts.values()],
|
|
868
887
|
loaded: proxied ? [] : [...loaded],
|
package/dist/run/events.d.ts
CHANGED
|
@@ -121,6 +121,10 @@ export type RunEventKind = (typeof RunEventKind)[keyof typeof RunEventKind];
|
|
|
121
121
|
* What a run has spent, counted from the start of the run rather than for the turn that
|
|
122
122
|
* carried it: a client draws the latest one it has seen and needs no arithmetic of its own,
|
|
123
123
|
* and one lost to the backlog cap costs nothing because the next supersedes it.
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* The counts are `UsageTotals`, the spelling `AgentLoopResult.usage` shares, with the cache's two
|
|
127
|
+
* fields optional so a host emitting usage of its own need not know them.
|
|
124
128
|
*/
|
|
125
129
|
export interface RunUsage {
|
|
126
130
|
promptTokens: number;
|
|
@@ -131,6 +135,12 @@ export interface RunUsage {
|
|
|
131
135
|
* emitting usage before this field existed still compiles; absent reads the same as zero.
|
|
132
136
|
*/
|
|
133
137
|
cachedTokens?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Whether any turn so far reported a cache count, as `UsageTotals.cacheReported` says it — so a
|
|
140
|
+
* `cachedTokens` of zero reads as a miss or as nothing said. `runAgentLoop` always sets it;
|
|
141
|
+
* optional for the reason `cachedTokens` is, and absent reads as not known.
|
|
142
|
+
*/
|
|
143
|
+
cacheReported?: boolean;
|
|
134
144
|
/**
|
|
135
145
|
* The turn that carried this report, on its own rather than added in — the half `runMetrics`
|
|
136
146
|
* reads, since a total says nothing about which turn was slow or where the cache broke. Absent
|
|
@@ -148,6 +158,30 @@ export interface RunUsage {
|
|
|
148
158
|
export interface TurnReport extends TurnUsage {
|
|
149
159
|
/** Why the model stopped, as `Turn.finishReason` says it; `length` is a turn cut short. */
|
|
150
160
|
finishReason: string;
|
|
161
|
+
/**
|
|
162
|
+
* The window the turn was served, where the loop was told one, for `largestPromptShare`.
|
|
163
|
+
*
|
|
164
|
+
* @remarks
|
|
165
|
+
* On the turn rather than only in `runMetrics`' options, because a run of several loops can put
|
|
166
|
+
* them on models with different windows, and one window for the whole run misstates how close
|
|
167
|
+
* any of them came.
|
|
168
|
+
*/
|
|
169
|
+
contextLength?: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* What `load_tools` calls resolved to: new tools, ones already loaded, and names in no catalogue.
|
|
173
|
+
*
|
|
174
|
+
* @remarks
|
|
175
|
+
* Carried on each `load_tools` result as `loads`, so `runMetrics` over every loop's events adds
|
|
176
|
+
* them up without the loops' results beside it.
|
|
177
|
+
*/
|
|
178
|
+
export interface LoadCounts {
|
|
179
|
+
/** Tools loaded that were not loaded already. */
|
|
180
|
+
toolsLoaded: number;
|
|
181
|
+
/** Tools asked for that were loaded already. */
|
|
182
|
+
redundantLoads: number;
|
|
183
|
+
/** Names asked for that are in no catalogue. */
|
|
184
|
+
unknownToolNames: number;
|
|
151
185
|
}
|
|
152
186
|
/**
|
|
153
187
|
* One thing that happened in a run, as a watcher receives it.
|
|
@@ -202,6 +236,11 @@ export interface RunEvent {
|
|
|
202
236
|
* `id` is.
|
|
203
237
|
*/
|
|
204
238
|
level?: string;
|
|
239
|
+
/**
|
|
240
|
+
* What the call loaded, on a `load_tools` `tool-result` and on no other event. Absent from one
|
|
241
|
+
* whose arguments did not parse, which loaded nothing. Optional for the reason `id` is.
|
|
242
|
+
*/
|
|
243
|
+
loads?: LoadCounts;
|
|
205
244
|
}
|
|
206
245
|
/**
|
|
207
246
|
* What `emit` is given: the run and the sequence are the bus's to assign.
|
|
@@ -334,13 +373,14 @@ export interface RunMetrics {
|
|
|
334
373
|
/** `server-log` events at `error` or above: `critical`, `alert` and `emergency` with it. */
|
|
335
374
|
serverErrors: number;
|
|
336
375
|
/**
|
|
337
|
-
* Tools `load_tools` loaded that were not loaded already
|
|
338
|
-
*
|
|
376
|
+
* Tools `load_tools` loaded that were not loaded already, summed from each result's `loads`.
|
|
377
|
+
* Absent where no result carried any; `runAgentLoop` fills all three with zero in a run on
|
|
378
|
+
* demand that loaded nothing.
|
|
339
379
|
*/
|
|
340
380
|
toolsLoaded?: number;
|
|
341
|
-
/** Tools the model asked `load_tools` for that it already had,
|
|
381
|
+
/** Tools the model asked `load_tools` for that it already had, summed the same way. */
|
|
342
382
|
redundantLoads?: number;
|
|
343
|
-
/** Names the model asked `load_tools` for that are in no catalogue,
|
|
383
|
+
/** Names the model asked `load_tools` for that are in no catalogue, summed the same way. */
|
|
344
384
|
unknownToolNames?: number;
|
|
345
385
|
promptTokens: number;
|
|
346
386
|
completionTokens: number;
|
|
@@ -382,7 +422,10 @@ export interface RunMetrics {
|
|
|
382
422
|
draftAcceptance?: number;
|
|
383
423
|
/** The biggest prompt any turn reported. */
|
|
384
424
|
largestPrompt?: number;
|
|
385
|
-
/**
|
|
425
|
+
/**
|
|
426
|
+
* The highest share of its window any turn's prompt took, for how close the run came: each
|
|
427
|
+
* turn over its own `contextLength`, or over the one `runMetrics` was told where it has none.
|
|
428
|
+
*/
|
|
386
429
|
largestPromptShare?: number;
|
|
387
430
|
/** How it ended, read off `done` and the last turn. */
|
|
388
431
|
outcome?: RunOutcome;
|
|
@@ -408,7 +451,10 @@ export declare const RunOutcome: {
|
|
|
408
451
|
export type RunOutcome = (typeof RunOutcome)[keyof typeof RunOutcome];
|
|
409
452
|
/** What `runMetrics` takes besides the events. */
|
|
410
453
|
export interface RunMetricsOptions {
|
|
411
|
-
/**
|
|
454
|
+
/**
|
|
455
|
+
* The window the run was served, for `largestPromptShare` over the turns that do not carry
|
|
456
|
+
* their own. Absent or zero leaves those out.
|
|
457
|
+
*/
|
|
412
458
|
contextLength?: number;
|
|
413
459
|
}
|
|
414
460
|
/**
|
|
@@ -425,6 +471,8 @@ export interface RunMetricsOptions {
|
|
|
425
471
|
* what it returns as blocks; a summary is another shape, and folding one in would give every
|
|
426
472
|
* consumer of `fold` a block it does not know how to draw. Derived from the `usage` reports' own
|
|
427
473
|
* `turn` rather than the running totals, so a run with several loops in it — a question per loop —
|
|
428
|
-
* adds up the same as one with a single loop.
|
|
474
|
+
* adds up the same as one with a single loop. That holds for everything here: what `load_tools`
|
|
475
|
+
* resolved to rides on its results, and each turn carries the window it was served, so the events
|
|
476
|
+
* of every loop of a run, concatenated, are a complete account of it.
|
|
429
477
|
*/
|
|
430
478
|
export declare function runMetrics(events: RunEvent[], { contextLength }?: RunMetricsOptions): RunMetrics;
|
package/dist/run/events.js
CHANGED
|
@@ -522,7 +522,9 @@ function outcomeOf({ ok, name }, last) {
|
|
|
522
522
|
* what it returns as blocks; a summary is another shape, and folding one in would give every
|
|
523
523
|
* consumer of `fold` a block it does not know how to draw. Derived from the `usage` reports' own
|
|
524
524
|
* `turn` rather than the running totals, so a run with several loops in it — a question per loop —
|
|
525
|
-
* adds up the same as one with a single loop.
|
|
525
|
+
* adds up the same as one with a single loop. That holds for everything here: what `load_tools`
|
|
526
|
+
* resolved to rides on its results, and each turn carries the window it was served, so the events
|
|
527
|
+
* of every loop of a run, concatenated, are a complete account of it.
|
|
526
528
|
*/
|
|
527
529
|
export function runMetrics(events, { contextLength } = {}) {
|
|
528
530
|
const metrics = {
|
|
@@ -570,6 +572,11 @@ export function runMetrics(events, { contextLength } = {}) {
|
|
|
570
572
|
if (event.ok === false) {
|
|
571
573
|
metrics.toolErrors[event.name] = (metrics.toolErrors[event.name] ?? 0) + 1;
|
|
572
574
|
}
|
|
575
|
+
if (event.loads) {
|
|
576
|
+
add('toolsLoaded', event.loads.toolsLoaded);
|
|
577
|
+
add('redundantLoads', event.loads.redundantLoads);
|
|
578
|
+
add('unknownToolNames', event.loads.unknownToolNames);
|
|
579
|
+
}
|
|
573
580
|
const called = pending.get(event.name)?.shift();
|
|
574
581
|
if (called !== undefined) {
|
|
575
582
|
add('toolMs', event.at - called);
|
|
@@ -625,6 +632,10 @@ export function runMetrics(events, { contextLength } = {}) {
|
|
|
625
632
|
}
|
|
626
633
|
if (turn.prompt > 0) {
|
|
627
634
|
metrics.largestPrompt = Math.max(metrics.largestPrompt ?? 0, turn.prompt);
|
|
635
|
+
const window = turn.contextLength ?? contextLength ?? 0;
|
|
636
|
+
if (window > 0) {
|
|
637
|
+
metrics.largestPromptShare = Math.max(metrics.largestPromptShare ?? 0, turn.prompt / window);
|
|
638
|
+
}
|
|
628
639
|
}
|
|
629
640
|
}
|
|
630
641
|
else if (event.kind === RunEventKind.Done) {
|
|
@@ -643,8 +654,5 @@ export function runMetrics(events, { contextLength } = {}) {
|
|
|
643
654
|
if (metrics.draftTotal && metrics.draftAccepted !== undefined) {
|
|
644
655
|
metrics.draftAcceptance = metrics.draftAccepted / metrics.draftTotal;
|
|
645
656
|
}
|
|
646
|
-
if (metrics.largestPrompt !== undefined && contextLength && contextLength > 0) {
|
|
647
|
-
metrics.largestPromptShare = metrics.largestPrompt / contextLength;
|
|
648
|
-
}
|
|
649
657
|
return metrics;
|
|
650
658
|
}
|
package/dist/run/run-calls.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type OpenAI from 'openai';
|
|
|
2
2
|
import type { CatalogServer } from '../tools/catalog.ts';
|
|
3
3
|
import type { Turn } from '../wire/stream.ts';
|
|
4
4
|
import { type EndedBy, type RunArtifact, type ToolAnswer, type ToolCall, type ToolCallOutcome, type ToolCallReport, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
|
|
5
|
-
import type { RunEventInput } from './events.ts';
|
|
5
|
+
import type { LoadCounts, RunEventInput } from './events.ts';
|
|
6
6
|
/**
|
|
7
7
|
* The tools between two turns: a step's calls read, run and written into the transcript.
|
|
8
8
|
*
|
|
@@ -89,11 +89,7 @@ export interface Calling {
|
|
|
89
89
|
/** The host's own tools by name: declared on every step, and never reached through `call_tool`. */
|
|
90
90
|
hostNames: ReadonlySet<string>;
|
|
91
91
|
loaded: Set<string>;
|
|
92
|
-
loads:
|
|
93
|
-
toolsLoaded: number;
|
|
94
|
-
redundantLoads: number;
|
|
95
|
-
unknownToolNames: number;
|
|
96
|
-
};
|
|
92
|
+
loads: LoadCounts;
|
|
97
93
|
used: Set<string>;
|
|
98
94
|
toolCalls: ToolCallOutcome[];
|
|
99
95
|
/** What the run's calls linked to, by uri, in the order first seen. */
|
package/dist/run/run-calls.js
CHANGED
|
@@ -155,17 +155,28 @@ function answerOf(answer) {
|
|
|
155
155
|
const shownAs = (run, { call, args, normal }) => run.proxies && call.function.name === CALL_TOOL
|
|
156
156
|
? shownCall(CALL_TOOL, args ? normal : call.function.arguments)
|
|
157
157
|
: { name: call.function.name, input: call.function.arguments };
|
|
158
|
+
/**
|
|
159
|
+
* Adds one call's load counts to the run's.
|
|
160
|
+
*
|
|
161
|
+
* @param into - The run's counts. Written to.
|
|
162
|
+
* @param counts - The call's. Not written to.
|
|
163
|
+
*/
|
|
164
|
+
function addLoads(into, counts) {
|
|
165
|
+
into.toolsLoaded += counts.toolsLoaded;
|
|
166
|
+
into.redundantLoads += counts.redundantLoads;
|
|
167
|
+
into.unknownToolNames += counts.unknownToolNames;
|
|
168
|
+
}
|
|
158
169
|
/**
|
|
159
170
|
* Answers a `load_tools`, counting what it loaded.
|
|
160
171
|
*
|
|
161
172
|
* @param run - The run. Written to: the names this call loads are added to `loaded`, and `loads`
|
|
162
173
|
* counts the new ones, the ones already loaded and the ones not in the catalogue.
|
|
163
174
|
* @param args - The call's arguments, read as `requestedNames` reads them.
|
|
164
|
-
* @returns The text the model reads
|
|
165
|
-
* where it named even one tool that was loaded already.
|
|
175
|
+
* @returns The text the model reads; `ok`, false where nothing it named was loadable and true
|
|
176
|
+
* where it named even one tool that was loaded already; and `counts`, this call's alone.
|
|
166
177
|
*/
|
|
167
178
|
function answerLoad(run, args) {
|
|
168
|
-
const { catalog, proxied, definitions, loaded
|
|
179
|
+
const { catalog, proxied, definitions, loaded } = run;
|
|
169
180
|
const resolved = expandNames(requestedNames(args), catalog);
|
|
170
181
|
const content = proxied
|
|
171
182
|
? proxyLoadResult(resolved, catalog, definedAs(definitions, resolved.matched), loaded)
|
|
@@ -173,14 +184,15 @@ function answerLoad(run, args) {
|
|
|
173
184
|
// Proxied, a load is only of what the result could define: a catalogued name the host
|
|
174
185
|
// gave no definition for was not loaded, and is not counted or remembered as though it was.
|
|
175
186
|
const hits = proxied ? resolved.matched.filter((hit) => definitions.has(hit)) : resolved.matched;
|
|
187
|
+
const counts = { toolsLoaded: 0, redundantLoads: 0, unknownToolNames: resolved.unknown.length };
|
|
176
188
|
for (const hit of hits) {
|
|
177
|
-
|
|
189
|
+
counts[loaded.has(hit) ? 'redundantLoads' : 'toolsLoaded']++;
|
|
178
190
|
}
|
|
179
|
-
loads
|
|
191
|
+
addLoads(run.loads, counts);
|
|
180
192
|
for (const hit of hits) {
|
|
181
193
|
loaded.add(hit);
|
|
182
194
|
}
|
|
183
|
-
return { content, ok: hits.length > 0 };
|
|
195
|
+
return { content, ok: hits.length > 0, counts };
|
|
184
196
|
}
|
|
185
197
|
/**
|
|
186
198
|
* Makes a call at most once per key, sharing the in-flight promise so two identical calls in one
|
|
@@ -353,6 +365,7 @@ async function runCall(run, prepared, answered, step) {
|
|
|
353
365
|
let links = [];
|
|
354
366
|
let end = false;
|
|
355
367
|
let ok = true;
|
|
368
|
+
let loads;
|
|
356
369
|
// Answered from an earlier identical call of the step, whose links are already counted.
|
|
357
370
|
let shared = false;
|
|
358
371
|
try {
|
|
@@ -360,7 +373,7 @@ async function runCall(run, prepared, answered, step) {
|
|
|
360
373
|
throw unreadable;
|
|
361
374
|
}
|
|
362
375
|
if (load) {
|
|
363
|
-
({ content, ok } = answerLoad(run, args));
|
|
376
|
+
({ content, ok, counts: loads } = answerLoad(run, args));
|
|
364
377
|
}
|
|
365
378
|
else {
|
|
366
379
|
if (refused) {
|
|
@@ -393,7 +406,14 @@ async function runCall(run, prepared, answered, step) {
|
|
|
393
406
|
finally {
|
|
394
407
|
close();
|
|
395
408
|
}
|
|
396
|
-
run.onEvent({
|
|
409
|
+
run.onEvent({
|
|
410
|
+
kind: RunEventKind.ToolResult,
|
|
411
|
+
id: call.id,
|
|
412
|
+
name,
|
|
413
|
+
ok,
|
|
414
|
+
text: preview(content),
|
|
415
|
+
...(loads ? { loads } : {}),
|
|
416
|
+
});
|
|
397
417
|
const isFirstAnswer = shared === false;
|
|
398
418
|
if (isFirstAnswer) {
|
|
399
419
|
collect(run, links, { callId: call.id, tool: name, step });
|
|
@@ -521,10 +541,11 @@ export async function loadShortlist(run, shortlist, messages) {
|
|
|
521
541
|
const content = proxyLoadResult(expandNames(names, catalog, names.length), catalog, shortlist);
|
|
522
542
|
run.onEvent({ kind: RunEventKind.ToolCall, id, name: LOAD_TOOLS, text: preview(args) });
|
|
523
543
|
run.onToolCall?.({ id, name: LOAD_TOOLS, args: { names }, raw: args });
|
|
524
|
-
|
|
544
|
+
const loads = { toolsLoaded: names.length, redundantLoads: 0, unknownToolNames: 0 };
|
|
545
|
+
run.onEvent({ kind: RunEventKind.ToolResult, id, name: LOAD_TOOLS, ok: true, text: preview(content), loads });
|
|
525
546
|
run.onToolResult?.({ id, name: LOAD_TOOLS, ok: true, content });
|
|
526
547
|
run.toolCalls.push({ id, name: LOAD_TOOLS, ok: true });
|
|
527
|
-
run.loads
|
|
548
|
+
addLoads(run.loads, loads);
|
|
528
549
|
for (const name of names) {
|
|
529
550
|
run.loaded.add(name);
|
|
530
551
|
}
|
|
@@ -62,6 +62,8 @@ export declare const isContinuable: (turn: Turn) => boolean;
|
|
|
62
62
|
* server that takes the request and ignores the prefill — hosted OpenAI among them — shows itself;
|
|
63
63
|
* that check is only as good as a restart being word for word. Either way the answer so far is
|
|
64
64
|
* kept, with a notice. So is it when the continuation fails any other way, since the tokens
|
|
65
|
-
* already in hand are worth more than the error; only a stop is thrown.
|
|
65
|
+
* already in hand are worth more than the error; only a stop is thrown. An `inFlight` box reads
|
|
66
|
+
* the answer so far with the continuation's piece joined on, so a stop mid-continuation keeps it
|
|
67
|
+
* all.
|
|
66
68
|
*/
|
|
67
69
|
export declare function continueTurn(client: OpenAI, supports: Capabilities, request: RequestBuilder, turn: Turn, { maxContinuations, ...options }?: ContinueTurnOptions): Promise<Turn>;
|
|
@@ -97,6 +97,22 @@ function joinUsage(first, next) {
|
|
|
97
97
|
}
|
|
98
98
|
return joined;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The answer so far with the piece a continuation in flight had added to it.
|
|
102
|
+
*
|
|
103
|
+
* @param sofar - The turn being continued, which may itself be a joined one. Not written to.
|
|
104
|
+
* @param [piece] - What the continuation had said. Absent adds nothing.
|
|
105
|
+
* @returns The two joined as `continueTurn` joins turns: content and reasoning in order, and the
|
|
106
|
+
* continuation's own calls. `reasoning` and `toolCalls` are left out where empty.
|
|
107
|
+
*/
|
|
108
|
+
function joinPartial(sofar, piece) {
|
|
109
|
+
const reasoning = sofar.reasoning + (piece?.reasoning ?? '');
|
|
110
|
+
return {
|
|
111
|
+
content: sofar.content + (piece?.content ?? ''),
|
|
112
|
+
...(reasoning ? { reasoning } : {}),
|
|
113
|
+
...(piece?.toolCalls ? { toolCalls: piece.toolCalls } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
100
116
|
/**
|
|
101
117
|
* Carries on an answer the token ceiling cut off, by sending the transcript again with the answer
|
|
102
118
|
* so far as a trailing assistant message, and joins the pieces into one turn.
|
|
@@ -124,17 +140,28 @@ function joinUsage(first, next) {
|
|
|
124
140
|
* server that takes the request and ignores the prefill — hosted OpenAI among them — shows itself;
|
|
125
141
|
* that check is only as good as a restart being word for word. Either way the answer so far is
|
|
126
142
|
* kept, with a notice. So is it when the continuation fails any other way, since the tokens
|
|
127
|
-
* already in hand are worth more than the error; only a stop is thrown.
|
|
143
|
+
* already in hand are worth more than the error; only a stop is thrown. An `inFlight` box reads
|
|
144
|
+
* the answer so far with the continuation's piece joined on, so a stop mid-continuation keeps it
|
|
145
|
+
* all.
|
|
128
146
|
*/
|
|
129
147
|
export async function continueTurn(client, supports, request, turn, { maxContinuations = 1, ...options } = {}) {
|
|
130
148
|
const refused = options.model === undefined ? undefined : modelCapabilitiesFor(supports, options.model);
|
|
131
149
|
const who = modelLabel(options.model);
|
|
150
|
+
// The caller's box reads the whole answer, not only the piece the continuation streams.
|
|
151
|
+
const outer = options.inFlight;
|
|
152
|
+
const piece = {};
|
|
153
|
+
const told = outer ? { inFlight: piece } : {};
|
|
132
154
|
let joined = turn;
|
|
133
155
|
for (let count = 0; count < maxContinuations && isContinuable(joined); count++) {
|
|
134
156
|
if (refused?.assistantPrefill === false) {
|
|
135
157
|
break;
|
|
136
158
|
}
|
|
137
159
|
const answer = joined.content;
|
|
160
|
+
if (outer) {
|
|
161
|
+
const sofar = joined;
|
|
162
|
+
piece.read = undefined;
|
|
163
|
+
outer.read = () => joinPartial(sofar, piece.read?.());
|
|
164
|
+
}
|
|
138
165
|
let next;
|
|
139
166
|
try {
|
|
140
167
|
next = await runTurn(client, supports, (capabilities, forModel) => {
|
|
@@ -143,7 +170,7 @@ export async function continueTurn(client, supports, request, turn, { maxContinu
|
|
|
143
170
|
...body,
|
|
144
171
|
messages: [...body.messages, { role: Role.Assistant, content: answer }],
|
|
145
172
|
};
|
|
146
|
-
}, { ...options, startInReasoning: false });
|
|
173
|
+
}, { ...options, ...told, startInReasoning: false });
|
|
147
174
|
}
|
|
148
175
|
catch (error) {
|
|
149
176
|
if (options.signal?.aborted) {
|
package/dist/wire/errors.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
2
|
import { ContextOverflow } from './retry.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LoopUsage, PartialTurn } from './stream.ts';
|
|
4
4
|
import type { RunArtifact, ToolCallOutcome } from './tool-calls.ts';
|
|
5
5
|
/**
|
|
6
6
|
* What went wrong, as a string.
|
|
@@ -27,11 +27,24 @@ export interface AgentLoopFailure {
|
|
|
27
27
|
/**
|
|
28
28
|
* The transcript, as the result's `messages` would have been: every assistant turn and tool
|
|
29
29
|
* result the run added, and no system prompt. Well-formed — a call the run was stopped during
|
|
30
|
-
* has a result saying so. The reply of a request that was in flight is not in it
|
|
30
|
+
* has a result saying so. The reply of a request that was in flight is not in it: see `partial`.
|
|
31
31
|
*/
|
|
32
32
|
messages: OpenAI.ChatCompletionMessageParam[];
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* What the request in flight had said when the run stopped or failed — a stop, the endpoint
|
|
35
|
+
* going silent, a stream that broke — continuations joined on. Absent where no request was in
|
|
36
|
+
* flight or it had said nothing.
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* Not in `messages`, because it is not a turn an endpoint will take back. It is what a person
|
|
40
|
+
* watching had been shown, and whether it belongs in the history is the host's call.
|
|
41
|
+
*/
|
|
42
|
+
partial?: PartialTurn;
|
|
43
|
+
/**
|
|
44
|
+
* Summed over the turns that came back, as the result's `usage` would have said it. A request
|
|
45
|
+
* that failed or was stopped adds nothing.
|
|
46
|
+
*/
|
|
47
|
+
usage: LoopUsage;
|
|
35
48
|
/** Every call that was made, in order. One the run was stopped during is `ok: false`. */
|
|
36
49
|
toolCalls: ToolCallOutcome[];
|
|
37
50
|
/**
|
|
@@ -59,7 +72,8 @@ export interface AgentLoopFailure {
|
|
|
59
72
|
export declare class AgentLoopError extends Error implements AgentLoopFailure {
|
|
60
73
|
readonly name: string;
|
|
61
74
|
readonly messages: OpenAI.ChatCompletionMessageParam[];
|
|
62
|
-
readonly
|
|
75
|
+
readonly partial?: PartialTurn;
|
|
76
|
+
readonly usage: LoopUsage;
|
|
63
77
|
readonly toolCalls: ToolCallOutcome[];
|
|
64
78
|
readonly artifacts: RunArtifact[];
|
|
65
79
|
readonly loaded: string[];
|
|
@@ -93,7 +107,8 @@ export declare class ToolIterationLimit extends AgentLoopError {
|
|
|
93
107
|
*/
|
|
94
108
|
export declare class AgentLoopOverflow extends ContextOverflow implements AgentLoopFailure {
|
|
95
109
|
readonly messages: OpenAI.ChatCompletionMessageParam[];
|
|
96
|
-
readonly
|
|
110
|
+
readonly partial?: PartialTurn;
|
|
111
|
+
readonly usage: LoopUsage;
|
|
97
112
|
readonly toolCalls: ToolCallOutcome[];
|
|
98
113
|
readonly artifacts: RunArtifact[];
|
|
99
114
|
readonly loaded: string[];
|
package/dist/wire/errors.js
CHANGED
|
@@ -24,6 +24,7 @@ export const errorMessage = (error) => (error instanceof Error ? error.message :
|
|
|
24
24
|
export class AgentLoopError extends Error {
|
|
25
25
|
name = 'AgentLoopError';
|
|
26
26
|
messages;
|
|
27
|
+
partial;
|
|
27
28
|
usage;
|
|
28
29
|
toolCalls;
|
|
29
30
|
artifacts;
|
|
@@ -38,6 +39,9 @@ export class AgentLoopError extends Error {
|
|
|
38
39
|
constructor(message, run, options) {
|
|
39
40
|
super(message, options);
|
|
40
41
|
this.messages = run.messages;
|
|
42
|
+
if (run.partial) {
|
|
43
|
+
this.partial = run.partial;
|
|
44
|
+
}
|
|
41
45
|
this.usage = run.usage;
|
|
42
46
|
this.toolCalls = run.toolCalls;
|
|
43
47
|
this.artifacts = run.artifacts ?? [];
|
|
@@ -67,6 +71,7 @@ export class ToolIterationLimit extends AgentLoopError {
|
|
|
67
71
|
*/
|
|
68
72
|
export class AgentLoopOverflow extends ContextOverflow {
|
|
69
73
|
messages;
|
|
74
|
+
partial;
|
|
70
75
|
usage;
|
|
71
76
|
toolCalls;
|
|
72
77
|
artifacts;
|
|
@@ -81,6 +86,9 @@ export class AgentLoopOverflow extends ContextOverflow {
|
|
|
81
86
|
constructor(message, run, options) {
|
|
82
87
|
super(message, options);
|
|
83
88
|
this.messages = run.messages;
|
|
89
|
+
if (run.partial) {
|
|
90
|
+
this.partial = run.partial;
|
|
91
|
+
}
|
|
84
92
|
this.usage = run.usage;
|
|
85
93
|
this.toolCalls = run.toolCalls;
|
|
86
94
|
this.artifacts = run.artifacts ?? [];
|
package/dist/wire/stream.d.ts
CHANGED
|
@@ -145,6 +145,60 @@ export interface TurnUsage {
|
|
|
145
145
|
* nothing reported them.
|
|
146
146
|
*/
|
|
147
147
|
export declare const noUsage: () => TurnUsage;
|
|
148
|
+
/**
|
|
149
|
+
* A run's token totals, in the spelling every run-level usage shares.
|
|
150
|
+
*
|
|
151
|
+
* @remarks
|
|
152
|
+
* The spelling of `RunUsage`, which is what the `usage` events carry and what hosts persist, so a
|
|
153
|
+
* host reads a loop's result and its events with one mapping rather than two. A single turn's own
|
|
154
|
+
* report, `TurnUsage`, keeps its shorter names: it is a different thing, a turn's measurements,
|
|
155
|
+
* and every field on it is that turn's.
|
|
156
|
+
*/
|
|
157
|
+
export interface UsageTotals {
|
|
158
|
+
promptTokens: number;
|
|
159
|
+
completionTokens: number;
|
|
160
|
+
totalTokens: number;
|
|
161
|
+
/** How much of `promptTokens` came from the endpoint's prompt cache. Zero where none said. */
|
|
162
|
+
cachedTokens: number;
|
|
163
|
+
/**
|
|
164
|
+
* Whether any turn of the run reported a cache count, which is what tells a `cachedTokens` of
|
|
165
|
+
* zero that is a miss from one the endpoint never reports. A turn reported one where its usage
|
|
166
|
+
* carries `uncached`.
|
|
167
|
+
*/
|
|
168
|
+
cacheReported: boolean;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* What a loop spent, as `AgentLoopResult.usage` and a failed run's `usage` carry it.
|
|
172
|
+
*
|
|
173
|
+
* @remarks
|
|
174
|
+
* `UsageTotals`, with the four counts also under the names this field had before it: a host that
|
|
175
|
+
* reads `usage.prompt` keeps compiling and reads the same number as `usage.promptTokens`.
|
|
176
|
+
*/
|
|
177
|
+
export interface LoopUsage extends TurnUsage, UsageTotals {
|
|
178
|
+
/** @deprecated Read `promptTokens`, the same number. */
|
|
179
|
+
prompt: number;
|
|
180
|
+
/** @deprecated Read `completionTokens`, the same number. */
|
|
181
|
+
completion: number;
|
|
182
|
+
/** @deprecated Read `totalTokens`, the same number. */
|
|
183
|
+
total: number;
|
|
184
|
+
/** @deprecated Read `cachedTokens`, the same number, and `cacheReported` for whether it was reported. */
|
|
185
|
+
cached: number;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Whether a turn's usage says the endpoint reported its cache.
|
|
189
|
+
*
|
|
190
|
+
* @param usage - One turn's report.
|
|
191
|
+
* @returns True where it carries `uncached`, which is present only where a cache count was.
|
|
192
|
+
*/
|
|
193
|
+
export declare const reportsCache: (usage: TurnUsage) => boolean;
|
|
194
|
+
/**
|
|
195
|
+
* A loop's summed counts in both spellings, for its result and its `usage` events.
|
|
196
|
+
*
|
|
197
|
+
* @param counts - The four counts summed over the loop's turns. Not written to.
|
|
198
|
+
* @param cacheReported - Whether any of those turns reported a cache count.
|
|
199
|
+
* @returns A new usage holding the four counts under both names, and the flag.
|
|
200
|
+
*/
|
|
201
|
+
export declare const loopUsage: (counts: TurnUsage, cacheReported: boolean) => LoopUsage;
|
|
148
202
|
/**
|
|
149
203
|
* The four token counts of two usages, added.
|
|
150
204
|
*
|
|
@@ -213,6 +267,39 @@ export interface Turn {
|
|
|
213
267
|
export interface Produced {
|
|
214
268
|
any: boolean;
|
|
215
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* What a turn had said when it ended without coming back: the answer, the scratchpad and the calls,
|
|
272
|
+
* as far as each had arrived.
|
|
273
|
+
*
|
|
274
|
+
* @remarks
|
|
275
|
+
* It is not a `Turn`, and not a message an endpoint will take back: it has no usage, no reason it
|
|
276
|
+
* ended, and a call in it may stop mid-JSON. It is what a person watching had been shown, for a
|
|
277
|
+
* host to keep or to show.
|
|
278
|
+
*/
|
|
279
|
+
export interface PartialTurn {
|
|
280
|
+
/** The answer so far, fences taken out; `""` where it had not begun. */
|
|
281
|
+
content: string;
|
|
282
|
+
/** The scratchpad so far, from its own field or a fence. Absent where none had arrived. */
|
|
283
|
+
reasoning?: string;
|
|
284
|
+
/**
|
|
285
|
+
* The calls begun, assembled as `Turn.toolCalls` would have been — a name may be missing and
|
|
286
|
+
* arguments may stop mid-JSON. Absent where none had begun.
|
|
287
|
+
*/
|
|
288
|
+
toolCalls?: ToolCall[];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Where the request in flight can be read from, after it has thrown.
|
|
292
|
+
*
|
|
293
|
+
* @remarks
|
|
294
|
+
* A box for the reason `Produced` is one: a rejected promise carries no stream state, and the
|
|
295
|
+
* error is often the signal's reason, which is shared by every run under it. Each `streamTurn`
|
|
296
|
+
* sets `read` as it starts, so after a retry or a downgrade it reads the latest attempt and
|
|
297
|
+
* nothing of the one before.
|
|
298
|
+
*/
|
|
299
|
+
export interface InFlight {
|
|
300
|
+
/** What the stream had said, or `undefined` where it had said nothing. */
|
|
301
|
+
read?: () => PartialTurn | undefined;
|
|
302
|
+
}
|
|
216
303
|
/** What `streamTurn` takes besides the request body. */
|
|
217
304
|
export interface StreamTurnOptions {
|
|
218
305
|
/** Cancels the request and the stream being read from it. */
|
|
@@ -235,6 +322,11 @@ export interface StreamTurnOptions {
|
|
|
235
322
|
firstChunkMs?: number;
|
|
236
323
|
/** Set by the first chunk that carries anything, so a failed call knows if it can be retried. */
|
|
237
324
|
produced?: Produced;
|
|
325
|
+
/**
|
|
326
|
+
* Given a `read` of what this call has said so far, as the call starts. Read it once the call has
|
|
327
|
+
* thrown; a turn that came back has it all, and the box is not cleared.
|
|
328
|
+
*/
|
|
329
|
+
inFlight?: InFlight;
|
|
238
330
|
/**
|
|
239
331
|
* The fences that mark a scratchpad written into `content`.
|
|
240
332
|
*
|
|
@@ -282,4 +374,4 @@ export interface StreamTurnOptions {
|
|
|
282
374
|
* which run it is: `step` is the caller's flow concept, and wrapping these into an `emit` is one
|
|
283
375
|
* line at the call site.
|
|
284
376
|
*/
|
|
285
|
-
export declare function streamTurn(client: OpenAI, body: OpenAI.ChatCompletionCreateParamsStreaming, { signal, idleMs, firstChunkMs, produced, fences, startInReasoning, onThinking, onOutput, headers, }?: StreamTurnOptions): Promise<Turn>;
|
|
377
|
+
export declare function streamTurn(client: OpenAI, body: OpenAI.ChatCompletionCreateParamsStreaming, { signal, idleMs, firstChunkMs, produced, inFlight, fences, startInReasoning, onThinking, onOutput, headers, }?: StreamTurnOptions): Promise<Turn>;
|
package/dist/wire/stream.js
CHANGED
|
@@ -42,6 +42,31 @@ export const CacheBreakReason = {
|
|
|
42
42
|
* nothing reported them.
|
|
43
43
|
*/
|
|
44
44
|
export const noUsage = () => ({ prompt: 0, completion: 0, total: 0, cached: 0 });
|
|
45
|
+
/**
|
|
46
|
+
* Whether a turn's usage says the endpoint reported its cache.
|
|
47
|
+
*
|
|
48
|
+
* @param usage - One turn's report.
|
|
49
|
+
* @returns True where it carries `uncached`, which is present only where a cache count was.
|
|
50
|
+
*/
|
|
51
|
+
export const reportsCache = (usage) => usage.uncached !== undefined;
|
|
52
|
+
/**
|
|
53
|
+
* A loop's summed counts in both spellings, for its result and its `usage` events.
|
|
54
|
+
*
|
|
55
|
+
* @param counts - The four counts summed over the loop's turns. Not written to.
|
|
56
|
+
* @param cacheReported - Whether any of those turns reported a cache count.
|
|
57
|
+
* @returns A new usage holding the four counts under both names, and the flag.
|
|
58
|
+
*/
|
|
59
|
+
export const loopUsage = (counts, cacheReported) => ({
|
|
60
|
+
prompt: counts.prompt,
|
|
61
|
+
completion: counts.completion,
|
|
62
|
+
total: counts.total,
|
|
63
|
+
cached: counts.cached,
|
|
64
|
+
promptTokens: counts.prompt,
|
|
65
|
+
completionTokens: counts.completion,
|
|
66
|
+
totalTokens: counts.total,
|
|
67
|
+
cachedTokens: counts.cached,
|
|
68
|
+
cacheReported,
|
|
69
|
+
});
|
|
45
70
|
/**
|
|
46
71
|
* The four token counts of two usages, added.
|
|
47
72
|
*
|
|
@@ -188,6 +213,25 @@ function assembledCalls(calls) {
|
|
|
188
213
|
}
|
|
189
214
|
/** The largest delay a timer takes, which is as close to none as the SDK's timeout option goes. */
|
|
190
215
|
const NO_SDK_TIMEOUT = 2_147_483_647;
|
|
216
|
+
/**
|
|
217
|
+
* A partial turn from what a stream had put together, or nothing where it had put nothing.
|
|
218
|
+
*
|
|
219
|
+
* @param content - The answer so far.
|
|
220
|
+
* @param reasoning - The scratchpad so far.
|
|
221
|
+
* @param calls - The calls begun, as they arrived. Not written to.
|
|
222
|
+
* @returns `undefined` where all three are empty; otherwise the content, with `reasoning` and
|
|
223
|
+
* `toolCalls` only where they hold something.
|
|
224
|
+
*/
|
|
225
|
+
function partialTurn(content, reasoning, calls) {
|
|
226
|
+
if (content === '' && reasoning === '' && calls.length === 0) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
content,
|
|
231
|
+
...(reasoning ? { reasoning } : {}),
|
|
232
|
+
...(calls.length ? { toolCalls: assembledCalls(calls) } : {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
191
235
|
/**
|
|
192
236
|
* Runs one turn as a stream, reporting tokens as they arrive and assembling them back into a
|
|
193
237
|
* message.
|
|
@@ -208,7 +252,7 @@ const NO_SDK_TIMEOUT = 2_147_483_647;
|
|
|
208
252
|
* which run it is: `step` is the caller's flow concept, and wrapping these into an `emit` is one
|
|
209
253
|
* line at the call site.
|
|
210
254
|
*/
|
|
211
|
-
export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, produced, fences = DEFAULT_FENCES, startInReasoning, onThinking, onOutput, headers, } = {}) {
|
|
255
|
+
export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, produced, inFlight, fences = DEFAULT_FENCES, startInReasoning, onThinking, onOutput, headers, } = {}) {
|
|
212
256
|
// Silence, not duration: the timer is rearmed on every chunk, so a model that is still
|
|
213
257
|
// talking is never cut off however long it takes, and one that has stopped talking does not
|
|
214
258
|
// hang the run until someone notices. A request that never answers at all is the same case
|
|
@@ -229,6 +273,10 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
|
|
|
229
273
|
idle = setTimeout(() => watchdog.abort(), ms);
|
|
230
274
|
}
|
|
231
275
|
};
|
|
276
|
+
if (inFlight) {
|
|
277
|
+
// Before the request, so a box an earlier attempt filled reads nothing of it.
|
|
278
|
+
inFlight.read = () => undefined;
|
|
279
|
+
}
|
|
232
280
|
try {
|
|
233
281
|
rearm(false);
|
|
234
282
|
const started = Date.now();
|
|
@@ -271,6 +319,11 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
|
|
|
271
319
|
// In arrival order, sorted by index at the end; a call from a server that sent none keeps
|
|
272
320
|
// its place in the order they arrived.
|
|
273
321
|
const calls = [];
|
|
322
|
+
if (inFlight) {
|
|
323
|
+
// Lazy, so a stream nobody stops pays nothing for it. Text held back as a possible tag is
|
|
324
|
+
// left out: it is a few characters that may yet be a marker.
|
|
325
|
+
inFlight.read = () => partialTurn(splitter.output, reasoning.join('') + splitter.reasoning, calls);
|
|
326
|
+
}
|
|
274
327
|
const usage = noUsage();
|
|
275
328
|
// The usage block's own cache count wins over llama.cpp's, which is the same number in the
|
|
276
329
|
// servers that send both; kept apart so the order the two arrive in does not decide.
|
package/llms.txt
CHANGED
|
@@ -187,6 +187,7 @@ What a run is doing, while it is doing it.
|
|
|
187
187
|
- `endRun` — Forgets a run that will not be emitting `done` — one whose process is tearing down, or whose loop threw where it could not be caught.
|
|
188
188
|
- `fold` — A run's events with each stretch of `thinking` or of `output` joined into one block, for a reader that takes a run in snapshots.
|
|
189
189
|
- `history` — The backlog alone, for a caller that wants a snapshot rather than a subscription.
|
|
190
|
+
- `LoadCounts` (type) — What `load_tools` calls resolved to: new tools, ones already loaded, and names in no catalogue.
|
|
190
191
|
- `RunEvent` (type) — One thing that happened in a run, as a watcher receives it.
|
|
191
192
|
- `RunEventInput` (type) — What `emit` is given: the run and the sequence are the bus's to assign.
|
|
192
193
|
- `RunEventKind` — Any one of the kinds in `RunEventKind`.
|
|
@@ -387,11 +388,15 @@ Everything about a request failing that is not about what the request said.
|
|
|
387
388
|
Reading one streamed turn back into a message.
|
|
388
389
|
|
|
389
390
|
- `CacheBreakReason` — Any one of the reasons in `CacheBreakReason`.
|
|
391
|
+
- `InFlight` (type) — Where the request in flight can be read from, after it has thrown.
|
|
392
|
+
- `LoopUsage` (type) — What a loop spent, as `AgentLoopResult.usage` and a failed run's `usage` carry it.
|
|
393
|
+
- `PartialTurn` (type) — What a turn had said when it ended without coming back: the answer, the scratchpad and the calls, as far as each had arrived.
|
|
390
394
|
- `Produced` (type) — Whether the model has said anything a second attempt would say twice.
|
|
391
395
|
- `StreamTurnOptions` (type) — What `streamTurn` takes besides the request body.
|
|
392
396
|
- `streamTurn` — Runs one turn as a stream, reporting tokens as they arrive and assembling them back into a message.
|
|
393
397
|
- `Turn` (type) — One streamed turn, put back together into the shape a loop and a transcript work with.
|
|
394
398
|
- `TurnUsage` (type) — What a turn cost, and how it went.
|
|
399
|
+
- `UsageTotals` (type) — A run's token totals, in the spelling every run-level usage shares.
|
|
395
400
|
|
|
396
401
|
### thinking
|
|
397
402
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.19.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",
|