@cubicecho/agent-core 4.12.0 → 4.14.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 +71 -10
- package/dist/core/config.d.ts +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/run/agent-loop.d.ts +28 -9
- package/dist/run/agent-loop.js +19 -12
- package/dist/spec/columns.d.ts +162 -0
- package/dist/spec/columns.js +275 -0
- package/llms.txt +14 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ only, Node >=22.
|
|
|
44
44
|
| `compaction` | Keeping a long run inside its window: `pruneToolResults` clears stale tool results, `planCompaction` and `compactTranscript` fold the oldest stretch into a summary. |
|
|
45
45
|
| `ledger` | What stretches of a transcript cost, read off the prompt counts the server reported rather than estimated: `recordRequest`, `tokensBetween`, `estimateFrom`, `rebaseLedger`. |
|
|
46
46
|
| `snapshot` | `exportCapabilities` and `importCapabilities`: the latched refusals as a JSON blob a consumer stores, so a restart need not learn them again. |
|
|
47
|
-
| `spec` | `parseSpec`, `resolveAgentSpec` and `exportSpec`: an agent as a versioned JSON document, layered into the flat config the loop takes. Imports nothing but types, and is published separately at `@cubicecho/agent-core/spec`. |
|
|
47
|
+
| `spec` | `parseSpec`, `resolveAgentSpec` and `exportSpec`: an agent as a versioned JSON document, layered into the flat config the loop takes. Imports nothing but types, and is published separately at `@cubicecho/agent-core/spec`. `defineSpecColumns` maps a stored row onto a layer and back, at `@cubicecho/agent-core/spec/columns`. |
|
|
48
48
|
| `reset` | `resetAll`: drops every cache and latch in one call, so a teardown cannot forget one. |
|
|
49
49
|
| `runtime` | `createRuntime`: a second set of those caches, with the functions that use them as methods — for a host whose tenants must not share a client pool, a latch or an event bus. `defaultRuntime` is the one the top-level functions use. |
|
|
50
50
|
| `tokens` | How big a request is, estimated: `estimateTokens` — characters over four, deliberately low — and what is built on it for everything here that has to guess at a window: `messageTokens`, `requestTokens`, `contextTokens`. |
|
|
@@ -768,8 +768,8 @@ distinctive than a word the catalogue shares has to have matched, and the tools
|
|
|
768
768
|
have to score well below the ones it kept — a hit just under the line scoring nearly as much as
|
|
769
769
|
one just over it means the ranking chose arbitrarily, which is the case a model is worth spending
|
|
770
770
|
on. Matching nothing is not confident either, since the words cannot tell a request that needs no
|
|
771
|
-
tools from one whose words are not in the catalogue. An empty
|
|
772
|
-
|
|
771
|
+
tools from one whose words are not in the catalogue. An empty model still means no preselection
|
|
772
|
+
at all, words included. `preselect` also takes a `temperature` and a
|
|
773
773
|
`reasoningEffort` and an `extraBody`, read as `ask` reads them, and `runAgentLoop({ preselect: true })` makes the call
|
|
774
774
|
itself — see "Its side tasks" below.
|
|
775
775
|
|
|
@@ -884,10 +884,12 @@ as the host passed them, this one as typed — which is why the snippet stores o
|
|
|
884
884
|
without waiting; a host that stores those notes with the turn, or would otherwise exit before the
|
|
885
885
|
turn is remembered, awaits it once the answer is out.
|
|
886
886
|
|
|
887
|
-
The loop gathers before its first request,
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
(see [Hooks](#hooks)).
|
|
887
|
+
The loop gathers before its first request, beside its own preselection when `preselect` asked for
|
|
888
|
+
one, and nothing else of the host's can run beside those. A host with pre-turn work to overlap with
|
|
889
|
+
the hooks — a compaction — calls `gather` itself, applies `withContext` to the current question as
|
|
890
|
+
well, calls `notify` after, and passes no `hooks` (see [Hooks](#hooks)). Such a host passes the
|
|
891
|
+
question as it was typed as `preselectQuery` too, or the preselector is shown the context in front
|
|
892
|
+
of it and picks tools for what was recalled rather than what was asked.
|
|
891
893
|
|
|
892
894
|
A host that stores its transcript — a row per message, so a crash or a stop mid-run still leaves
|
|
893
895
|
readable history — gets each message as the loop appends it, and the run as it stood when the loop
|
|
@@ -1400,6 +1402,50 @@ a form can load it. Nothing in `src/` imports it — the same direction the conf
|
|
|
1400
1402
|
what lets a document travel between hosts without the loop growing an opinion about where an agent
|
|
1401
1403
|
comes from.
|
|
1402
1404
|
|
|
1405
|
+
### From a row, and back
|
|
1406
|
+
|
|
1407
|
+
A document has one spelling of "inherit" — the field is absent — and a database row cannot leave a
|
|
1408
|
+
column out, so every host stores a marker instead: `""` for a text column, `-1` where zero is a real
|
|
1409
|
+
answer, the word `inherit` in an enum, `null` in a nullable one. `defineSpecColumns` is the table
|
|
1410
|
+
from those columns to the fields they feed, with each column's marker a parameter, so the projection
|
|
1411
|
+
is declared once rather than written out in both directions:
|
|
1412
|
+
|
|
1413
|
+
```ts
|
|
1414
|
+
import { defineSpecColumns } from "@cubicecho/agent-core/spec/columns";
|
|
1415
|
+
|
|
1416
|
+
const profile = defineSpecColumns<Agent>(
|
|
1417
|
+
{
|
|
1418
|
+
model: { path: "model.model", inherit: "" },
|
|
1419
|
+
maxTokens: { path: "model.maxTokens", inherit: -1 },
|
|
1420
|
+
temperature: { path: "model.temperature", inherit: null },
|
|
1421
|
+
toolDiscovery: { path: "tools.discovery", inherit: "inherit" },
|
|
1422
|
+
systemPrompt: { path: "prompt.system", inherit: "" },
|
|
1423
|
+
compactionModel: { path: "tasks.compaction.model", inherit: "" },
|
|
1424
|
+
toolExecution: { path: ["extensions", "com.example.host", "toolExecution"], inherit: "inherit" },
|
|
1425
|
+
},
|
|
1426
|
+
{ handled: ["bundle", "tools.servers"] },
|
|
1427
|
+
);
|
|
1428
|
+
|
|
1429
|
+
resolveAgentSpec([settings.toLayer(settingsRow), profile.toLayer(agentRow)]); // markers say nothing
|
|
1430
|
+
const row = profile.fromLayer(spec); // an absent field comes back as its column's marker
|
|
1431
|
+
const lost = profile.dropped(spec); // [{ path: "model.extraBody", value: {...} }, ...]
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
The table is keyed by column, so a column the row does not have, a marker of the wrong type and a
|
|
1435
|
+
path the format has no field at are all type errors. A column with no `inherit` says every value it
|
|
1436
|
+
holds, which is a host's bottom layer; `null` and `undefined` are absent whatever the marker. A
|
|
1437
|
+
column that stores something other than its field — a switch for a discovery mode — converts with
|
|
1438
|
+
`toSpec` and `fromSpec`, and either returning `undefined` says nothing: that is how a marker that
|
|
1439
|
+
is a range (a trimmed blank, any negative) is spelled, and how a column refuses a value it cannot
|
|
1440
|
+
hold. `prompt.<id>` is one part's text; an extension's reverse-domain key has dots of its own, so
|
|
1441
|
+
its path is written as segments.
|
|
1442
|
+
|
|
1443
|
+
`dropped` is the import preview's list: what a document says that `fromLayer` will not keep, in the
|
|
1444
|
+
document's order, and never silently. A field with no column is reported at its path, a task or
|
|
1445
|
+
extension with nothing mapped in it whole, and a value a column's `fromSpec` refused carries that
|
|
1446
|
+
column's name. What the host reads by hand — a bundle, `tools.servers` resolved to its own row ids —
|
|
1447
|
+
goes in `handled` and is left out.
|
|
1448
|
+
|
|
1403
1449
|
### Its side tasks
|
|
1404
1450
|
|
|
1405
1451
|
`tasks.<key>` configures a side task apart from the main model — its own model, endpoint, ceiling,
|
|
@@ -1444,16 +1490,31 @@ with `tasks.compaction` writing the summary and no falling back to the main mode
|
|
|
1444
1490
|
are off by default: a host that already calls `preselect` or compacts in `beforeStep` is not sent
|
|
1445
1491
|
a second request.
|
|
1446
1492
|
|
|
1493
|
+
The preselector falls back to the main model. With neither `tasks.toolSelect` nor
|
|
1494
|
+
`toolSelectModel` naming one, `preselect: true` asks `config.model` on the agent's own endpoint at
|
|
1495
|
+
`preselect`'s defaults: a host that opted in has asked for a preselection, and an empty name is a
|
|
1496
|
+
setting nobody filled in. Compaction has no such fallback. A host that wants no preselection passes
|
|
1497
|
+
no `preselect`. A spec layer that declines the task with `model: ""` leaves it absent from the
|
|
1498
|
+
resolved agent, as one nobody configured is, so under `preselect: true` the main model is asked
|
|
1499
|
+
there too.
|
|
1500
|
+
|
|
1501
|
+
What it asks about is the question as written: the last user message of `messages`, which the hooks'
|
|
1502
|
+
context never reaches because the loop puts that on the request. A host that put context on the
|
|
1503
|
+
question itself passes `preselectQuery`. The preselection and the hooks' `gather` run side by side,
|
|
1504
|
+
since neither reads the other, so asking for both costs the first request one round trip rather
|
|
1505
|
+
than two.
|
|
1506
|
+
|
|
1447
1507
|
What the loop picked it says twice. A `notice` event names the tools before the first `turn` event
|
|
1448
1508
|
— `PRESELECTED_NOTICE` and then the names, comma-separated — and the result's `preselected` is the
|
|
1449
1509
|
shortlist the run opened with, the host's own where it gave one and empty where nothing was picked.
|
|
1450
1510
|
A run that fails carries it too, beside `loaded` and `used`.
|
|
1451
1511
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1512
|
+
Called directly, `preselect` reads an empty model as no preselection and says nothing about it; it
|
|
1513
|
+
is only the loop that falls back. A host calling it itself whose settings read `""` as "use the
|
|
1514
|
+
run's model" says so in the call:
|
|
1454
1515
|
|
|
1455
1516
|
```ts
|
|
1456
|
-
|
|
1517
|
+
await preselect(config, settings.toolSelectModel || settings.model, catalog, prompt, options);
|
|
1457
1518
|
```
|
|
1458
1519
|
|
|
1459
1520
|
```ts
|
package/dist/core/config.d.ts
CHANGED
|
@@ -148,7 +148,10 @@ export type ToolDiscovery = (typeof ToolDiscovery)[keyof typeof ToolDiscovery];
|
|
|
148
148
|
export interface ToolPolicy {
|
|
149
149
|
/** How tool definitions reach the model. */
|
|
150
150
|
toolDiscovery: ToolDiscovery;
|
|
151
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The model that does the preselection pass. Empty means don't preselect, to `preselect` itself;
|
|
153
|
+
* `runAgentLoop`, asked to preselect, reads empty as the run's own `model`.
|
|
154
|
+
*/
|
|
152
155
|
toolSelectModel: string;
|
|
153
156
|
/** Hard stop on runaway tool loops. */
|
|
154
157
|
maxToolIterations: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { preview, type ToolDispatch } from './run/run-calls.ts';
|
|
|
26
26
|
export { resetAll } from './runtime/reset.ts';
|
|
27
27
|
export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from './runtime/runtime.ts';
|
|
28
28
|
export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from './runtime/snapshot.ts';
|
|
29
|
+
export { defineSpecColumns, type ExtensionPath, type SpecColumn, type SpecColumnMapping, type SpecColumns, type SpecColumnsOptions, type SpecDrop, type SpecPath, type SpecSection, } from './spec/columns.ts';
|
|
29
30
|
export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, type AgentHook, type AgentSpec, type EndpointSpec, type ExportSpecOptions, exportSpec, findSpecSecrets, type ModelSpec, type ParsedSpec, type ParseSpecOptions, type PromptPart, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, type SecretHint, SecretSign, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from './spec/spec.ts';
|
|
30
31
|
export type { CatalogServer } from './tools/catalog.ts';
|
|
31
32
|
export { SERVER_INSTRUCTION_TOKENS, type ServerInstructionsOptions, serverInstructionsPrompt, } from './tools/instructions.ts';
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export { preview } from './run/run-calls.js';
|
|
|
26
26
|
export { resetAll } from './runtime/reset.js';
|
|
27
27
|
export { createRuntime, defaultRuntime } from './runtime/runtime.js';
|
|
28
28
|
export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from './runtime/snapshot.js';
|
|
29
|
+
export { defineSpecColumns, } from './spec/columns.js';
|
|
29
30
|
export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, findSpecSecrets, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, resolveAgentSpec, SecretSign, SPEC_EVENTS, } from './spec/spec.js';
|
|
30
31
|
export { SERVER_INSTRUCTION_TOKENS, serverInstructionsPrompt, } from './tools/instructions.js';
|
|
31
32
|
export { PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, } from './tools/preselect.js';
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -43,9 +43,10 @@ export interface AgentLoopRequest {
|
|
|
43
43
|
* The hooks a loop runs around one question. See `hooks.ts`.
|
|
44
44
|
*
|
|
45
45
|
* @remarks
|
|
46
|
-
* The loop gathers before its first request
|
|
47
|
-
* pre-turn work of its own to overlap with
|
|
48
|
-
* itself, puts the context on with `withContext`, calls
|
|
46
|
+
* The loop gathers before its first request, beside its own preselection where it was asked for
|
|
47
|
+
* one, and nothing else runs beside the two. A host with pre-turn work of its own to overlap with
|
|
48
|
+
* the hooks — a compaction — calls `gather` itself, puts the context on with `withContext`, calls
|
|
49
|
+
* `notify` after, passes no `hooks`, and hands `preselectQuery` the question as it was written.
|
|
49
50
|
*/
|
|
50
51
|
export interface AgentLoopHooks {
|
|
51
52
|
run: HookRunner;
|
|
@@ -109,7 +110,8 @@ export interface AgentLoopOptions {
|
|
|
109
110
|
* @remarks
|
|
110
111
|
* `tasks` is a resolved agent's, and is read only for the side tasks the loop was asked to run:
|
|
111
112
|
* `tasks.toolSelect` under `preselect` and `tasks.compaction` under `compact`. `toolSelectModel`
|
|
112
|
-
* is the flattened spelling of the first, for a host that is not on the spec.
|
|
113
|
+
* is the flattened spelling of the first, for a host that is not on the spec. With neither
|
|
114
|
+
* naming a model, `preselect` asks `model` itself; `compact` never does.
|
|
113
115
|
*/
|
|
114
116
|
config: Endpoint & ModelParams & Pick<ToolPolicy, 'maxToolIterations'> & Partial<Pick<ToolPolicy, 'toolDiscovery' | 'toolSelectModel'>> & Partial<RetryPolicy> & {
|
|
115
117
|
contextLength?: number;
|
|
@@ -200,20 +202,37 @@ export interface AgentLoopOptions {
|
|
|
200
202
|
* The preselector is `config.tasks.toolSelect` with everything it states — its endpoint, model,
|
|
201
203
|
* ceiling, temperature and reasoning effort, keyed as `taskCall` keys a task against `config` —
|
|
202
204
|
* and otherwise `config.toolSelectModel` on the config's own endpoint at `preselect`'s defaults.
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
205
|
+
* With neither naming a model it is `config.model` itself, on the same terms as
|
|
206
|
+
* `toolSelectModel`: a host that opted in has asked for a preselection, and an empty name is a
|
|
207
|
+
* setting nobody filled in rather than a refusal. A host that wants none passes no `preselect`.
|
|
208
|
+
*
|
|
209
|
+
* What it is asked about is `preselectQuery`, and otherwise the text of the last user message as
|
|
210
|
+
* the host gave it — the hooks' context is put on the request, never on `messages`, so the
|
|
211
|
+
* tools are picked on what was asked rather than on what was recalled. Nothing is asked in eager
|
|
212
|
+
* mode, with no question, or when `preselected` is given — an empty one included, which is how
|
|
213
|
+
* a host says it has already decided.
|
|
206
214
|
*
|
|
207
215
|
* `true` takes `preselect`'s defaults; an object passes `keywords` and `maxPerLoad` through.
|
|
208
216
|
* Its notices arrive as `notice` events, and a preselection that fails picks nothing rather
|
|
209
217
|
* than ending the run. What it picked is a notice too, `PRESELECTED_NOTICE` and the names,
|
|
210
|
-
* before the first `turn` event, and is the result's `preselected`. It runs
|
|
211
|
-
* gather,
|
|
218
|
+
* before the first `turn` event, and is the result's `preselected`. It runs beside the hooks'
|
|
219
|
+
* `gather` rather than before it, since neither reads what the other returns, and the first
|
|
220
|
+
* request waits for both.
|
|
212
221
|
*/
|
|
213
222
|
preselect?: boolean | {
|
|
214
223
|
keywords?: boolean | KeywordPreselectOptions;
|
|
215
224
|
maxPerLoad?: number;
|
|
216
225
|
};
|
|
226
|
+
/**
|
|
227
|
+
* What the loop's own preselection is asked about, in place of the last user message's text.
|
|
228
|
+
* Read only under `preselect`.
|
|
229
|
+
*
|
|
230
|
+
* @remarks
|
|
231
|
+
* For a host that gathered its hooks itself and sent the question with `withContext` already on
|
|
232
|
+
* it: the loop cannot tell the context from the question, and a preselector shown both picks
|
|
233
|
+
* tools for whatever memory recalled. Blank asks nothing, as a blank question does.
|
|
234
|
+
*/
|
|
235
|
+
preselectQuery?: string;
|
|
217
236
|
/**
|
|
218
237
|
* Has the loop fold the transcript itself before a step, once `planCompaction` says the window
|
|
219
238
|
* is filling, with the summary written by `config.tasks.compaction` on its own settings. Off by
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -218,17 +218,18 @@ const userText = (message) => {
|
|
|
218
218
|
* name on its own endpoint where it does not.
|
|
219
219
|
*
|
|
220
220
|
* @param options - The run's options. Read for the config, the catalogue, the signal, what
|
|
221
|
-
* `preselect` passes through, and the last user message, whose text is
|
|
221
|
+
* `preselect` passes through, and `preselectQuery` or else the last user message, whose text is
|
|
222
|
+
* what is asked about.
|
|
222
223
|
* @param notice - Hears the preselector's notices, a failure among them.
|
|
223
|
-
* @returns The names picked. Empty where there is no user message
|
|
224
|
+
* @returns The names picked. Empty where the query is blank or there is no user message — nothing
|
|
224
225
|
* is asked — and where the preselection picked nothing or failed. An aborted request still rejects.
|
|
225
226
|
*
|
|
226
227
|
* @remarks
|
|
227
|
-
*
|
|
228
|
+
* With no preselector named the main model is asked, on the config's own endpoint.
|
|
228
229
|
*/
|
|
229
230
|
async function chooseTools(options, notice) {
|
|
230
231
|
const { config, catalog = [], signal } = options;
|
|
231
|
-
const prompt = userText(options.messages.findLast((message) => message.role === Role.User));
|
|
232
|
+
const prompt = options.preselectQuery ?? userText(options.messages.findLast((message) => message.role === Role.User));
|
|
232
233
|
if (!prompt.trim()) {
|
|
233
234
|
return [];
|
|
234
235
|
}
|
|
@@ -239,7 +240,7 @@ async function chooseTools(options, notice) {
|
|
|
239
240
|
};
|
|
240
241
|
const selector = config.tasks?.toolSelect;
|
|
241
242
|
if (!selector?.model) {
|
|
242
|
-
return preselect(config, config.toolSelectModel
|
|
243
|
+
return preselect(config, config.toolSelectModel || config.model, catalog, prompt, asked);
|
|
243
244
|
}
|
|
244
245
|
const call = taskCall(selector, asked, config);
|
|
245
246
|
return preselect(call.endpoint, call.model, catalog, prompt, call.options);
|
|
@@ -494,6 +495,18 @@ async function runSteps(options, standing, pin) {
|
|
|
494
495
|
// The host's own fields win over the pin's, so an explicit `cache_prompt: false` stays false.
|
|
495
496
|
const extraBody = pin ? { ...pin.fields, ...config.extraBody } : config.extraBody;
|
|
496
497
|
const sending = { ...config, extraBody };
|
|
498
|
+
// Started before the preselection rather than after it: neither reads the other, and in a row
|
|
499
|
+
// they cost the first request two round trips where one will do. A failing runner is a note, but
|
|
500
|
+
// a host's `onNote` that throws rejects the gather, perhaps while the preselection is still out:
|
|
501
|
+
// handled at once so that is no unhandled rejection, and still thrown below, where it always was.
|
|
502
|
+
const gathering = hooks
|
|
503
|
+
? gather(hooks.run, hooks.events ?? [HookEvent.BeforeTurn], hooks.context, {
|
|
504
|
+
signal,
|
|
505
|
+
onNote: hooks.onNote,
|
|
506
|
+
maxTokens: hooks.maxTokens,
|
|
507
|
+
})
|
|
508
|
+
: Promise.resolve({ context: '', notes: [] });
|
|
509
|
+
gathering.catch(() => undefined);
|
|
497
510
|
const { proxied, onDemand, loaded, preselected } = await discovery(options, notice);
|
|
498
511
|
const instructions = serverInstructions === false
|
|
499
512
|
? ''
|
|
@@ -531,13 +544,7 @@ async function runSteps(options, standing, pin) {
|
|
|
531
544
|
// hooks' context with it, which is right.
|
|
532
545
|
const question = messages.findLast((message) => message.role === Role.User);
|
|
533
546
|
const questionAt = () => (question ? messages.indexOf(question) : -1);
|
|
534
|
-
const gathered =
|
|
535
|
-
? await gather(hooks.run, hooks.events ?? [HookEvent.BeforeTurn], hooks.context, {
|
|
536
|
-
signal,
|
|
537
|
-
onNote: hooks.onNote,
|
|
538
|
-
maxTokens: hooks.maxTokens,
|
|
539
|
-
})
|
|
540
|
-
: { context: '', notes: [] };
|
|
547
|
+
const gathered = await gathering;
|
|
541
548
|
// Read once, so what the result hands back is what every step of this run said.
|
|
542
549
|
const preface = hooks?.preface ?? configureHooks().preface;
|
|
543
550
|
const loads = { toolsLoaded: 0, redundantLoads: 0, unknownToolNames: 0 };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { type AgentSpec, type EndpointSpec, type ModelSpec, type RetrySpec, type TaskSpec, type ToolsSpec } from './spec.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A host's stored agent, read as a spec layer and written back: one table of columns, and the
|
|
4
|
+
* marker each column stores for "inherit".
|
|
5
|
+
*
|
|
6
|
+
* Every host with profiles had written this projection by hand, three times with three markers —
|
|
7
|
+
* `""` and `-1` and the word `inherit` in one, `null` in the next, none at all in the third — and
|
|
8
|
+
* the import half of it once more, with a list of what a row could not keep. The merge was already
|
|
9
|
+
* `resolveAgentSpec`; what each repeated was which column feeds which field, and how a column at
|
|
10
|
+
* its marker becomes a field the layer does not have, which is the only spelling of "inherit" a
|
|
11
|
+
* spec knows.
|
|
12
|
+
*
|
|
13
|
+
* Browser-safe for the reason `spec.ts` is: it imports that module and a guard, and nothing that
|
|
14
|
+
* needs Node, so a form can preview an import with it.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Where in a spec a column lives, as a dotted path. A path the format has no field for does not
|
|
18
|
+
* typecheck.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* `prompt.<id>` is the text of the prompt part with that id, since a row holds a prompt as one
|
|
22
|
+
* string and a spec holds it as named layers. A task name is everything between `tasks.` and the
|
|
23
|
+
* next dot, so a task whose name has a dot in it cannot be addressed — nor can an extension's
|
|
24
|
+
* reverse-domain key, which is what `ExtensionPath` is for.
|
|
25
|
+
*/
|
|
26
|
+
export type SpecPath = 'id' | 'name' | 'description' | `endpoint.${keyof EndpointSpec}` | `model.${keyof ModelSpec}` | `prompt.${string}` | `tools.${keyof ToolsSpec}` | `retry.${keyof RetrySpec}` | `tasks.${string}.endpoint.${keyof EndpointSpec}` | `tasks.${string}.${Exclude<keyof TaskSpec, 'endpoint'>}`;
|
|
27
|
+
/**
|
|
28
|
+
* A path into a host's own extension, as segments, because its key is a reverse domain and has
|
|
29
|
+
* dots of its own.
|
|
30
|
+
*/
|
|
31
|
+
export type ExtensionPath = readonly ['extensions', string, ...string[]];
|
|
32
|
+
/** The top-level keys of a spec, for naming a whole section a host reads by hand. */
|
|
33
|
+
export type SpecSection = '$schema' | 'id' | 'name' | 'description' | 'endpoint' | 'model' | 'prompt' | 'tools' | 'retry' | 'tasks' | 'hooks' | 'bundle' | 'extensions' | 'requires';
|
|
34
|
+
/**
|
|
35
|
+
* One column: where its value lives in a spec, what it stores for "inherit", and how its value
|
|
36
|
+
* crosses when the column and the field do not hold the same thing.
|
|
37
|
+
*
|
|
38
|
+
* @typeParam Value - The column's own type, which `inherit` and both conversions are checked
|
|
39
|
+
* against.
|
|
40
|
+
*/
|
|
41
|
+
export interface SpecColumn<Value> {
|
|
42
|
+
path: SpecPath | ExtensionPath;
|
|
43
|
+
/**
|
|
44
|
+
* What the column stores for "say nothing here": a value at it is a field the layer leaves out,
|
|
45
|
+
* and a field the layer leaves out reads back as it. Compared with `===`. Left out, the column
|
|
46
|
+
* has no marker — every value it holds is said, which is a host's bottom layer — and an absent
|
|
47
|
+
* field leaves the column out of what `fromLayer` returns.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* `null` and `undefined` are absent whatever the marker is, as they are to `parseSpec`, so a
|
|
51
|
+
* nullable column needs no marker to inherit and `inherit: null` only adds the read-back.
|
|
52
|
+
*/
|
|
53
|
+
inherit?: Value;
|
|
54
|
+
/**
|
|
55
|
+
* The column's value as the field holds it, for a column that stores something else — a switch
|
|
56
|
+
* for a mode, say. Called for a value that is neither absent nor the marker, and `undefined`
|
|
57
|
+
* back says nothing, which is how a marker that is a range rather than one value (a trimmed
|
|
58
|
+
* blank, any negative) is spelled. Left out, the value crosses as it is.
|
|
59
|
+
*/
|
|
60
|
+
toSpec?: (value: Exclude<Value, null | undefined>) => unknown;
|
|
61
|
+
/**
|
|
62
|
+
* The field's value as the column would store it, or `undefined` where the column cannot hold
|
|
63
|
+
* it — which `fromLayer` writes as the marker and `dropped` reports against this column. Left
|
|
64
|
+
* out, the value crosses as `parseSpec` left it, and the path is what says its type.
|
|
65
|
+
*/
|
|
66
|
+
fromSpec?: (value: unknown) => Value | undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A row's columns, each mapped to a field: keyed by column, so a name the row does not have is an
|
|
70
|
+
* excess property and a marker of the wrong type does not typecheck.
|
|
71
|
+
*
|
|
72
|
+
* @typeParam Row - The stored row. Only the columns named here are read or written.
|
|
73
|
+
*/
|
|
74
|
+
export type SpecColumns<Row> = {
|
|
75
|
+
[Column in keyof Row]?: SpecColumn<Row[Column]>;
|
|
76
|
+
};
|
|
77
|
+
/** What `defineSpecColumns` takes besides the columns. */
|
|
78
|
+
export interface SpecColumnsOptions {
|
|
79
|
+
/**
|
|
80
|
+
* What the host reads by hand, so `dropped` does not report it: a section (`bundle`), a path
|
|
81
|
+
* (`tools.servers`, where a list of slugs becomes a list of row ids) or an extension. `spec` is
|
|
82
|
+
* never reported, and needs no entry here.
|
|
83
|
+
*/
|
|
84
|
+
handled?: readonly (SpecSection | SpecPath | `tasks.${string}` | ExtensionPath)[];
|
|
85
|
+
}
|
|
86
|
+
/** One thing a document says that a row will not keep. */
|
|
87
|
+
export interface SpecDrop<Row> {
|
|
88
|
+
/** Where it is, in `parseSpec`'s spelling: `model.extraBody`, `prompt[1]`, `hooks[0]`. */
|
|
89
|
+
path: string;
|
|
90
|
+
/** What the document held there. */
|
|
91
|
+
value: unknown;
|
|
92
|
+
/**
|
|
93
|
+
* The column that refused it: present when there is a column for the field and its `fromSpec`
|
|
94
|
+
* could not store this value, absent when there is no column for the field at all.
|
|
95
|
+
*/
|
|
96
|
+
column?: keyof Row;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A row's column table, both ways.
|
|
100
|
+
*
|
|
101
|
+
* @typeParam Row - The stored row.
|
|
102
|
+
*/
|
|
103
|
+
export interface SpecColumnMapping<Row> {
|
|
104
|
+
/**
|
|
105
|
+
* The row as a spec layer, saying only what the row says.
|
|
106
|
+
*
|
|
107
|
+
* @param row - The row, or any part of it: a column the row does not carry says nothing.
|
|
108
|
+
* @returns A layer holding every column that is neither absent nor at its marker. A section
|
|
109
|
+
* nothing was said in is left out, and the layer is not yet parsed.
|
|
110
|
+
*/
|
|
111
|
+
toLayer(row: Partial<Row>): AgentSpec;
|
|
112
|
+
/**
|
|
113
|
+
* A spec layer as the row would store it.
|
|
114
|
+
*
|
|
115
|
+
* @param layer - A layer `parseSpec` has read, so each field is of its path's type.
|
|
116
|
+
* @returns Every mapped column: its field's value, or its marker where the field is absent or
|
|
117
|
+
* `fromSpec` refused it. A column with no marker is left out where its field is absent.
|
|
118
|
+
*/
|
|
119
|
+
fromLayer(layer: AgentSpec): Partial<Row>;
|
|
120
|
+
/**
|
|
121
|
+
* What a spec says that `fromLayer` would not keep: a field with no column, and a value a column
|
|
122
|
+
* refused.
|
|
123
|
+
*
|
|
124
|
+
* @param spec - A layer `parseSpec` has read.
|
|
125
|
+
* @returns Each loss once, in the document's order. Empty when the row keeps everything.
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* A field this format names is reported at its own path, a section with nothing mapped in it
|
|
129
|
+
* whole beneath that: an `extraBody` no column takes is one entry, not one per key. A prompt
|
|
130
|
+
* part is a column's when its id is, and its `ref` is reported, since a column holds a text.
|
|
131
|
+
*/
|
|
132
|
+
dropped(spec: AgentSpec): SpecDrop<Row>[];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A column table from a stored row to a spec layer and back, with each column's "inherit" marker
|
|
136
|
+
* a parameter.
|
|
137
|
+
*
|
|
138
|
+
* @typeParam Row - The stored row: a settings row, a profile, an override layer.
|
|
139
|
+
* @param columns - Each mapped column, keyed by its name on the row.
|
|
140
|
+
* @param [options] - What the host reads by hand, outside the table.
|
|
141
|
+
* @returns `toLayer`, `fromLayer` and `dropped` over that one table.
|
|
142
|
+
* @throws When two columns write the same field, or one writes a section another writes inside.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const profile = defineSpecColumns<Agent>({
|
|
147
|
+
* model: { path: 'model.model', inherit: '' },
|
|
148
|
+
* maxTokens: { path: 'model.maxTokens', inherit: -1 },
|
|
149
|
+
* toolDiscovery: { path: 'tools.discovery', inherit: 'inherit' },
|
|
150
|
+
* compactionModel: { path: 'tasks.compaction.model', inherit: '' },
|
|
151
|
+
* toolExecution: { path: ['extensions', 'com.example.host', 'toolExecution'], inherit: 'inherit' },
|
|
152
|
+
* });
|
|
153
|
+
* resolveAgentSpec([settings.toLayer(row), profile.toLayer(agent)]);
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @remarks
|
|
157
|
+
* Keyed by column rather than by field, so the marker and the conversions are typed against that
|
|
158
|
+
* column's own type and a column cannot be mapped twice. A column feeds one field: a row that
|
|
159
|
+
* spreads one field over several columns, or a record column over many fields, reads that part by
|
|
160
|
+
* hand and names it in `handled`.
|
|
161
|
+
*/
|
|
162
|
+
export declare function defineSpecColumns<Row>(columns: SpecColumns<Row>, { handled }?: SpecColumnsOptions): SpecColumnMapping<Row>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { isRecord } from '../core/guards.js';
|
|
2
|
+
import { AGENT_SPEC, } from './spec.js';
|
|
3
|
+
/**
|
|
4
|
+
* The sections `dropped` always walks into, so a field the format names is reported at its own
|
|
5
|
+
* path. Anything deeper — one task, one extension — is walked into only where a column is mapped
|
|
6
|
+
* beneath it, and is otherwise reported whole: a side task this host does not run is one loss.
|
|
7
|
+
*/
|
|
8
|
+
const STRUCTURAL = ['endpoint', 'model', 'tools', 'retry', 'tasks', 'extensions'];
|
|
9
|
+
/** The section whose entries are named layers of text, addressed by `prompt.<id>`. */
|
|
10
|
+
const PROMPT = 'prompt';
|
|
11
|
+
/** The section whose entries are hooks, which `dropped` reports one by one. */
|
|
12
|
+
const HOOKS = 'hooks';
|
|
13
|
+
/** The key every layer declares itself under, which belongs to no column. */
|
|
14
|
+
const TOKEN = 'spec';
|
|
15
|
+
/**
|
|
16
|
+
* Absent and null are the same thing, as they are to `parseSpec`.
|
|
17
|
+
*
|
|
18
|
+
* @param value - A column's or a field's value.
|
|
19
|
+
* @returns `true` for `undefined` and `null` only.
|
|
20
|
+
*/
|
|
21
|
+
const absent = (value) => value === undefined || value === null;
|
|
22
|
+
/**
|
|
23
|
+
* A path as segments.
|
|
24
|
+
*
|
|
25
|
+
* @param path - A dotted path or an extension's segments.
|
|
26
|
+
* @returns The segments: a prompt id is one segment whatever it holds, and a task name the text up
|
|
27
|
+
* to the next dot.
|
|
28
|
+
*/
|
|
29
|
+
function segmentsOf(path) {
|
|
30
|
+
if (typeof path !== 'string') {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
const prefix = `${PROMPT}.`;
|
|
34
|
+
if (path.startsWith(prefix)) {
|
|
35
|
+
return [PROMPT, path.slice(prefix.length)];
|
|
36
|
+
}
|
|
37
|
+
return path.split('.');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether one path is the other or lies beneath it.
|
|
41
|
+
*
|
|
42
|
+
* @param prefix - The shorter path.
|
|
43
|
+
* @param path - The path that may lie beneath it.
|
|
44
|
+
* @returns `true` when every segment of `prefix` begins `path`.
|
|
45
|
+
*/
|
|
46
|
+
const startsWith = (prefix, path) => prefix.length <= path.length && prefix.every((segment, index) => segment === path[index]);
|
|
47
|
+
/**
|
|
48
|
+
* Whether two paths are the same segments.
|
|
49
|
+
*
|
|
50
|
+
* @param a - One path.
|
|
51
|
+
* @param b - The other.
|
|
52
|
+
* @returns `true` when they are the same length and agree throughout.
|
|
53
|
+
*/
|
|
54
|
+
const samePath = (a, b) => a.length === b.length && startsWith(a, b);
|
|
55
|
+
/**
|
|
56
|
+
* The value at a path in a layer.
|
|
57
|
+
*
|
|
58
|
+
* @param layer - The layer.
|
|
59
|
+
* @param segments - Where to look. `['prompt', id]` reads that part's text.
|
|
60
|
+
* @returns What is there, or `undefined`.
|
|
61
|
+
*/
|
|
62
|
+
function read(layer, segments) {
|
|
63
|
+
if (segments[0] === PROMPT) {
|
|
64
|
+
return layer.prompt?.find((part) => part.id === segments[1])?.text;
|
|
65
|
+
}
|
|
66
|
+
let held = layer;
|
|
67
|
+
for (const segment of segments) {
|
|
68
|
+
if (isRecord(held) === false) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
held = held[segment];
|
|
72
|
+
}
|
|
73
|
+
return held;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Puts a value at a path in a layer being built, making the sections on the way.
|
|
77
|
+
*
|
|
78
|
+
* @param layer - The layer, as a plain object.
|
|
79
|
+
* @param segments - Where it goes. `['prompt', id]` appends a part.
|
|
80
|
+
* @param value - What goes there.
|
|
81
|
+
*/
|
|
82
|
+
function write(layer, segments, value) {
|
|
83
|
+
if (segments[0] === PROMPT) {
|
|
84
|
+
const parts = Array.isArray(layer.prompt) ? layer.prompt : [];
|
|
85
|
+
layer.prompt = [...parts, { id: segments[1], text: value }];
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let section = layer;
|
|
89
|
+
for (const segment of segments.slice(0, -1)) {
|
|
90
|
+
const next = section[segment];
|
|
91
|
+
section[segment] = isRecord(next) ? next : {};
|
|
92
|
+
section = section[segment];
|
|
93
|
+
}
|
|
94
|
+
section[segments[segments.length - 1]] = value;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Refuses a table that cannot be read both ways.
|
|
98
|
+
*
|
|
99
|
+
* @param entries - The table's columns.
|
|
100
|
+
* @throws When a path has an empty segment, or one column's path is another's or lies beneath it —
|
|
101
|
+
* two columns writing one field, or one writing a section another writes inside.
|
|
102
|
+
*/
|
|
103
|
+
function checkTable(entries) {
|
|
104
|
+
for (const [index, entry] of entries.entries()) {
|
|
105
|
+
if (entry.segments.some((segment) => segment === '')) {
|
|
106
|
+
throw new Error(`defineSpecColumns: ${String(entry.column)} has an empty segment in its path`);
|
|
107
|
+
}
|
|
108
|
+
for (const other of entries.slice(index + 1)) {
|
|
109
|
+
if (startsWith(entry.segments, other.segments) || startsWith(other.segments, entry.segments)) {
|
|
110
|
+
throw new Error(`defineSpecColumns: ${String(entry.column)} and ${String(other.column)} write the same field`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A column table from a stored row to a spec layer and back, with each column's "inherit" marker
|
|
117
|
+
* a parameter.
|
|
118
|
+
*
|
|
119
|
+
* @typeParam Row - The stored row: a settings row, a profile, an override layer.
|
|
120
|
+
* @param columns - Each mapped column, keyed by its name on the row.
|
|
121
|
+
* @param [options] - What the host reads by hand, outside the table.
|
|
122
|
+
* @returns `toLayer`, `fromLayer` and `dropped` over that one table.
|
|
123
|
+
* @throws When two columns write the same field, or one writes a section another writes inside.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const profile = defineSpecColumns<Agent>({
|
|
128
|
+
* model: { path: 'model.model', inherit: '' },
|
|
129
|
+
* maxTokens: { path: 'model.maxTokens', inherit: -1 },
|
|
130
|
+
* toolDiscovery: { path: 'tools.discovery', inherit: 'inherit' },
|
|
131
|
+
* compactionModel: { path: 'tasks.compaction.model', inherit: '' },
|
|
132
|
+
* toolExecution: { path: ['extensions', 'com.example.host', 'toolExecution'], inherit: 'inherit' },
|
|
133
|
+
* });
|
|
134
|
+
* resolveAgentSpec([settings.toLayer(row), profile.toLayer(agent)]);
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* Keyed by column rather than by field, so the marker and the conversions are typed against that
|
|
139
|
+
* column's own type and a column cannot be mapped twice. A column feeds one field: a row that
|
|
140
|
+
* spreads one field over several columns, or a record column over many fields, reads that part by
|
|
141
|
+
* hand and names it in `handled`.
|
|
142
|
+
*/
|
|
143
|
+
export function defineSpecColumns(columns, { handled = [] } = {}) {
|
|
144
|
+
const entries = [];
|
|
145
|
+
for (const column of Object.keys(columns)) {
|
|
146
|
+
const definition = columns[column];
|
|
147
|
+
if (definition) {
|
|
148
|
+
entries.push({ column, segments: segmentsOf(definition.path), hasMarker: 'inherit' in definition, definition });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
checkTable(entries);
|
|
152
|
+
const handledPaths = handled.map(segmentsOf);
|
|
153
|
+
/**
|
|
154
|
+
* What a column stores for a field's value.
|
|
155
|
+
*
|
|
156
|
+
* @param entry - The column.
|
|
157
|
+
* @param value - The field's value, present.
|
|
158
|
+
* @returns The column's value, or `undefined` where it refused it.
|
|
159
|
+
*/
|
|
160
|
+
const stored = (entry, value) => entry.definition.fromSpec ? entry.definition.fromSpec(value) : value;
|
|
161
|
+
return {
|
|
162
|
+
toLayer(row) {
|
|
163
|
+
const layer = { spec: AGENT_SPEC };
|
|
164
|
+
for (const { column, segments, hasMarker, definition } of entries) {
|
|
165
|
+
const value = row[column];
|
|
166
|
+
const inherits = absent(value) || (hasMarker && value === definition.inherit);
|
|
167
|
+
if (inherits) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const said = definition.toSpec ? definition.toSpec(value) : value;
|
|
171
|
+
if (absent(said) === false) {
|
|
172
|
+
write(layer, segments, said);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Built as a plain object because the paths are data; each field is the column's value, of
|
|
176
|
+
// its path's type when the host's `toSpec` says so, and `parseSpec` is the check on that.
|
|
177
|
+
return layer;
|
|
178
|
+
},
|
|
179
|
+
fromLayer(layer) {
|
|
180
|
+
const row = {};
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
const value = read(layer, entry.segments);
|
|
183
|
+
const kept = absent(value) ? undefined : stored(entry, value);
|
|
184
|
+
if (kept !== undefined) {
|
|
185
|
+
row[entry.column] = kept;
|
|
186
|
+
}
|
|
187
|
+
else if (entry.hasMarker) {
|
|
188
|
+
row[entry.column] = entry.definition.inherit;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return row;
|
|
192
|
+
},
|
|
193
|
+
dropped(spec) {
|
|
194
|
+
const drops = [];
|
|
195
|
+
const isHandled = (path) => handledPaths.some((prefix) => startsWith(prefix, path));
|
|
196
|
+
const columnAt = (path) => entries.find((entry) => samePath(entry.segments, path));
|
|
197
|
+
const mappedBeneath = (path) => entries.some((entry) => entry.segments.length > path.length && startsWith(path, entry.segments));
|
|
198
|
+
const isStructural = (path) => path.length === 0 || (path.length === 1 && STRUCTURAL.includes(path[0]));
|
|
199
|
+
/**
|
|
200
|
+
* Reports a column's refusal of a value, if it refuses it.
|
|
201
|
+
*
|
|
202
|
+
* @param entry - The column at this path.
|
|
203
|
+
* @param path - The path as `parseSpec` spells it.
|
|
204
|
+
* @param value - The field's value, present.
|
|
205
|
+
*/
|
|
206
|
+
const offer = (entry, path, value) => {
|
|
207
|
+
if (stored(entry, value) === undefined) {
|
|
208
|
+
drops.push({ path, value, column: entry.column });
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Reports every part of a prompt no column takes, and every reference one does.
|
|
213
|
+
*
|
|
214
|
+
* @param parts - The layer's prompt.
|
|
215
|
+
*/
|
|
216
|
+
const walkPrompt = (parts) => {
|
|
217
|
+
for (const [index, part] of parts.entries()) {
|
|
218
|
+
const path = [PROMPT, part.id];
|
|
219
|
+
if (isHandled(path)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const entry = columnAt(path);
|
|
223
|
+
if (entry === undefined) {
|
|
224
|
+
drops.push({ path: `${PROMPT}[${index}]`, value: part });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (absent(part.text) === false) {
|
|
228
|
+
offer(entry, `${PROMPT}[${index}].text`, part.text);
|
|
229
|
+
}
|
|
230
|
+
if (part.ref) {
|
|
231
|
+
drops.push({ path: `${PROMPT}[${index}].ref`, value: part.ref });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Reports what lies at a path and is not kept, descending where something beneath is mapped.
|
|
237
|
+
*
|
|
238
|
+
* @param value - What the document holds at the path.
|
|
239
|
+
* @param path - Where it is, as segments.
|
|
240
|
+
*/
|
|
241
|
+
const walk = (value, path) => {
|
|
242
|
+
if (absent(value) || isHandled(path)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const entry = columnAt(path);
|
|
246
|
+
if (entry) {
|
|
247
|
+
offer(entry, path.join('.'), value);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (path.length === 1 && path[0] === PROMPT && Array.isArray(value)) {
|
|
251
|
+
walkPrompt(value);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (path.length === 1 && path[0] === HOOKS && Array.isArray(value)) {
|
|
255
|
+
for (const [index, hook] of value.entries()) {
|
|
256
|
+
drops.push({ path: `${HOOKS}[${index}]`, value: hook });
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (isRecord(value) && (isStructural(path) || mappedBeneath(path))) {
|
|
261
|
+
for (const [key, held] of Object.entries(value)) {
|
|
262
|
+
const isToken = path.length === 0 && key === TOKEN;
|
|
263
|
+
if (isToken === false) {
|
|
264
|
+
walk(held, [...path, key]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
drops.push({ path: path.join('.'), value });
|
|
270
|
+
};
|
|
271
|
+
walk(spec, []);
|
|
272
|
+
return drops;
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
package/llms.txt
CHANGED
|
@@ -226,6 +226,20 @@ What endpoints and models refused, carried across a restart.
|
|
|
226
226
|
- `importCapabilities` — Latches what a stored snapshot says was refused, on top of whatever this process has learned.
|
|
227
227
|
- `ModelSnapshot` (type) — What one model on an endpoint refused.
|
|
228
228
|
|
|
229
|
+
### columns
|
|
230
|
+
|
|
231
|
+
A host's stored agent, read as a spec layer and written back: one table of columns, and the marker each column stores for "inherit".
|
|
232
|
+
|
|
233
|
+
- `defineSpecColumns` — A column table from a stored row to a spec layer and back, with each column's "inherit" marker a parameter.
|
|
234
|
+
- `ExtensionPath` (type) — A path into a host's own extension, as segments, because its key is a reverse domain and has dots of its own.
|
|
235
|
+
- `SpecColumn` (type) — One column: where its value lives in a spec, what it stores for "inherit", and how its value crosses when the column and the field do not hold the same thing.
|
|
236
|
+
- `SpecColumnMapping` (type) — A row's column table, both ways.
|
|
237
|
+
- `SpecColumns` (type) — A row's columns, each mapped to a field: keyed by column, so a name the row does not have is an excess property and a marker of the wrong type does not typecheck.
|
|
238
|
+
- `SpecColumnsOptions` (type) — What `defineSpecColumns` takes besides the columns.
|
|
239
|
+
- `SpecDrop` (type) — One thing a document says that a row will not keep.
|
|
240
|
+
- `SpecPath` (type) — Where in a spec a column lives, as a dotted path.
|
|
241
|
+
- `SpecSection` (type) — The top-level keys of a spec, for naming a whole section a host reads by hand.
|
|
242
|
+
|
|
229
243
|
### spec
|
|
230
244
|
|
|
231
245
|
A JSON document that defines an agent, and the rules for reading one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.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",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./spec": {
|
|
36
36
|
"types": "./dist/spec/spec.d.ts",
|
|
37
37
|
"import": "./dist/spec/spec.js"
|
|
38
|
+
},
|
|
39
|
+
"./spec/columns": {
|
|
40
|
+
"types": "./dist/spec/columns.d.ts",
|
|
41
|
+
"import": "./dist/spec/columns.js"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"files": [
|