@cubicecho/agent-core 4.20.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 +32 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/run/agent-loop.d.ts +19 -1
- package/dist/run/agent-loop.js +27 -5
- 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 +8 -3
- package/dist/run/run-calls.js +79 -33
- package/dist/wire/tool-calls.d.ts +48 -5
- package/dist/wire/tool-calls.js +22 -0
- package/llms.txt +15 -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
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +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, 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
|
/**
|
|
@@ -379,6 +380,23 @@ export interface AgentLoopOptions {
|
|
|
379
380
|
* `idempotentHint` annotations can answer it; nothing in an OpenAI tool definition can.
|
|
380
381
|
*/
|
|
381
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;
|
|
382
400
|
/**
|
|
383
401
|
* Told of every call the model made, as it starts — the ones `dispatch` never sees included:
|
|
384
402
|
* `load_tools`, an identical repeat answered from the first, and a call whose arguments could
|
|
@@ -532,7 +550,7 @@ export interface AgentLoopResult {
|
|
|
532
550
|
/**
|
|
533
551
|
* What the run's calls linked to, one per uri in the order first seen, each with the call and
|
|
534
552
|
* step that first linked to it and how many calls did. Empty where `dispatch` answered with
|
|
535
|
-
* strings only.
|
|
553
|
+
* strings only and `artifacts` collected nothing else.
|
|
536
554
|
*/
|
|
537
555
|
artifacts: RunArtifact[];
|
|
538
556
|
/**
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -20,6 +20,7 @@ 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
26
|
import { assistantMessage, definedAs, loadShortlist, readCalls, runCalls, writtenMessage, } from './run-calls.js';
|
|
@@ -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
|
/**
|
|
@@ -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,
|
|
@@ -884,7 +906,7 @@ async function runSteps(options, standing, pin) {
|
|
|
884
906
|
messages,
|
|
885
907
|
usage: loopUsage(usage, cacheReported),
|
|
886
908
|
toolCalls,
|
|
887
|
-
artifacts:
|
|
909
|
+
artifacts: artifacts.list(),
|
|
888
910
|
loaded: proxied ? [] : [...loaded],
|
|
889
911
|
used: [...used],
|
|
890
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
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { ArtifactAction, ArtifactSource, } from '../wire/tool-calls.js';
|
|
2
|
+
import { FUNCTION_TOOL, SchemaType } from '../wire/wire.js';
|
|
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 const RECORD_ARTIFACT = 'record_artifact';
|
|
13
|
+
/** The definition of `record_artifact`, as the loop declares it beside the host's own tools. */
|
|
14
|
+
export const RECORD_ARTIFACT_TOOL = {
|
|
15
|
+
type: FUNCTION_TOOL,
|
|
16
|
+
function: {
|
|
17
|
+
name: RECORD_ARTIFACT,
|
|
18
|
+
description: 'Name something a tool call in this run stored, so the run lists it: a file one of your tools ' +
|
|
19
|
+
'wrote, a page it published, an object it uploaded. Only for a location a tool was actually ' +
|
|
20
|
+
'given or answered with, and only after that call: one that no tool call in this run touched ' +
|
|
21
|
+
'is refused. Your reply is already kept, so never record it, and never invent a file to put ' +
|
|
22
|
+
'it in. Calling this adds the title and description a person would want.',
|
|
23
|
+
parameters: {
|
|
24
|
+
type: SchemaType.Object,
|
|
25
|
+
properties: {
|
|
26
|
+
location: {
|
|
27
|
+
type: SchemaType.String,
|
|
28
|
+
description: 'The path or URI, exactly as the tool that stored it was given it.',
|
|
29
|
+
},
|
|
30
|
+
title: { type: SchemaType.String, description: 'A short name for it.' },
|
|
31
|
+
description: { type: SchemaType.String, description: 'One sentence on what it is for.' },
|
|
32
|
+
mediaType: { type: SchemaType.String, description: 'e.g. text/markdown. Optional.' },
|
|
33
|
+
},
|
|
34
|
+
required: ['location'],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
/** A verb at the front of a tool name that means something was written somewhere. */
|
|
40
|
+
const WRITE_VERB = /^(write|create|save|upload|put|append|edit|move|rename|copy|publish|delete|remove)(_|$)/;
|
|
41
|
+
/** Writes that leave nothing a person would call an artifact behind. */
|
|
42
|
+
const NOT_ARTIFACTS = new Set(['create_directory', 'create_dir', 'create_folder', 'mkdir']);
|
|
43
|
+
/** The argument names a location goes by, in the order they are believed. */
|
|
44
|
+
const LOCATION_KEYS = [
|
|
45
|
+
'destination',
|
|
46
|
+
'dest',
|
|
47
|
+
'target',
|
|
48
|
+
'path',
|
|
49
|
+
'file_path',
|
|
50
|
+
'filePath',
|
|
51
|
+
'filename',
|
|
52
|
+
'file',
|
|
53
|
+
'uri',
|
|
54
|
+
'url',
|
|
55
|
+
'key',
|
|
56
|
+
];
|
|
57
|
+
/** The argument names a write's content goes by, in the order they are believed. */
|
|
58
|
+
const CONTENT_KEYS = ['content', 'contents', 'text', 'body', 'data'];
|
|
59
|
+
/** The separator between a server's slug and its tool, in the names a pool hands the loop. */
|
|
60
|
+
const SLUG_SEPARATOR = '__';
|
|
61
|
+
const encoder = new TextEncoder();
|
|
62
|
+
/**
|
|
63
|
+
* What a write did, read off its verb.
|
|
64
|
+
*
|
|
65
|
+
* @param verb - The tool's own name, lowercased, after any slug.
|
|
66
|
+
* @returns `updated` for an edit or append, `moved` for a move or rename, `deleted` for a delete or
|
|
67
|
+
* remove, and `created` for everything else.
|
|
68
|
+
*/
|
|
69
|
+
function actionOf(verb) {
|
|
70
|
+
if (/^(edit|append)/.test(verb)) {
|
|
71
|
+
return ArtifactAction.Updated;
|
|
72
|
+
}
|
|
73
|
+
if (/^(move|rename)/.test(verb)) {
|
|
74
|
+
return ArtifactAction.Moved;
|
|
75
|
+
}
|
|
76
|
+
if (/^(delete|remove)/.test(verb)) {
|
|
77
|
+
return ArtifactAction.Deleted;
|
|
78
|
+
}
|
|
79
|
+
return ArtifactAction.Created;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The first of these arguments that is a string, under the first key that holds one.
|
|
83
|
+
*
|
|
84
|
+
* @param args - The call's arguments.
|
|
85
|
+
* @param keys - The names to try, in order.
|
|
86
|
+
* @param [isBlankOk] - Whether an empty or all-space string counts. It does for content.
|
|
87
|
+
* @returns The string, or `undefined` where none of the keys held one.
|
|
88
|
+
*/
|
|
89
|
+
function firstString(args, keys, isBlankOk = false) {
|
|
90
|
+
return keys
|
|
91
|
+
.map((key) => args[key])
|
|
92
|
+
.find((value) => typeof value === 'string' && (isBlankOk || value.trim() !== ''));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* What a successful call wrote, if its name and arguments look like a write.
|
|
96
|
+
*
|
|
97
|
+
* @param name - The tool, as the call named it: `fs__write_file` and `write_file` read alike, the
|
|
98
|
+
* verb being the last `__` segment.
|
|
99
|
+
* @param args - The call's arguments.
|
|
100
|
+
* @returns The draft, or `null` where the name has no write verb, names a folder, or the arguments
|
|
101
|
+
* hold no location.
|
|
102
|
+
*/
|
|
103
|
+
export function detectArtifact(name, args) {
|
|
104
|
+
const verb = name.split(SLUG_SEPARATOR).at(-1)?.toLowerCase() ?? '';
|
|
105
|
+
const isWrite = WRITE_VERB.test(verb) && NOT_ARTIFACTS.has(verb) === false;
|
|
106
|
+
const location = isWrite ? firstString(args, LOCATION_KEYS) : undefined;
|
|
107
|
+
if (location === undefined) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const content = firstString(args, CONTENT_KEYS, true);
|
|
111
|
+
const mediaType = mediaTypeFor(location);
|
|
112
|
+
return {
|
|
113
|
+
location: location.trim(),
|
|
114
|
+
source: ArtifactSource.Detected,
|
|
115
|
+
action: actionOf(verb),
|
|
116
|
+
...(mediaType ? { mediaType } : {}),
|
|
117
|
+
...(content === undefined ? {} : { sizeBytes: encoder.encode(content).byteLength }),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What the model said through `record_artifact`.
|
|
122
|
+
*
|
|
123
|
+
* @param args - The call's arguments.
|
|
124
|
+
* @returns The draft, or `null` where it named no location.
|
|
125
|
+
*/
|
|
126
|
+
export function declaredArtifact(args) {
|
|
127
|
+
const text = (key) => (typeof args[key] === 'string' ? args[key].trim() : '');
|
|
128
|
+
const location = text('location');
|
|
129
|
+
if (location === '') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const mediaType = text('mediaType') || mediaTypeFor(location);
|
|
133
|
+
return {
|
|
134
|
+
location,
|
|
135
|
+
source: ArtifactSource.Declared,
|
|
136
|
+
action: ArtifactAction.Created,
|
|
137
|
+
...(text('title') ? { title: text('title') } : {}),
|
|
138
|
+
...(text('description') ? { description: text('description') } : {}),
|
|
139
|
+
...(mediaType ? { mediaType } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const MEDIA_TYPES = {
|
|
143
|
+
md: 'text/markdown',
|
|
144
|
+
markdown: 'text/markdown',
|
|
145
|
+
txt: 'text/plain',
|
|
146
|
+
csv: 'text/csv',
|
|
147
|
+
html: 'text/html',
|
|
148
|
+
htm: 'text/html',
|
|
149
|
+
css: 'text/css',
|
|
150
|
+
json: 'application/json',
|
|
151
|
+
yaml: 'application/yaml',
|
|
152
|
+
yml: 'application/yaml',
|
|
153
|
+
xml: 'application/xml',
|
|
154
|
+
js: 'text/javascript',
|
|
155
|
+
ts: 'text/x-typescript',
|
|
156
|
+
tsx: 'text/x-typescript',
|
|
157
|
+
py: 'text/x-python',
|
|
158
|
+
sh: 'application/x-sh',
|
|
159
|
+
svg: 'image/svg+xml',
|
|
160
|
+
png: 'image/png',
|
|
161
|
+
jpg: 'image/jpeg',
|
|
162
|
+
jpeg: 'image/jpeg',
|
|
163
|
+
gif: 'image/gif',
|
|
164
|
+
webp: 'image/webp',
|
|
165
|
+
pdf: 'application/pdf',
|
|
166
|
+
zip: 'application/zip',
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* A media type guessed off a location's extension.
|
|
170
|
+
*
|
|
171
|
+
* @param location - The path or URI. A query or fragment is ignored.
|
|
172
|
+
* @returns The media type, or `null` for a name with no extension, a dotfile, or an extension this
|
|
173
|
+
* table does not know.
|
|
174
|
+
*/
|
|
175
|
+
export function mediaTypeFor(location) {
|
|
176
|
+
const name = location.split(/[?#]/)[0].split(/[\\/]/).at(-1) ?? '';
|
|
177
|
+
const dot = name.lastIndexOf('.');
|
|
178
|
+
if (dot <= 0) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return MEDIA_TYPES[name.slice(dot + 1).toLowerCase()] ?? null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* How an artifact came to be known, reading an absent `source` as the link it always was.
|
|
185
|
+
*
|
|
186
|
+
* @param artifact - One of a run's artifacts.
|
|
187
|
+
* @returns Its source, `linked` where it carries none.
|
|
188
|
+
*/
|
|
189
|
+
export const artifactSource = (artifact) => artifact.source ?? ArtifactSource.Linked;
|
|
190
|
+
/**
|
|
191
|
+
* A location as it is compared: no scheme, no leading slashes, no trailing ones.
|
|
192
|
+
*
|
|
193
|
+
* @param location - The path or URI.
|
|
194
|
+
* @returns The bare form, so `/work/a.md`, `work/a.md` and `file:///work/a.md` compare equal.
|
|
195
|
+
*/
|
|
196
|
+
function bare(location) {
|
|
197
|
+
return location
|
|
198
|
+
.trim()
|
|
199
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '')
|
|
200
|
+
.replace(/^\/+|\/+$/g, '');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A declared or detected location as a link, the shape every artifact is kept in.
|
|
204
|
+
*
|
|
205
|
+
* @param draft - What was said of it.
|
|
206
|
+
* @returns A link whose `uri` is the location and whose `name` is its last segment.
|
|
207
|
+
*/
|
|
208
|
+
function linkOf(draft) {
|
|
209
|
+
const name = draft.location.split(/[?#]/)[0].split(/[\\/]/).filter(Boolean).at(-1) ?? draft.location;
|
|
210
|
+
return {
|
|
211
|
+
uri: draft.location,
|
|
212
|
+
name,
|
|
213
|
+
...(draft.title ? { title: draft.title } : {}),
|
|
214
|
+
...(draft.description ? { description: draft.description } : {}),
|
|
215
|
+
...(draft.mediaType ? { mimeType: draft.mediaType } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* What tells one call from another across a run: an id is distinct only within its step.
|
|
220
|
+
*
|
|
221
|
+
* @param from - The call.
|
|
222
|
+
* @returns Its step and id as one string.
|
|
223
|
+
*/
|
|
224
|
+
const callKey = ({ callId, step }) => `${step}\0${callId}`;
|
|
225
|
+
/**
|
|
226
|
+
* A run's artifacts, one per location, and what its calls touched for a declaration to be checked
|
|
227
|
+
* against.
|
|
228
|
+
*
|
|
229
|
+
* @remarks
|
|
230
|
+
* The merge rules are kanban-server's. A later write moves `action` and `sizeBytes` on and leaves
|
|
231
|
+
* the rest; a declaration over a detection fills in what only the model could say and takes the
|
|
232
|
+
* source over, without turning an `updated` back into `created`. A link is the server's word, so
|
|
233
|
+
* a declaration over one fills only what it left out, and a link over either guess takes over.
|
|
234
|
+
* Each method that adds a location answers with the new artifact, for the caller to announce;
|
|
235
|
+
* a merge into one already known answers nothing, since the event says when a location was first
|
|
236
|
+
* seen and the result says what it ended as.
|
|
237
|
+
*/
|
|
238
|
+
export class RunArtifacts {
|
|
239
|
+
#byLocation = new Map();
|
|
240
|
+
/** Each string a call was given or answered with, bare, beside the call. Kept only to declare. */
|
|
241
|
+
#witnessed = [];
|
|
242
|
+
/** The last call each artifact was counted for, so a call that links and writes one counts once. */
|
|
243
|
+
#countedFor = new WeakMap();
|
|
244
|
+
/**
|
|
245
|
+
* The artifacts, in the order first seen.
|
|
246
|
+
*
|
|
247
|
+
* @returns A new array of the artifacts themselves, which later merges keep writing to.
|
|
248
|
+
*/
|
|
249
|
+
list() {
|
|
250
|
+
return [...this.#byLocation.values()];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Adds one call's links, counting a uri seen before and taking over a guess at the same one.
|
|
254
|
+
*
|
|
255
|
+
* @param links - What the call's answer linked to, a uri given twice counted once.
|
|
256
|
+
* @param from - The call they came from.
|
|
257
|
+
* @returns The artifacts this call added, in the order its links gave them.
|
|
258
|
+
*/
|
|
259
|
+
link(links, from) {
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
const added = [];
|
|
262
|
+
for (const link of links) {
|
|
263
|
+
if (seen.has(link.uri)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
seen.add(link.uri);
|
|
267
|
+
const known = this.#byLocation.get(link.uri);
|
|
268
|
+
if (known === undefined) {
|
|
269
|
+
const artifact = { link, ...from, count: 1 };
|
|
270
|
+
this.#byLocation.set(link.uri, artifact);
|
|
271
|
+
this.#countedFor.set(artifact, callKey(from));
|
|
272
|
+
added.push(artifact);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
this.#count(known, from);
|
|
276
|
+
if (known.source !== undefined) {
|
|
277
|
+
// What a guess said of it stays only where the server's own link says nothing.
|
|
278
|
+
const { title, description, mimeType } = known.link;
|
|
279
|
+
known.link = {
|
|
280
|
+
...(title ? { title } : {}),
|
|
281
|
+
...(description ? { description } : {}),
|
|
282
|
+
...(mimeType ? { mimeType } : {}),
|
|
283
|
+
...link,
|
|
284
|
+
};
|
|
285
|
+
delete known.source;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return added;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Keeps what a successful call was given and answered, so a declaration naming it is believed.
|
|
292
|
+
*
|
|
293
|
+
* @param args - The call's arguments. Every string in them, however deep, is kept bare.
|
|
294
|
+
* @param answer - What the call answered, kept whole.
|
|
295
|
+
* @param links - The uris its answer linked to.
|
|
296
|
+
* @param from - The call.
|
|
297
|
+
*/
|
|
298
|
+
witness(args, answer, links, from) {
|
|
299
|
+
const strings = (value) => {
|
|
300
|
+
if (typeof value === 'string') {
|
|
301
|
+
this.#witnessed.push({ text: bare(value), from });
|
|
302
|
+
}
|
|
303
|
+
else if (Array.isArray(value)) {
|
|
304
|
+
value.forEach(strings);
|
|
305
|
+
}
|
|
306
|
+
else if (value && typeof value === 'object') {
|
|
307
|
+
Object.values(value).forEach(strings);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
strings(args);
|
|
311
|
+
this.#witnessed.push({ text: answer, from });
|
|
312
|
+
for (const { uri } of links) {
|
|
313
|
+
this.#witnessed.push({ text: bare(uri), from });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* The latest call in the run that was given or answered with `location`, which is telos's rule
|
|
318
|
+
* for believing a declaration.
|
|
319
|
+
*
|
|
320
|
+
* @param location - The path or URI a declaration names.
|
|
321
|
+
* @returns That call, or `undefined` where none was. A blank location was touched by none.
|
|
322
|
+
*/
|
|
323
|
+
seen(location) {
|
|
324
|
+
const wanted = bare(location);
|
|
325
|
+
if (wanted === '') {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
return this.#witnessed.findLast(({ text }) => text === wanted || text.includes(wanted))?.from;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Merges a declaration or a detection into the artifact at its location, or adds one.
|
|
332
|
+
*
|
|
333
|
+
* @param draft - What was said of the location.
|
|
334
|
+
* @param from - The call to put a new artifact down to: the write, or for a declaration the call
|
|
335
|
+
* that touched the location. A merge keeps the earlier one's.
|
|
336
|
+
* @returns The artifact where this added one, and `undefined` where it merged into one known.
|
|
337
|
+
*
|
|
338
|
+
* @remarks
|
|
339
|
+
* A declaration also finds an artifact whose location is the same bare, so a model's
|
|
340
|
+
* `/work/a.md` describes the `work/a.md` the write was given rather than standing beside it.
|
|
341
|
+
*/
|
|
342
|
+
add(draft, from) {
|
|
343
|
+
const known = this.#find(draft);
|
|
344
|
+
if (known === undefined) {
|
|
345
|
+
const artifact = {
|
|
346
|
+
link: linkOf(draft),
|
|
347
|
+
...from,
|
|
348
|
+
count: 1,
|
|
349
|
+
source: draft.source,
|
|
350
|
+
...(draft.source === ArtifactSource.Detected ? { action: draft.action } : {}),
|
|
351
|
+
...(draft.sizeBytes === undefined ? {} : { sizeBytes: draft.sizeBytes }),
|
|
352
|
+
};
|
|
353
|
+
this.#byLocation.set(draft.location, artifact);
|
|
354
|
+
this.#countedFor.set(artifact, callKey(from));
|
|
355
|
+
return artifact;
|
|
356
|
+
}
|
|
357
|
+
if (draft.source === ArtifactSource.Detected) {
|
|
358
|
+
this.#count(known, from);
|
|
359
|
+
known.action = draft.action;
|
|
360
|
+
if (draft.sizeBytes !== undefined) {
|
|
361
|
+
known.sizeBytes = draft.sizeBytes;
|
|
362
|
+
}
|
|
363
|
+
if (known.link.mimeType === undefined && draft.mediaType) {
|
|
364
|
+
known.link = { ...known.link, mimeType: draft.mediaType };
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
const isLinked = known.source === undefined;
|
|
369
|
+
// Over a link, only what it left out; over a guess, what the model said wins.
|
|
370
|
+
const fill = (field, value) => {
|
|
371
|
+
const isTaken = isLinked && Boolean(known.link[field]);
|
|
372
|
+
if (value && isTaken === false) {
|
|
373
|
+
known.link = { ...known.link, [field]: value };
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
fill('title', draft.title);
|
|
377
|
+
fill('description', draft.description);
|
|
378
|
+
fill('mimeType', draft.mediaType);
|
|
379
|
+
if (isLinked === false) {
|
|
380
|
+
known.source = ArtifactSource.Declared;
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Counts one more call that made an artifact, unless that call was counted for it already.
|
|
386
|
+
*
|
|
387
|
+
* @param artifact - The artifact. Written to.
|
|
388
|
+
* @param from - The call.
|
|
389
|
+
*/
|
|
390
|
+
#count(artifact, from) {
|
|
391
|
+
const key = callKey(from);
|
|
392
|
+
if (this.#countedFor.get(artifact) !== key) {
|
|
393
|
+
artifact.count++;
|
|
394
|
+
this.#countedFor.set(artifact, key);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The artifact a draft lands on: the one at its location, or for a declaration one that is the
|
|
399
|
+
* same location bare.
|
|
400
|
+
*
|
|
401
|
+
* @param draft - What was said.
|
|
402
|
+
* @returns The artifact, or `undefined` where the location is new.
|
|
403
|
+
*/
|
|
404
|
+
#find(draft) {
|
|
405
|
+
const exact = this.#byLocation.get(draft.location);
|
|
406
|
+
if (exact !== undefined || draft.source === ArtifactSource.Detected) {
|
|
407
|
+
return exact;
|
|
408
|
+
}
|
|
409
|
+
const wanted = bare(draft.location);
|
|
410
|
+
return [...this.#byLocation.values()].find((artifact) => bare(artifact.link.uri) === wanted);
|
|
411
|
+
}
|
|
412
|
+
}
|
package/dist/run/events.d.ts
CHANGED
|
@@ -91,9 +91,11 @@ export declare const RunEventKind: {
|
|
|
91
91
|
/** Something the runner did that is not the model's doing — a preselection, a retry. */
|
|
92
92
|
readonly Notice: 'notice';
|
|
93
93
|
/**
|
|
94
|
-
* A tool call's answer linked to something the run had not produced before
|
|
95
|
-
*
|
|
96
|
-
*
|
|
94
|
+
* A tool call's answer linked to something the run had not produced before — or, where the run
|
|
95
|
+
* collects them, a write was detected or a declaration believed at a new location. `id` is the
|
|
96
|
+
* call's the artifact is put down to, `name` its tool, `text` the link's title or else its uri,
|
|
97
|
+
* and `link` the link itself. Once per location per run: a later call that links or writes it
|
|
98
|
+
* again only adds to its count, and a declaration over it only fills it in.
|
|
97
99
|
*/
|
|
98
100
|
readonly Artifact: 'artifact';
|
|
99
101
|
/**
|
package/dist/run/events.js
CHANGED
|
@@ -58,9 +58,11 @@ export const RunEventKind = {
|
|
|
58
58
|
/** Something the runner did that is not the model's doing — a preselection, a retry. */
|
|
59
59
|
Notice: 'notice',
|
|
60
60
|
/**
|
|
61
|
-
* A tool call's answer linked to something the run had not produced before
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* A tool call's answer linked to something the run had not produced before — or, where the run
|
|
62
|
+
* collects them, a write was detected or a declaration believed at a new location. `id` is the
|
|
63
|
+
* call's the artifact is put down to, `name` its tool, `text` the link's title or else its uri,
|
|
64
|
+
* and `link` the link itself. Once per location per run: a later call that links or writes it
|
|
65
|
+
* again only adds to its count, and a declaration over it only fills it in.
|
|
64
66
|
*/
|
|
65
67
|
Artifact: 'artifact',
|
|
66
68
|
/**
|
package/dist/run/run-calls.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
2
|
import type { CatalogServer } from '../tools/catalog.ts';
|
|
3
3
|
import type { Turn } from '../wire/stream.ts';
|
|
4
|
-
import { type EndedBy, type
|
|
4
|
+
import { type EndedBy, type ToolAnswer, type ToolCall, type ToolCallOutcome, type ToolCallReport, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
|
|
5
|
+
import { type RunArtifacts } from './artifacts.ts';
|
|
5
6
|
import type { LoadCounts, RunEventInput } from './events.ts';
|
|
6
7
|
/**
|
|
7
8
|
* The tools between two turns: a step's calls read, run and written into the transcript.
|
|
@@ -104,8 +105,12 @@ export interface Calling {
|
|
|
104
105
|
loads: LoadCounts;
|
|
105
106
|
used: Set<string>;
|
|
106
107
|
toolCalls: ToolCallOutcome[];
|
|
107
|
-
/** What the run's calls linked to,
|
|
108
|
-
artifacts:
|
|
108
|
+
/** What the run's calls linked to, wrote, or were said to have stored, by location. */
|
|
109
|
+
artifacts: RunArtifacts;
|
|
110
|
+
/** Whether `record_artifact` is the loop's to answer, and every successful call is witnessed. */
|
|
111
|
+
declares: boolean;
|
|
112
|
+
/** Which successful calls are read for the shape of a write. Absent where none are. */
|
|
113
|
+
detects: ((call: ToolCallRequest) => boolean) | undefined;
|
|
109
114
|
dedupable: (call: ToolCallRequest) => boolean;
|
|
110
115
|
dispatch: ToolDispatch;
|
|
111
116
|
/** All of a step's calls together, each alone, or together in runs of the calls it passes. */
|
package/dist/run/run-calls.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CALL_TOOL, expandNames, inCatalog, LOAD_TOOLS, loadResult, proxiedCall,
|
|
|
3
3
|
import { errorMessage } from '../wire/errors.js';
|
|
4
4
|
import { parseToolArguments, } from '../wire/tool-calls.js';
|
|
5
5
|
import { FUNCTION_TOOL, Role } from '../wire/wire.js';
|
|
6
|
+
import { declaredArtifact, detectArtifact, RECORD_ARTIFACT, } from './artifacts.js';
|
|
6
7
|
import { RunEventKind } from './events.js';
|
|
7
8
|
/**
|
|
8
9
|
* The tools between two turns: a step's calls read, run and written into the transcript.
|
|
@@ -262,28 +263,57 @@ async function once(answered, key, make) {
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
/**
|
|
265
|
-
*
|
|
266
|
+
* Tells watchers of artifacts the run had not produced before.
|
|
266
267
|
*
|
|
267
|
-
* @param run - The run
|
|
268
|
-
* @param
|
|
269
|
-
* @param from - The call they came from.
|
|
268
|
+
* @param run - The run, whose watchers hear of them.
|
|
269
|
+
* @param added - The new artifacts, each told once.
|
|
270
270
|
*/
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
271
|
+
function announceArtifacts(run, added) {
|
|
272
|
+
for (const { link, callId, tool } of added) {
|
|
273
|
+
run.onEvent({ kind: RunEventKind.Artifact, id: callId, name: tool, text: link.title || link.uri, link });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Adds what one successful call produced to the run's artifacts: its links, and — where the run
|
|
278
|
+
* collects them — what it was given and answered, and the write it reads as.
|
|
279
|
+
*
|
|
280
|
+
* @param run - The run. Written to: its `artifacts`.
|
|
281
|
+
* @param request - The call, under the tool it was shown as.
|
|
282
|
+
* @param content - What it answered.
|
|
283
|
+
* @param links - What its answer linked to.
|
|
284
|
+
* @param from - The call, as an artifact is put down to it.
|
|
285
|
+
*/
|
|
286
|
+
function collect(run, request, content, links, from) {
|
|
287
|
+
announceArtifacts(run, run.artifacts.link(links, from));
|
|
288
|
+
if (run.declares) {
|
|
289
|
+
run.artifacts.witness(request.args, content, links, from);
|
|
286
290
|
}
|
|
291
|
+
const draft = run.detects?.(request) ? detectArtifact(request.name, request.args) : null;
|
|
292
|
+
const added = draft ? run.artifacts.add(draft, from) : undefined;
|
|
293
|
+
announceArtifacts(run, added ? [added] : []);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Answers a `record_artifact`, keeping the declaration where some call in the run touched the
|
|
297
|
+
* location.
|
|
298
|
+
*
|
|
299
|
+
* @param run - The run. Written to: its `artifacts`, where the declaration is believed.
|
|
300
|
+
* @param args - The call's arguments.
|
|
301
|
+
* @returns What the model reads. Throws, which the model reads instead, for a declaration with no
|
|
302
|
+
* location or one no call in the run was given or answered with.
|
|
303
|
+
*/
|
|
304
|
+
function answerRecord(run, args) {
|
|
305
|
+
const draft = declaredArtifact(args);
|
|
306
|
+
if (draft === null) {
|
|
307
|
+
throw new Error(`${RECORD_ARTIFACT} needs a location.`);
|
|
308
|
+
}
|
|
309
|
+
const from = run.artifacts.seen(draft.location);
|
|
310
|
+
if (from === undefined) {
|
|
311
|
+
throw new Error(`No tool call in this run touched ${draft.location}, so it was not recorded. ` +
|
|
312
|
+
'Record only what one of your tools stored, after the call that stored it.');
|
|
313
|
+
}
|
|
314
|
+
const added = run.artifacts.add(draft, from);
|
|
315
|
+
announceArtifacts(run, added ? [added] : []);
|
|
316
|
+
return `Recorded ${draft.location}.`;
|
|
287
317
|
}
|
|
288
318
|
/**
|
|
289
319
|
* A call looked through and built into the request every host callback is handed.
|
|
@@ -323,10 +353,11 @@ function prepare(run, entry) {
|
|
|
323
353
|
// By the name the model called, not the one a `call_tool` wraps: `load_tools` is not in
|
|
324
354
|
// the catalogue, and one reached through `call_tool` is refused like any other.
|
|
325
355
|
const load = onDemand && call.function.name === LOAD_TOOLS;
|
|
326
|
-
const
|
|
356
|
+
const record = run.declares && call.function.name === RECORD_ARTIFACT;
|
|
357
|
+
const dispatched = args !== undefined && load === false && record === false && refused === undefined;
|
|
327
358
|
// Keyed on the inner call, so one made through `call_tool` and one made natively share.
|
|
328
359
|
const key = `${name}\0${proxy ? JSON.stringify(inner) : normal}`;
|
|
329
|
-
return { entry, request, load, refused, dedupable: dispatched && run.dedupable(request), key };
|
|
360
|
+
return { entry, request, load, record, refused, dedupable: dispatched && run.dedupable(request), key };
|
|
330
361
|
}
|
|
331
362
|
/**
|
|
332
363
|
* Whether a call is answered without `dispatch` seeing it, so it can run beside anything.
|
|
@@ -334,10 +365,15 @@ function prepare(run, entry) {
|
|
|
334
365
|
* @param prepared - The call.
|
|
335
366
|
* @param answered - The step's calls already made, by key. A call that threw is not in it.
|
|
336
367
|
* @param batch - The keys of the calls in the batch being gathered, which a repeat shares.
|
|
337
|
-
* @returns True for a `load_tools`, a call whose arguments could not be read,
|
|
338
|
-
* was refused, and an identical repeat that `dedupeToolCalls` answers from the
|
|
368
|
+
* @returns True for a `load_tools`, a `record_artifact`, a call whose arguments could not be read,
|
|
369
|
+
* a `call_tool` that was refused, and an identical repeat that `dedupeToolCalls` answers from the
|
|
370
|
+
* first.
|
|
339
371
|
*/
|
|
340
|
-
const answersItself = ({ entry, load, refused, dedupable, key }, answered, batch) => entry.args === undefined ||
|
|
372
|
+
const answersItself = ({ entry, load, record, refused, dedupable, key }, answered, batch) => entry.args === undefined ||
|
|
373
|
+
load ||
|
|
374
|
+
record ||
|
|
375
|
+
refused !== undefined ||
|
|
376
|
+
(dedupable && (answered.has(key) || batch.has(key)));
|
|
341
377
|
/**
|
|
342
378
|
* Where the batch of calls starting at `from` ends: every call, one call, or the calls up to the
|
|
343
379
|
* first one the host's predicate does not pass.
|
|
@@ -390,7 +426,7 @@ function batchEnd(run, ready, from, total, answered) {
|
|
|
390
426
|
*/
|
|
391
427
|
async function runCall(run, prepared, answered, step) {
|
|
392
428
|
const { catalog, onDemand, proxied, signal } = run;
|
|
393
|
-
const { entry, request, load, refused, dedupable, key } = prepared;
|
|
429
|
+
const { entry, request, load, record, refused, dedupable, key } = prepared;
|
|
394
430
|
const { call, args, error: unreadable } = entry;
|
|
395
431
|
const { name, raw } = request;
|
|
396
432
|
run.onEvent({ kind: RunEventKind.ToolCall, id: call.id, name, text: preview(raw) });
|
|
@@ -410,6 +446,9 @@ async function runCall(run, prepared, answered, step) {
|
|
|
410
446
|
if (load) {
|
|
411
447
|
({ content, ok, counts: loads } = answerLoad(run, args));
|
|
412
448
|
}
|
|
449
|
+
else if (record) {
|
|
450
|
+
content = answerRecord(run, args);
|
|
451
|
+
}
|
|
413
452
|
else {
|
|
414
453
|
if (refused) {
|
|
415
454
|
throw refused.error;
|
|
@@ -449,9 +488,10 @@ async function runCall(run, prepared, answered, step) {
|
|
|
449
488
|
text: preview(content),
|
|
450
489
|
...(loads ? { loads } : {}),
|
|
451
490
|
});
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
491
|
+
// Only what `dispatch` answered made anything, and a repeat made nothing the first did not.
|
|
492
|
+
const isMade = ok && load === false && record === false && shared === false;
|
|
493
|
+
if (isMade) {
|
|
494
|
+
collect(run, request, content, links, { callId: call.id, tool: name, step });
|
|
455
495
|
}
|
|
456
496
|
const result = { id: call.id, name, ok, content, ...(links.length ? { links } : {}) };
|
|
457
497
|
run.onToolResult?.(result);
|
|
@@ -515,11 +555,17 @@ export async function runCalls(run, parsed, messages, step) {
|
|
|
515
555
|
const start = from;
|
|
516
556
|
const to = batchEnd(run, ready, start, parsed.length, answered);
|
|
517
557
|
begun = to;
|
|
518
|
-
|
|
519
|
-
const { result, end } = await runCall(run, ready(
|
|
520
|
-
outcomes[
|
|
521
|
-
ends[
|
|
522
|
-
}
|
|
558
|
+
const runAt = async (at) => {
|
|
559
|
+
const { result, end } = await runCall(run, ready(at), answered, step);
|
|
560
|
+
outcomes[at] = result;
|
|
561
|
+
ends[at] = end;
|
|
562
|
+
};
|
|
563
|
+
// A declaration describes what the batch beside it stored, so it waits for the rest of the
|
|
564
|
+
// batch: run together, it would be checked before the write it names had answered.
|
|
565
|
+
const positions = parsed.slice(start, to).map((_, offset) => start + offset);
|
|
566
|
+
const isRecord = (at) => ready(at).record;
|
|
567
|
+
await Promise.all(positions.filter((at) => isRecord(at) === false).map(runAt));
|
|
568
|
+
await Promise.all(positions.filter(isRecord).map(runAt));
|
|
523
569
|
for (const { id, name, ok, content } of outcomes.slice(from, to).flatMap((outcome) => outcome ?? [])) {
|
|
524
570
|
toolCalls.push({ id, name, ok });
|
|
525
571
|
await keep(id, content);
|
|
@@ -278,25 +278,68 @@ export interface ToolCallReport {
|
|
|
278
278
|
*/
|
|
279
279
|
log: (entry: ServerLogEntry) => void;
|
|
280
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* How the loop came to know of an artifact, most trusted first.
|
|
283
|
+
*
|
|
284
|
+
* @remarks
|
|
285
|
+
* `linked` is a call's answer saying so, and is the only one that takes no guessing. `declared` is
|
|
286
|
+
* the model naming it through `record_artifact`, believed only where some call in the run touched
|
|
287
|
+
* the location. `detected` is the loop reading a successful call as a write: a verb at the front
|
|
288
|
+
* of its name and a path in its arguments. The last two are opt-in, through `artifacts` on the
|
|
289
|
+
* loop's options.
|
|
290
|
+
*/
|
|
291
|
+
export declare const ArtifactSource: {
|
|
292
|
+
readonly Linked: 'linked';
|
|
293
|
+
readonly Declared: 'declared';
|
|
294
|
+
readonly Detected: 'detected';
|
|
295
|
+
};
|
|
296
|
+
/** Any one of the sources in `ArtifactSource`. */
|
|
297
|
+
export type ArtifactSource = (typeof ArtifactSource)[keyof typeof ArtifactSource];
|
|
298
|
+
/** What the run's last write did to an artifact's location, as a detection read it. */
|
|
299
|
+
export declare const ArtifactAction: {
|
|
300
|
+
readonly Created: 'created';
|
|
301
|
+
readonly Updated: 'updated';
|
|
302
|
+
readonly Moved: 'moved';
|
|
303
|
+
readonly Deleted: 'deleted';
|
|
304
|
+
};
|
|
305
|
+
/** Any one of the actions in `ArtifactAction`. */
|
|
306
|
+
export type ArtifactAction = (typeof ArtifactAction)[keyof typeof ArtifactAction];
|
|
281
307
|
/**
|
|
282
308
|
* One thing a run's tool calls produced: the first link to a uri, and where it came from.
|
|
283
309
|
*
|
|
284
310
|
* @remarks
|
|
285
311
|
* A link is what one call answered with; an artifact is what the run made, one per uri however
|
|
286
|
-
* many calls linked to it.
|
|
312
|
+
* many calls linked to it. A declared or detected artifact is one per location the same way, and
|
|
313
|
+
* its `link` is built from what was said of it: the location as `uri`, its last segment as `name`.
|
|
287
314
|
*/
|
|
288
315
|
export interface RunArtifact {
|
|
289
|
-
/**
|
|
316
|
+
/**
|
|
317
|
+
* The link as the first call to give its uri gave it. A later call's says nothing new here, and
|
|
318
|
+
* a declaration only fills in a `title` or `description` it left out.
|
|
319
|
+
*/
|
|
290
320
|
link: ResourceLink;
|
|
291
|
-
/** The call that first linked to it. */
|
|
321
|
+
/** The call that first linked to it, wrote it, or — for a declaration alone — last touched it. */
|
|
292
322
|
callId: string;
|
|
293
323
|
/** That call's tool. For a `call_tool`, the tool it named. */
|
|
294
324
|
tool: string;
|
|
295
325
|
/** The loop's step that call was made in, from 0. */
|
|
296
326
|
step: number;
|
|
297
327
|
/**
|
|
298
|
-
* How many calls linked to it. A deduplicated repeat does not count, since nothing
|
|
299
|
-
* twice,
|
|
328
|
+
* How many calls linked to it or wrote it. A deduplicated repeat does not count, since nothing
|
|
329
|
+
* was made twice, a call that links one uri twice counts once, and a declaration counts none.
|
|
300
330
|
*/
|
|
301
331
|
count: number;
|
|
332
|
+
/**
|
|
333
|
+
* How it came to be known: `linked` where absent, which is every artifact a call's answer
|
|
334
|
+
* linked to, so the shape from before the other two sources still reads the same. A declaration
|
|
335
|
+
* over a detection takes the source over; a link over either does too.
|
|
336
|
+
*/
|
|
337
|
+
source?: ArtifactSource;
|
|
338
|
+
/**
|
|
339
|
+
* What the latest write a detection read did to it. Absent where no detection did: a link or a
|
|
340
|
+
* declaration says a thing exists, not what was done to it.
|
|
341
|
+
*/
|
|
342
|
+
action?: ArtifactAction;
|
|
343
|
+
/** The bytes the latest detected write carried, where its arguments held the content. */
|
|
344
|
+
sizeBytes?: number;
|
|
302
345
|
}
|
package/dist/wire/tool-calls.js
CHANGED
|
@@ -489,3 +489,25 @@ export const ServerLogLevel = {
|
|
|
489
489
|
Alert: 'alert',
|
|
490
490
|
Emergency: 'emergency',
|
|
491
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* How the loop came to know of an artifact, most trusted first.
|
|
494
|
+
*
|
|
495
|
+
* @remarks
|
|
496
|
+
* `linked` is a call's answer saying so, and is the only one that takes no guessing. `declared` is
|
|
497
|
+
* the model naming it through `record_artifact`, believed only where some call in the run touched
|
|
498
|
+
* the location. `detected` is the loop reading a successful call as a write: a verb at the front
|
|
499
|
+
* of its name and a path in its arguments. The last two are opt-in, through `artifacts` on the
|
|
500
|
+
* loop's options.
|
|
501
|
+
*/
|
|
502
|
+
export const ArtifactSource = {
|
|
503
|
+
Linked: 'linked',
|
|
504
|
+
Declared: 'declared',
|
|
505
|
+
Detected: 'detected',
|
|
506
|
+
};
|
|
507
|
+
/** What the run's last write did to an artifact's location, as a detection read it. */
|
|
508
|
+
export const ArtifactAction = {
|
|
509
|
+
Created: 'created',
|
|
510
|
+
Updated: 'updated',
|
|
511
|
+
Moved: 'moved',
|
|
512
|
+
Deleted: 'deleted',
|
|
513
|
+
};
|
package/llms.txt
CHANGED
|
@@ -178,6 +178,19 @@ The loop above a turn: send, run the tools the model asked for, send again, unti
|
|
|
178
178
|
- `StepWindow` (type) — How full the window is as a step is about to be sent, as `beforeStep` is told it.
|
|
179
179
|
- `WrittenMessage` (type) — What `onMessage` is handed beside an assistant message the model wrote: that message as the model produced it.
|
|
180
180
|
|
|
181
|
+
### artifacts
|
|
182
|
+
|
|
183
|
+
What a run left behind, from the two sources that take a guess: the model saying so, and a call that reads as a write.
|
|
184
|
+
|
|
185
|
+
- `ArtifactDraft` (type) — What one declaration or detection said of a location, before it is merged into the run's.
|
|
186
|
+
- `ArtifactOptions` (type) — Which of the guessing sources a run collects, beside the links every run keeps.
|
|
187
|
+
- `artifactSource` — How an artifact came to be known, reading an absent `source` as the link it always was.
|
|
188
|
+
- `declaredArtifact` — What the model said through `record_artifact`.
|
|
189
|
+
- `detectArtifact` — What a successful call wrote, if its name and arguments look like a write.
|
|
190
|
+
- `mediaTypeFor` — A media type guessed off a location's extension.
|
|
191
|
+
- `RECORD_ARTIFACT` — The name of the tool the loop declares and answers itself when `artifacts.declare` is on.
|
|
192
|
+
- `RECORD_ARTIFACT_TOOL` — The definition of `record_artifact`, as the loop declares it beside the host's own tools.
|
|
193
|
+
|
|
181
194
|
### events
|
|
182
195
|
|
|
183
196
|
What a run is doing, while it is doing it.
|
|
@@ -433,6 +446,8 @@ Reading what a model meant by a tool call when it did not write one cleanly.
|
|
|
433
446
|
|
|
434
447
|
- `ARGUMENTS_MALFORMED` — The `ToolArgumentsError` kind for a call the model wrote wrongly.
|
|
435
448
|
- `ARGUMENTS_TRUNCATED` — The `ToolArgumentsError` kind for a call cut off at the reply ceiling.
|
|
449
|
+
- `ArtifactAction` — Any one of the actions in `ArtifactAction`.
|
|
450
|
+
- `ArtifactSource` — Any one of the sources in `ArtifactSource`.
|
|
436
451
|
- `EndedBy` (type) — The call that ended a run through its answer's `end`, as `AgentLoopResult.ended` says it.
|
|
437
452
|
- `parseToolArguments` — A tool call's arguments as the object the tool is handed.
|
|
438
453
|
- `ResourceLink` (type) — A handle to something a tool call made or found — a document it wrote, a file it saved.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.21.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",
|