@cubicecho/agent-core 4.19.0 → 4.21.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 +41 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/run/agent-loop.d.ts +42 -2
- package/dist/run/agent-loop.js +40 -17
- package/dist/run/artifacts.d.ts +145 -0
- package/dist/run/artifacts.js +412 -0
- package/dist/run/events.d.ts +5 -3
- package/dist/run/events.js +5 -3
- package/dist/run/run-calls.d.ts +21 -4
- package/dist/run/run-calls.js +120 -39
- package/dist/wire/tool-calls.d.ts +64 -6
- package/dist/wire/tool-calls.js +22 -0
- package/llms.txt +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -568,6 +568,38 @@ its calls made on the error too, read off with `failedRun`.
|
|
|
568
568
|
A link meant for the model alone — one whose MCP `audience` leaves out `"user"` — is the
|
|
569
569
|
dispatcher's to drop, since that is MCP's rule and this package knows nothing of MCP.
|
|
570
570
|
|
|
571
|
+
A link is the only source that takes no guessing, and the default. For servers that never send
|
|
572
|
+
one, `artifacts` turns on the two that do, each off unless asked for:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
const { artifacts } = await runAgentLoop({
|
|
576
|
+
// ...
|
|
577
|
+
artifacts: { declare: true, detect: true },
|
|
578
|
+
});
|
|
579
|
+
// [{ link, callId, tool, step, count, source: "declared", action: "updated", sizeBytes }]
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
- **`declare`** puts `record_artifact` (`RECORD_ARTIFACT_TOOL`) beside `hostTools` on every step,
|
|
583
|
+
in every mode, and the loop answers it without `dispatch` seeing it. It takes `location`,
|
|
584
|
+
`title`, `description` and `mediaType`. A declaration is believed only where some successful
|
|
585
|
+
call in the run was given or answered with that location — otherwise a model with no file tools
|
|
586
|
+
"saves" its answer to a file it never wrote — and the refusal is the call's answer, for the model
|
|
587
|
+
to read. A believed one is put down to the latest call that touched the location. In a
|
|
588
|
+
`parallel` batch it waits for the batch's other calls, so a write beside it counts.
|
|
589
|
+
- **`detect`** reads every successful call for the shape of a write: a verb at the front of the
|
|
590
|
+
tool's own name (`write`, `create`, `save`, `upload`, `edit`, `move`, `delete`…) and a path in
|
|
591
|
+
its arguments (`destination`, `path`, `file_path`, `uri`…). Creating a folder is not an artifact.
|
|
592
|
+
A predicate in place of `true` reads only the calls it passes, which is how a host keeps its own
|
|
593
|
+
tools' writes off the list. `detectArtifact` is the same reading, for a host to call itself.
|
|
594
|
+
|
|
595
|
+
One artifact per location per run, whichever sources found it. A later write moves `action`
|
|
596
|
+
(`created`, `updated`, `moved`, `deleted`) and `sizeBytes` on; a declaration over a detection
|
|
597
|
+
fills in the title and description and takes the `source` over, and never turns an `updated` back
|
|
598
|
+
into `created`. A link is the server's word: a declaration over one fills only what it left out,
|
|
599
|
+
and a link over either guess takes over. `source` is absent on a linked artifact, so the shape
|
|
600
|
+
from before reads the same — `artifactSource` reads it as `linked`. The `artifact` event goes out
|
|
601
|
+
once per location; the result's `artifacts` say what each ended as.
|
|
602
|
+
|
|
571
603
|
### A call that ends the run
|
|
572
604
|
|
|
573
605
|
A tool whose call is the end of the job — handing off, asking a person — says so with `end` on
|
|
@@ -610,8 +642,10 @@ string. What still is not an object throws a `ToolArgumentsError` whose `kind` i
|
|
|
610
642
|
the turn stopped at the ceiling, with a message telling the model so, and `malformed` otherwise.
|
|
611
643
|
The repaired JSON is what the transcript keeps, and an unreadable call is replayed as `{}`, because
|
|
612
644
|
a server that parses replayed arguments refuses the originals on every later request. `dispatch`
|
|
613
|
-
is still handed the model's own text as `raw`,
|
|
614
|
-
|
|
645
|
+
is still handed the model's own text as `raw`, with `repaired` true where reading it took a repair
|
|
646
|
+
— whitespace and blank arguments do not count — and the dedupe compares repaired arguments, so
|
|
647
|
+
`{'a': 1}` and `{"a": 1}` are one call. A call recovered from text, below, is always `repaired`,
|
|
648
|
+
and its `raw` is the arguments recovery read out of the reply, as JSON.
|
|
615
649
|
|
|
616
650
|
A server whose tool-call parser was written for another template streams the model's call as
|
|
617
651
|
plain text, and the run ends on a reply that is nothing but a call nobody made. Unless
|
|
@@ -969,7 +1003,11 @@ try {
|
|
|
969
1003
|
|
|
970
1004
|
`onMessage` hears the assistant message as it will be replayed — recovered calls folded in, and
|
|
971
1005
|
arguments repaired, which the turn `onTurn` is handed does not show — together with that `Turn`,
|
|
972
|
-
and then each tool result, in transcript order.
|
|
1006
|
+
and then each tool result, in transcript order. Beside an assistant message the model wrote comes a
|
|
1007
|
+
fourth argument, `{ raw }`: the same message as the model produced it, its text whole with any call
|
|
1008
|
+
written into it left there, and its calls' arguments exactly as streamed, under the same ids. Store
|
|
1009
|
+
or show `raw` to keep what the model said; replay `message` to keep what an endpoint will take back.
|
|
1010
|
+
The loop's own messages — a proxied run's preselected load — come without it. `onTurn` is not awaited and never sees a tool
|
|
973
1011
|
result; this is the one to store from. What it returns is ignored, since `beforeStep` is the one way to rewrite
|
|
974
1012
|
the transcript, and what it throws ends the run.
|
|
975
1013
|
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools,
|
|
|
19
19
|
export { CACHE_PROMPT_FIELD, resetSlots, SESSION_HEADER, SLOT_FIELD, type SlotAllocator, slotAllocator, } from './endpoint/slots.ts';
|
|
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
|
-
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, type StepWindow, } from './run/agent-loop.ts';
|
|
22
|
+
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, type StepWindow, type WrittenMessage, } from './run/agent-loop.ts';
|
|
23
|
+
export { type ArtifactDraft, type ArtifactOptions, artifactSource, declaredArtifact, detectArtifact, mediaTypeFor, RECORD_ARTIFACT, RECORD_ARTIFACT_TOOL, } from './run/artifacts.ts';
|
|
23
24
|
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
25
|
export { buildBody } from './run/request-body.ts';
|
|
25
26
|
export { preview, type ToolDispatch } from './run/run-calls.ts';
|
|
@@ -40,5 +41,5 @@ export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow,
|
|
|
40
41
|
export { CacheBreakReason, type InFlight, type LoopUsage, type PartialTurn, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, type UsageTotals, } from './wire/stream.ts';
|
|
41
42
|
export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, SPLIT_OUTPUT, SPLIT_REASONING, type Split, stripThinking, THINK_FENCE, } from './wire/thinking.ts';
|
|
42
43
|
export { type ContextBreakdown, type ContextBreakdownOptions, compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, type TokenEstimateOptions, } from './wire/tokens.ts';
|
|
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';
|
|
44
|
+
export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, ArtifactAction, ArtifactSource, 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';
|
|
44
45
|
export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire/wire.ts';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { CACHE_PROMPT_FIELD, resetSlots, SESSION_HEADER, SLOT_FIELD, slotAllocat
|
|
|
20
20
|
export { HOOK_EVENTS, HookEvent, INJECT_EVENTS } from './hooks/hook-events.js';
|
|
21
21
|
export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from './hooks/hooks.js';
|
|
22
22
|
export { PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, } from './run/agent-loop.js';
|
|
23
|
+
export { artifactSource, declaredArtifact, detectArtifact, mediaTypeFor, RECORD_ARTIFACT, RECORD_ARTIFACT_TOOL, } from './run/artifacts.js';
|
|
23
24
|
export { configureEvents, emit, endRun, fold, history, RunEventKind, RunOutcome, resetEvents, runMetrics, watch, } from './run/events.js';
|
|
24
25
|
export { buildBody } from './run/request-body.js';
|
|
25
26
|
export { preview } from './run/run-calls.js';
|
|
@@ -39,5 +40,5 @@ export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow,
|
|
|
39
40
|
export { CacheBreakReason, streamTurn, } from './wire/stream.js';
|
|
40
41
|
export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, SPLIT_OUTPUT, SPLIT_REASONING, stripThinking, THINK_FENCE, } from './wire/thinking.js';
|
|
41
42
|
export { compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, } from './wire/tokens.js';
|
|
42
|
-
export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, recoverToolCalls, ServerLogLevel, ToolArgumentsError, } from './wire/tool-calls.js';
|
|
43
|
+
export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, ArtifactAction, ArtifactSource, parseToolArguments, recoverToolCalls, ServerLogLevel, ToolArgumentsError, } from './wire/tool-calls.js';
|
|
43
44
|
export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire/wire.js';
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { type ToolOrder } from '../tools/tool-loading.ts';
|
|
|
11
11
|
import { type SideTask } from '../turn/side-task.ts';
|
|
12
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
|
+
import { type ArtifactOptions } from './artifacts.ts';
|
|
14
15
|
import { type RunEventInput, type RunMetrics } from './events.ts';
|
|
15
16
|
import { type ToolDispatch } from './run-calls.ts';
|
|
16
17
|
/**
|
|
@@ -23,6 +24,24 @@ import { type ToolDispatch } from './run-calls.ts';
|
|
|
23
24
|
* and two never did. What is here is the part that does not know what the run is for; the
|
|
24
25
|
* prompts, the tools and what a run means stay with the caller.
|
|
25
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* What `onMessage` is handed beside an assistant message the model wrote: that message as the model
|
|
29
|
+
* produced it.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* The message `onMessage` is handed is what later requests replay — arguments repaired, `{}` for
|
|
33
|
+
* none, recovered calls taken out of the text and made calls — which is right to send and quietly
|
|
34
|
+
* wrong to show as the model's. Absent for a message the loop wrote itself, such as a proxied
|
|
35
|
+
* run's preselected load.
|
|
36
|
+
*/
|
|
37
|
+
export interface WrittenMessage {
|
|
38
|
+
/**
|
|
39
|
+
* The reply as it streamed: its text whole, a call written into it as text left there, and the
|
|
40
|
+
* calls it sent as calls with their arguments as streamed. A call that never got a name is left
|
|
41
|
+
* off, since nothing answers it. Ids are the turn's, so the tool results pair with either.
|
|
42
|
+
*/
|
|
43
|
+
raw: OpenAI.ChatCompletionAssistantMessageParam;
|
|
44
|
+
}
|
|
26
45
|
/**
|
|
27
46
|
* One step's request as `onRequest` is handed it: what the model is about to read.
|
|
28
47
|
*
|
|
@@ -361,6 +380,23 @@ export interface AgentLoopOptions {
|
|
|
361
380
|
* `idempotentHint` annotations can answer it; nothing in an OpenAI tool definition can.
|
|
362
381
|
*/
|
|
363
382
|
dedupeToolCalls?: boolean | ((call: ToolCallRequest) => boolean);
|
|
383
|
+
/**
|
|
384
|
+
* Collects artifacts from the two sources that take a guess, beside the `links` every run keeps:
|
|
385
|
+
* `declare` has the model name them through `record_artifact`, and `detect` reads each successful
|
|
386
|
+
* call for the shape of a write. Both off by default.
|
|
387
|
+
*
|
|
388
|
+
* @remarks
|
|
389
|
+
* For servers that never send a `resource_link`, which is the source to rely on. One artifact per
|
|
390
|
+
* location either way: a later write moves `action` on, a declaration over a detection fills in
|
|
391
|
+
* the title and description and takes the `source` over, and a link over either takes over too.
|
|
392
|
+
*
|
|
393
|
+
* `record_artifact` is declared beside `hostTools`, in every mode, and answered by the loop
|
|
394
|
+
* without `dispatch` seeing it. A declaration is believed only where some successful call in the
|
|
395
|
+
* run was given or answered with the location, and is otherwise refused for the model to read;
|
|
396
|
+
* run in a batch, it waits for the batch's other calls. `onToolCall` and `onToolResult` hear it
|
|
397
|
+
* as they hear `load_tools`, and it counts in `toolCalls` but not in `used`.
|
|
398
|
+
*/
|
|
399
|
+
artifacts?: ArtifactOptions;
|
|
364
400
|
/**
|
|
365
401
|
* Told of every call the model made, as it starts — the ones `dispatch` never sees included:
|
|
366
402
|
* `load_tools`, an identical repeat answered from the first, and a call whose arguments could
|
|
@@ -459,8 +495,12 @@ export interface AgentLoopOptions {
|
|
|
459
495
|
* a proxied run writes for its `preselected` shortlist, told as step zero's before any request.
|
|
460
496
|
* What it returns is ignored: `beforeStep` is the one way to rewrite the transcript. What it
|
|
461
497
|
* throws ends the run, and it is not called again for that run.
|
|
498
|
+
*
|
|
499
|
+
* Beside an assistant message the model wrote comes `written`, whose `raw` is the message as the
|
|
500
|
+
* model produced it, for a host that shows or stores what was said rather than what is replayed.
|
|
501
|
+
* See `WrittenMessage`.
|
|
462
502
|
*/
|
|
463
|
-
onMessage?: (message: OpenAI.ChatCompletionMessageParam, step: number, turn?: Turn) => void | Promise<void>;
|
|
503
|
+
onMessage?: (message: OpenAI.ChatCompletionMessageParam, step: number, turn?: Turn, written?: WrittenMessage) => void | Promise<void>;
|
|
464
504
|
/**
|
|
465
505
|
* How many times an answer cut off at `maxTokens` is continued, zero for never. Opt-in because
|
|
466
506
|
* it spends another request, and on a server that does not continue a trailing assistant
|
|
@@ -510,7 +550,7 @@ export interface AgentLoopResult {
|
|
|
510
550
|
/**
|
|
511
551
|
* What the run's calls linked to, one per uri in the order first seen, each with the call and
|
|
512
552
|
* step that first linked to it and how many calls did. Empty where `dispatch` answered with
|
|
513
|
-
* strings only.
|
|
553
|
+
* strings only and `artifacts` collected nothing else.
|
|
514
554
|
*/
|
|
515
555
|
artifacts: RunArtifact[];
|
|
516
556
|
/**
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -20,9 +20,10 @@ 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';
|
|
23
|
+
import { RECORD_ARTIFACT_TOOL, RunArtifacts } from './artifacts.js';
|
|
23
24
|
import { RunEventKind, RunOutcome, runMetrics, stamp, } from './events.js';
|
|
24
25
|
import { buildBody } from './request-body.js';
|
|
25
|
-
import { assistantMessage, definedAs, loadShortlist, readCalls, runCalls, } from './run-calls.js';
|
|
26
|
+
import { assistantMessage, definedAs, loadShortlist, readCalls, runCalls, writtenMessage, } from './run-calls.js';
|
|
26
27
|
/** The `preselectRouting` that sends the first step the shortlist alone. */
|
|
27
28
|
export const PRESELECT_EXCLUSIVE = 'exclusive';
|
|
28
29
|
/** The `preselectRouting` that loads the shortlist after what was carried, and sends the first step like any other. */
|
|
@@ -136,7 +137,7 @@ export async function runAgentLoop(options) {
|
|
|
136
137
|
}),
|
|
137
138
|
};
|
|
138
139
|
let result;
|
|
139
|
-
const apart = hostToolsApart(options);
|
|
140
|
+
const apart = hostToolsApart(withRecordTool(options));
|
|
140
141
|
try {
|
|
141
142
|
pin = await pinSession(apart);
|
|
142
143
|
result = await runSteps(apart, standing, pin);
|
|
@@ -440,6 +441,23 @@ function hostToolsApart(options) {
|
|
|
440
441
|
...(preselected ? { preselected: preselected.filter(isCatalogued) } : {}),
|
|
441
442
|
};
|
|
442
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* The run's options with `record_artifact` among the host's tools, where the run declares.
|
|
446
|
+
*
|
|
447
|
+
* @param options - What `runAgentLoop` was given. Not written to.
|
|
448
|
+
* @returns `options` itself where `artifacts.declare` is off, and otherwise a copy whose `hostTools`
|
|
449
|
+
* open with the loop's definition, which a host tool of the same name does not displace.
|
|
450
|
+
*
|
|
451
|
+
* @remarks
|
|
452
|
+
* A host tool in all but who answers it, so it is declared in every mode, kept off the catalogue,
|
|
453
|
+
* recoverable from text, and refused through `call_tool` exactly as the host's own are.
|
|
454
|
+
*/
|
|
455
|
+
function withRecordTool(options) {
|
|
456
|
+
if (options.artifacts?.declare !== true) {
|
|
457
|
+
return options;
|
|
458
|
+
}
|
|
459
|
+
return { ...options, hostTools: loadedTools([RECORD_ARTIFACT_TOOL], options.hostTools ?? []) };
|
|
460
|
+
}
|
|
443
461
|
/** What opens the notice naming the loop's own picks, ahead of the names. */
|
|
444
462
|
export const PRESELECTED_NOTICE = 'tools picked before the run: ';
|
|
445
463
|
/**
|
|
@@ -502,8 +520,8 @@ function definitionsByName(tools) {
|
|
|
502
520
|
* Offers each message written into the transcript to the host.
|
|
503
521
|
*
|
|
504
522
|
* @param onMessage - The host's callback, awaited. Absent offers nothing.
|
|
505
|
-
* @returns The offer, taking a message, its step
|
|
506
|
-
* host has taken the message, and rejects with what the host threw — once.
|
|
523
|
+
* @returns The offer, taking a message, its step, the turn it came with and what the model wrote:
|
|
524
|
+
* it resolves once the host has taken the message, and rejects with what the host threw — once.
|
|
507
525
|
*
|
|
508
526
|
* @remarks
|
|
509
527
|
* Once `onMessage` has thrown the run is ending on that, and the results still to be written are
|
|
@@ -511,12 +529,12 @@ function definitionsByName(tools) {
|
|
|
511
529
|
*/
|
|
512
530
|
function announcer(onMessage) {
|
|
513
531
|
let heard = true;
|
|
514
|
-
return async (message, step, turn) => {
|
|
532
|
+
return async (message, step, turn, written) => {
|
|
515
533
|
if (!onMessage || !heard) {
|
|
516
534
|
return;
|
|
517
535
|
}
|
|
518
536
|
try {
|
|
519
|
-
await onMessage(message, step, turn);
|
|
537
|
+
await onMessage(message, step, turn, written);
|
|
520
538
|
}
|
|
521
539
|
catch (error) {
|
|
522
540
|
heard = false;
|
|
@@ -558,21 +576,21 @@ const usageEvent = (usage, turn, ledger, contextLength) => {
|
|
|
558
576
|
* nothing is looked for in a turn that made a named call of its own or has no text.
|
|
559
577
|
* @param notice - Told when calls were recovered, and how many.
|
|
560
578
|
* @returns `calls`, the turn's own less any with no name, and `content`, its text as it was — or,
|
|
561
|
-
* where calls were recovered, those calls and the text without them.
|
|
579
|
+
* where calls were recovered, those calls and the text without them, and `recovered` true.
|
|
562
580
|
*/
|
|
563
581
|
function turnCalls(turn, recoverable, notice) {
|
|
564
582
|
// A call with no name is a fragment the server never finished sending: nothing to run, and
|
|
565
583
|
// an assistant message naming it would be answered by nothing.
|
|
566
584
|
const calls = turn.toolCalls.filter((call) => call.function.name);
|
|
567
585
|
if (!recoverable || calls.length || !turn.content) {
|
|
568
|
-
return { calls, content: turn.content };
|
|
586
|
+
return { calls, content: turn.content, recovered: false };
|
|
569
587
|
}
|
|
570
588
|
const recovered = recoverToolCalls(turn.content, { names: recoverable });
|
|
571
589
|
if (!recovered.toolCalls.length) {
|
|
572
|
-
return { calls, content: turn.content };
|
|
590
|
+
return { calls, content: turn.content, recovered: false };
|
|
573
591
|
}
|
|
574
592
|
notice(`recovered ${counted(recovered.toolCalls.length, 'tool call')} the model wrote as text; the server's tool-call parser does not match this model's template`);
|
|
575
|
-
return { calls: recovered.toolCalls, content: recovered.content };
|
|
593
|
+
return { calls: recovered.toolCalls, content: recovered.content, recovered: true };
|
|
576
594
|
}
|
|
577
595
|
/**
|
|
578
596
|
* The steps of `runAgentLoop`, throwing what they caught as it was caught.
|
|
@@ -586,7 +604,7 @@ function turnCalls(turn, recoverable, notice) {
|
|
|
586
604
|
*/
|
|
587
605
|
async function runSteps(options, standing, pin) {
|
|
588
606
|
const { config, system = '', tools = [], catalog = [], dispatch, hooks, signal } = options;
|
|
589
|
-
const { onRequest, onTurn, onMessage, onToolCall, onToolResult, beforeStep, parallel = false, recoverToolCalls: recover = true, maxContinuations = 0, toolOrder = true, dedupeToolCalls = true, preselectRouting = PRESELECT_EXCLUSIVE, serverInstructions = false, } = options;
|
|
607
|
+
const { onRequest, onTurn, onMessage, onToolCall, onToolResult, beforeStep, parallel = false, recoverToolCalls: recover = true, maxContinuations = 0, toolOrder = true, dedupeToolCalls = true, preselectRouting = PRESELECT_EXCLUSIVE, serverInstructions = false, artifacts: collecting = {}, } = options;
|
|
590
608
|
const dedupable = typeof dedupeToolCalls === 'function' ? dedupeToolCalls : () => dedupeToolCalls;
|
|
591
609
|
const started = Date.now();
|
|
592
610
|
const { recorded, onEvent } = recorder(options.onEvent);
|
|
@@ -637,7 +655,9 @@ async function runSteps(options, standing, pin) {
|
|
|
637
655
|
let usage = noUsage();
|
|
638
656
|
let cacheReported = false;
|
|
639
657
|
const toolCalls = [];
|
|
640
|
-
const artifacts = new
|
|
658
|
+
const artifacts = new RunArtifacts();
|
|
659
|
+
const { detect = false } = collecting;
|
|
660
|
+
const detects = typeof detect === 'function' ? detect : detect ? () => true : undefined;
|
|
641
661
|
// A box per request, emptied once its turn is in `messages`: a failure while the tools run had
|
|
642
662
|
// no request in flight.
|
|
643
663
|
let inFlight = {};
|
|
@@ -648,7 +668,7 @@ async function runSteps(options, standing, pin) {
|
|
|
648
668
|
...(partial ? { partial } : {}),
|
|
649
669
|
usage: loopUsage(usage, cacheReported),
|
|
650
670
|
toolCalls,
|
|
651
|
-
artifacts:
|
|
671
|
+
artifacts: artifacts.list(),
|
|
652
672
|
// As the result would have said it: proxied, nothing is loaded that the history does not hold.
|
|
653
673
|
loaded: proxied ? [] : [...loaded],
|
|
654
674
|
used: [...used],
|
|
@@ -693,6 +713,8 @@ async function runSteps(options, standing, pin) {
|
|
|
693
713
|
used,
|
|
694
714
|
toolCalls,
|
|
695
715
|
artifacts,
|
|
716
|
+
declares: collecting.declare === true,
|
|
717
|
+
detects,
|
|
696
718
|
dedupable,
|
|
697
719
|
dispatch,
|
|
698
720
|
parallel,
|
|
@@ -850,15 +872,16 @@ async function runSteps(options, standing, pin) {
|
|
|
850
872
|
notice(`the model stopped at maxTokens (${config.maxTokens}); this turn is cut short`);
|
|
851
873
|
}
|
|
852
874
|
const canRecover = recover && (tools.length > 0 || hostTools.length > 0 || onDemand);
|
|
853
|
-
const { calls, content } = turnCalls(turn, canRecover ? recoverable : undefined, notice);
|
|
875
|
+
const { calls, content, recovered } = turnCalls(turn, canRecover ? recoverable : undefined, notice);
|
|
854
876
|
const shown = { ...turn, content, toolCalls: calls };
|
|
855
877
|
onTurn?.(shown, step);
|
|
856
878
|
// Read before the assistant message is written, so what is replayed on every later request
|
|
857
879
|
// is the repaired JSON.
|
|
858
|
-
const parsed = readCalls(calls, turn.finishReason);
|
|
880
|
+
const parsed = readCalls(calls, turn.finishReason, recovered);
|
|
859
881
|
const assistant = assistantMessage(content, parsed);
|
|
860
882
|
messages.push(assistant);
|
|
861
|
-
|
|
883
|
+
// Recovered calls were never calls on the wire: what the model said is the text they came from.
|
|
884
|
+
await announce(assistant, step, shown, { raw: writtenMessage(turn.content, recovered ? [] : calls) });
|
|
862
885
|
const isAnswer = calls.length === 0;
|
|
863
886
|
const ended = isAnswer ? undefined : await runCalls(calling, parsed, messages, step);
|
|
864
887
|
if (ended) {
|
|
@@ -883,7 +906,7 @@ async function runSteps(options, standing, pin) {
|
|
|
883
906
|
messages,
|
|
884
907
|
usage: loopUsage(usage, cacheReported),
|
|
885
908
|
toolCalls,
|
|
886
|
-
artifacts:
|
|
909
|
+
artifacts: artifacts.list(),
|
|
887
910
|
loaded: proxied ? [] : [...loaded],
|
|
888
911
|
used: [...used],
|
|
889
912
|
preselected: [...preselected],
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
import { ArtifactAction, ArtifactSource, type ResourceLink, type RunArtifact, type ToolCallRequest } from '../wire/tool-calls.ts';
|
|
3
|
+
/**
|
|
4
|
+
* What a run left behind, from the two sources that take a guess: the model saying so, and a call
|
|
5
|
+
* that reads as a write. kanban-server and telos each grew these rules, and this is them in one
|
|
6
|
+
* place; a call's `links`, which take no guessing, are kept by the same ledger.
|
|
7
|
+
*
|
|
8
|
+
* A declaration is believed only where some call in the run was given or answered with that
|
|
9
|
+
* location. Without that, a model with no file tools "saves" its answer to a file it never wrote.
|
|
10
|
+
*/
|
|
11
|
+
/** The name of the tool the loop declares and answers itself when `artifacts.declare` is on. */
|
|
12
|
+
export declare const RECORD_ARTIFACT = "record_artifact";
|
|
13
|
+
/** The definition of `record_artifact`, as the loop declares it beside the host's own tools. */
|
|
14
|
+
export declare const RECORD_ARTIFACT_TOOL: OpenAI.ChatCompletionFunctionTool;
|
|
15
|
+
/**
|
|
16
|
+
* Which of the guessing sources a run collects, beside the links every run keeps.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Both are off by default: `linked` is the source to rely on, and these are for servers that
|
|
20
|
+
* never send a `resource_link`.
|
|
21
|
+
*/
|
|
22
|
+
export interface ArtifactOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Declares `record_artifact` on every step and answers it in the loop, never through `dispatch`.
|
|
25
|
+
* The host's own tool of that name, if it has one, is not dispatched while this is on.
|
|
26
|
+
*
|
|
27
|
+
* @defaultValue `false`
|
|
28
|
+
*/
|
|
29
|
+
declare?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Reads every successful call for the shape of a write. A predicate reads only the calls it
|
|
32
|
+
* passes, which is how a host keeps its own tools' writes — a note on its board — off the list.
|
|
33
|
+
*
|
|
34
|
+
* @defaultValue `false`
|
|
35
|
+
*/
|
|
36
|
+
detect?: boolean | ((call: ToolCallRequest) => boolean);
|
|
37
|
+
}
|
|
38
|
+
/** What one declaration or detection said of a location, before it is merged into the run's. */
|
|
39
|
+
export interface ArtifactDraft {
|
|
40
|
+
/** The path or URI, trimmed. */
|
|
41
|
+
location: string;
|
|
42
|
+
source: typeof ArtifactSource.Declared | typeof ArtifactSource.Detected;
|
|
43
|
+
/** What was done to it. A declaration says `created`, and never moves an earlier write's. */
|
|
44
|
+
action: ArtifactAction;
|
|
45
|
+
title?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
/** As declared, or else a guess off the extension. */
|
|
48
|
+
mediaType?: string;
|
|
49
|
+
/** The bytes of the content the write carried, where its arguments held it. */
|
|
50
|
+
sizeBytes?: number;
|
|
51
|
+
}
|
|
52
|
+
/** The call an artifact is put down to: the one that linked it, wrote it, or touched it. */
|
|
53
|
+
export type ArtifactFrom = Pick<RunArtifact, 'callId' | 'tool' | 'step'>;
|
|
54
|
+
/**
|
|
55
|
+
* What a successful call wrote, if its name and arguments look like a write.
|
|
56
|
+
*
|
|
57
|
+
* @param name - The tool, as the call named it: `fs__write_file` and `write_file` read alike, the
|
|
58
|
+
* verb being the last `__` segment.
|
|
59
|
+
* @param args - The call's arguments.
|
|
60
|
+
* @returns The draft, or `null` where the name has no write verb, names a folder, or the arguments
|
|
61
|
+
* hold no location.
|
|
62
|
+
*/
|
|
63
|
+
export declare function detectArtifact(name: string, args: Record<string, unknown>): ArtifactDraft | null;
|
|
64
|
+
/**
|
|
65
|
+
* What the model said through `record_artifact`.
|
|
66
|
+
*
|
|
67
|
+
* @param args - The call's arguments.
|
|
68
|
+
* @returns The draft, or `null` where it named no location.
|
|
69
|
+
*/
|
|
70
|
+
export declare function declaredArtifact(args: Record<string, unknown>): ArtifactDraft | null;
|
|
71
|
+
/**
|
|
72
|
+
* A media type guessed off a location's extension.
|
|
73
|
+
*
|
|
74
|
+
* @param location - The path or URI. A query or fragment is ignored.
|
|
75
|
+
* @returns The media type, or `null` for a name with no extension, a dotfile, or an extension this
|
|
76
|
+
* table does not know.
|
|
77
|
+
*/
|
|
78
|
+
export declare function mediaTypeFor(location: string): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* How an artifact came to be known, reading an absent `source` as the link it always was.
|
|
81
|
+
*
|
|
82
|
+
* @param artifact - One of a run's artifacts.
|
|
83
|
+
* @returns Its source, `linked` where it carries none.
|
|
84
|
+
*/
|
|
85
|
+
export declare const artifactSource: (artifact: Pick<RunArtifact, 'source'>) => ArtifactSource;
|
|
86
|
+
/**
|
|
87
|
+
* A run's artifacts, one per location, and what its calls touched for a declaration to be checked
|
|
88
|
+
* against.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* The merge rules are kanban-server's. A later write moves `action` and `sizeBytes` on and leaves
|
|
92
|
+
* the rest; a declaration over a detection fills in what only the model could say and takes the
|
|
93
|
+
* source over, without turning an `updated` back into `created`. A link is the server's word, so
|
|
94
|
+
* a declaration over one fills only what it left out, and a link over either guess takes over.
|
|
95
|
+
* Each method that adds a location answers with the new artifact, for the caller to announce;
|
|
96
|
+
* a merge into one already known answers nothing, since the event says when a location was first
|
|
97
|
+
* seen and the result says what it ended as.
|
|
98
|
+
*/
|
|
99
|
+
export declare class RunArtifacts {
|
|
100
|
+
#private;
|
|
101
|
+
/**
|
|
102
|
+
* The artifacts, in the order first seen.
|
|
103
|
+
*
|
|
104
|
+
* @returns A new array of the artifacts themselves, which later merges keep writing to.
|
|
105
|
+
*/
|
|
106
|
+
list(): RunArtifact[];
|
|
107
|
+
/**
|
|
108
|
+
* Adds one call's links, counting a uri seen before and taking over a guess at the same one.
|
|
109
|
+
*
|
|
110
|
+
* @param links - What the call's answer linked to, a uri given twice counted once.
|
|
111
|
+
* @param from - The call they came from.
|
|
112
|
+
* @returns The artifacts this call added, in the order its links gave them.
|
|
113
|
+
*/
|
|
114
|
+
link(links: ResourceLink[], from: ArtifactFrom): RunArtifact[];
|
|
115
|
+
/**
|
|
116
|
+
* Keeps what a successful call was given and answered, so a declaration naming it is believed.
|
|
117
|
+
*
|
|
118
|
+
* @param args - The call's arguments. Every string in them, however deep, is kept bare.
|
|
119
|
+
* @param answer - What the call answered, kept whole.
|
|
120
|
+
* @param links - The uris its answer linked to.
|
|
121
|
+
* @param from - The call.
|
|
122
|
+
*/
|
|
123
|
+
witness(args: Record<string, unknown>, answer: string, links: ResourceLink[], from: ArtifactFrom): void;
|
|
124
|
+
/**
|
|
125
|
+
* The latest call in the run that was given or answered with `location`, which is telos's rule
|
|
126
|
+
* for believing a declaration.
|
|
127
|
+
*
|
|
128
|
+
* @param location - The path or URI a declaration names.
|
|
129
|
+
* @returns That call, or `undefined` where none was. A blank location was touched by none.
|
|
130
|
+
*/
|
|
131
|
+
seen(location: string): ArtifactFrom | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Merges a declaration or a detection into the artifact at its location, or adds one.
|
|
134
|
+
*
|
|
135
|
+
* @param draft - What was said of the location.
|
|
136
|
+
* @param from - The call to put a new artifact down to: the write, or for a declaration the call
|
|
137
|
+
* that touched the location. A merge keeps the earlier one's.
|
|
138
|
+
* @returns The artifact where this added one, and `undefined` where it merged into one known.
|
|
139
|
+
*
|
|
140
|
+
* @remarks
|
|
141
|
+
* A declaration also finds an artifact whose location is the same bare, so a model's
|
|
142
|
+
* `/work/a.md` describes the `work/a.md` the write was given rather than standing beside it.
|
|
143
|
+
*/
|
|
144
|
+
add(draft: ArtifactDraft, from: ArtifactFrom): RunArtifact | undefined;
|
|
145
|
+
}
|