@cubicecho/agent-core 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/endpoint/capabilities.d.ts +15 -1
- package/dist/endpoint/capabilities.js +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run/request-body.d.ts +1 -1
- package/dist/run/request-body.js +2 -4
- package/dist/spec/spec.d.ts +47 -3
- package/dist/spec/spec.js +197 -28
- package/dist/tools/preselect.d.ts +5 -3
- package/dist/tools/preselect.js +4 -3
- package/dist/turn/side-task.d.ts +14 -1
- package/dist/turn/side-task.js +10 -3
- package/llms.txt +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -588,7 +588,7 @@ one just over it means the ranking chose arbitrarily, which is the case a model
|
|
|
588
588
|
on. Matching nothing is not confident either, since the words cannot tell a request that needs no
|
|
589
589
|
tools from one whose words are not in the catalogue. An empty `toolSelectModel` still means no
|
|
590
590
|
preselection at all, words included. `preselect` also takes a `temperature` and a
|
|
591
|
-
`reasoningEffort`, read as `ask` reads them, and `runAgentLoop({ preselect: true })` makes the call
|
|
591
|
+
`reasoningEffort` and an `extraBody`, read as `ask` reads them, and `runAgentLoop({ preselect: true })` makes the call
|
|
592
592
|
itself — see "Its side tasks" below.
|
|
593
593
|
|
|
594
594
|
A turn cut off at `maxTokens` is said so as a notice, and with `maxContinuations` above zero it is
|
|
@@ -1165,6 +1165,24 @@ applies `resolveApiKey` afterwards, so a document naming somebody else's endpoin
|
|
|
1165
1165
|
host send its own provider key there. `exportSpec(spec)` strips a bundled server's `env` and
|
|
1166
1166
|
`headers`, and a redacted document is still a valid one.
|
|
1167
1167
|
|
|
1168
|
+
Those two fields are where the format puts a bundled server's credentials, and not the only place a
|
|
1169
|
+
pasted `.mcp.json` has them: an `--api-key` among the `args`, a `?token=` or a `user:pass@` in the
|
|
1170
|
+
`url`. `exportSpec` keeps those, since a server with its query string cut off is a different
|
|
1171
|
+
server, and `findSpecSecrets` says where they may be so a host can warn before it calls a file safe
|
|
1172
|
+
to share:
|
|
1173
|
+
|
|
1174
|
+
```ts
|
|
1175
|
+
const out = exportSpec(spec);
|
|
1176
|
+
for (const { path, server, sign } of findSpecSecrets(out)) {
|
|
1177
|
+
warn(`${server}: ${path} may hold a credential (${sign})`); // bundle.mcpServers[0].args[2] … (flag)
|
|
1178
|
+
}
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
A hint is a place and a reason — `SecretSign.Userinfo`, `Query`, `Flag`, `Value` or `Field` — and
|
|
1182
|
+
never the value. It is a guess in both directions: a credential in a positional argument with no
|
|
1183
|
+
telling prefix is missed, and a flag called `--keyboard` is named. A `${VAR}` is a reference, and is
|
|
1184
|
+
not reported.
|
|
1185
|
+
|
|
1168
1186
|
`bundle.mcpServers` is parsed only under `parseSpec(document, { bundle: true })`. A bundled server
|
|
1169
1187
|
is a command line and an environment map, so importing one is equivalent to running a program, and
|
|
1170
1188
|
that is a decision a host makes rather than inherits from a default.
|
|
@@ -1178,7 +1196,7 @@ comes from.
|
|
|
1178
1196
|
### Its side tasks
|
|
1179
1197
|
|
|
1180
1198
|
`tasks.<key>` configures a side task apart from the main model — its own model, endpoint, ceiling,
|
|
1181
|
-
temperature
|
|
1199
|
+
temperature, reasoning effort and `extraBody` — and resolution hands each back as a `ResolvedTask` holding only
|
|
1182
1200
|
what a layer stated. `taskCall` turns one into the arguments `ask`, `askJson`, `summariser` and
|
|
1183
1201
|
`preselect` already take, so a setting added to a task later arrives without the host's call
|
|
1184
1202
|
changing:
|
|
@@ -1199,6 +1217,13 @@ no-thinking hints by default, and absent or `"off"` keeps them; a level — `"lo
|
|
|
1199
1217
|
sent as `reasoning_effort` in their place, for a compaction model an operator pointed at a model
|
|
1200
1218
|
that should deliberate.
|
|
1201
1219
|
|
|
1220
|
+
`extraBody` carries the sampler fields a model card asks for, as it does on the main model: `ask`,
|
|
1221
|
+
`askJson`, `summariser` and `preselect` all take one in their options, merge it in after the
|
|
1222
|
+
temperature, and offer its keys to `negotiate` as `droppable`, so a field the endpoint refuses by
|
|
1223
|
+
name is latched off for that model and the request is sent again without it. The agent's own
|
|
1224
|
+
`model.extraBody` is never sent to a side task, and `taskCall` lays a task's over the host's key by
|
|
1225
|
+
key.
|
|
1226
|
+
|
|
1202
1227
|
The third argument is the endpoint the main turn uses. Given it, a task on the same endpoint is
|
|
1203
1228
|
sent the agent's spelling of the URL and, having no key of its own, the agent's key, so the two
|
|
1204
1229
|
share one entry in `capabilitiesFor` and a refusal learned by either is known to both; a task on
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EndpointIdentity } from '../core/config.ts';
|
|
2
2
|
import type { Produced } from '../wire/stream.ts';
|
|
3
3
|
/**
|
|
4
4
|
* What an endpoint turned out not to support, and answering it when it says so.
|
|
@@ -307,6 +307,20 @@ export declare const ceilingAndTemperature: (refused: ModelCapabilities | undefi
|
|
|
307
307
|
} | {
|
|
308
308
|
temperature?: number | undefined;
|
|
309
309
|
};
|
|
310
|
+
/**
|
|
311
|
+
* The request fields an `extraBody` adds as this model takes them, to spread into a request body.
|
|
312
|
+
*
|
|
313
|
+
* @param refused - What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
314
|
+
* has refused nothing.
|
|
315
|
+
* @param [extraBody] - The fields to add. Absent adds none.
|
|
316
|
+
* @returns A fresh object of the fields, less the `RESERVED_BODY_FIELDS` and those the model has
|
|
317
|
+
* refused by name.
|
|
318
|
+
*
|
|
319
|
+
* @remarks
|
|
320
|
+
* The keys are what a caller hands `negotiate` as `droppable`, so a field the endpoint has never
|
|
321
|
+
* heard of is latched into `refusedFields` and left out of the next send.
|
|
322
|
+
*/
|
|
323
|
+
export declare const extraFields: (refused: ModelCapabilities | undefined, extraBody?: Record<string, unknown>) => Record<string, unknown>;
|
|
310
324
|
/** What `negotiate` takes besides the request. All optional. */
|
|
311
325
|
export interface NegotiateOptions {
|
|
312
326
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RESERVED_BODY_FIELDS } from '../core/config.js';
|
|
1
2
|
import { getOrCreate } from '../core/guards.js';
|
|
2
3
|
import { scoped } from '../core/scope.js';
|
|
3
4
|
import { errorMessage } from '../wire/errors.js';
|
|
@@ -353,6 +354,20 @@ export const ceilingAndTemperature = (refused, maxTokens, temperature) => {
|
|
|
353
354
|
...(refused?.chosenTemperature === false ? {} : { temperature }),
|
|
354
355
|
};
|
|
355
356
|
};
|
|
357
|
+
/**
|
|
358
|
+
* The request fields an `extraBody` adds as this model takes them, to spread into a request body.
|
|
359
|
+
*
|
|
360
|
+
* @param refused - What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
361
|
+
* has refused nothing.
|
|
362
|
+
* @param [extraBody] - The fields to add. Absent adds none.
|
|
363
|
+
* @returns A fresh object of the fields, less the `RESERVED_BODY_FIELDS` and those the model has
|
|
364
|
+
* refused by name.
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
367
|
+
* The keys are what a caller hands `negotiate` as `droppable`, so a field the endpoint has never
|
|
368
|
+
* heard of is latched into `refusedFields` and left out of the next send.
|
|
369
|
+
*/
|
|
370
|
+
export const extraFields = (refused, extraBody = {}) => Object.fromEntries(Object.entries(extraBody).filter(([field]) => !RESERVED_BODY_FIELDS.includes(field) && !refused?.refusedFields.has(field)));
|
|
356
371
|
/**
|
|
357
372
|
* How to answer a refused effort *value*, or `undefined` when there is nothing new to learn or
|
|
358
373
|
* nowhere left to go — in which case the refusal is the caller's, as it was before this existed.
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { preview } from './run/run-calls.ts';
|
|
|
25
25
|
export { resetAll } from './runtime/reset.ts';
|
|
26
26
|
export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from './runtime/runtime.ts';
|
|
27
27
|
export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from './runtime/snapshot.ts';
|
|
28
|
-
export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, type AgentHook, type AgentSpec, type EndpointSpec, type ExportSpecOptions, exportSpec, type ModelSpec, type ParsedSpec, type ParseSpecOptions, type PromptPart, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from './spec/spec.ts';
|
|
28
|
+
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';
|
|
29
29
|
export type { CatalogServer } from './tools/catalog.ts';
|
|
30
30
|
export { type KeywordPreselection, type KeywordPreselectOptions, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, type ToolMatch, } from './tools/preselect.ts';
|
|
31
31
|
export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, type ToolOrder, } from './tools/tool-loading.ts';
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { preview } from './run/run-calls.js';
|
|
|
25
25
|
export { resetAll } from './runtime/reset.js';
|
|
26
26
|
export { createRuntime, defaultRuntime } from './runtime/runtime.js';
|
|
27
27
|
export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from './runtime/snapshot.js';
|
|
28
|
-
export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, resolveAgentSpec, SPEC_EVENTS, } from './spec/spec.js';
|
|
28
|
+
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';
|
|
29
29
|
export { PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, } from './tools/preselect.js';
|
|
30
30
|
export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, } from './tools/tool-loading.js';
|
|
31
31
|
export { continueTurn, } from './turn/continuation.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
|
-
import {
|
|
2
|
+
import type { ModelParams } from '../core/config.ts';
|
|
3
3
|
import { type Capabilities, type ModelCapabilities } from '../endpoint/capabilities.ts';
|
|
4
4
|
import { type ToolOrder } from '../tools/tool-loading.ts';
|
|
5
5
|
/**
|
package/dist/run/request-body.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ceilingAndTemperature, effortFor, } from '../endpoint/capabilities.js';
|
|
1
|
+
import { ceilingAndTemperature, effortFor, extraFields, } from '../endpoint/capabilities.js';
|
|
3
2
|
import { relaxTools, sanitizeTools } from '../endpoint/schema-compat.js';
|
|
4
3
|
import { orderTools } from '../tools/tool-loading.js';
|
|
5
4
|
/**
|
|
@@ -33,12 +32,11 @@ export function buildBody(config, supports, refused, messages, tools = [], order
|
|
|
33
32
|
const sorted = orderTools(tools, order);
|
|
34
33
|
const declared = supports.strictSchemas ? sanitizeTools(sorted) : relaxTools(sanitizeTools(sorted));
|
|
35
34
|
const effort = effortFor(refused, config.reasoningEffort);
|
|
36
|
-
const extra = Object.entries(config.extraBody ?? {}).filter(([field]) => !RESERVED_BODY_FIELDS.includes(field) && !refused?.refusedFields.has(field));
|
|
37
35
|
return {
|
|
38
36
|
...ceilingAndTemperature(refused, config.maxTokens, config.temperature),
|
|
39
37
|
...(effort ? { reasoning_effort: effort } : {}),
|
|
40
38
|
...(supports.usageInStream ? { stream_options: { include_usage: true } } : {}),
|
|
41
|
-
...
|
|
39
|
+
...extraFields(refused, config.extraBody),
|
|
42
40
|
model: config.model,
|
|
43
41
|
messages,
|
|
44
42
|
stream: true,
|
package/dist/spec/spec.d.ts
CHANGED
|
@@ -151,6 +151,8 @@ export interface TaskSpec {
|
|
|
151
151
|
* side task is sent by default; see `SideTaskOptions.reasoningEffort`.
|
|
152
152
|
*/
|
|
153
153
|
reasoningEffort?: string;
|
|
154
|
+
/** Request fields the interface cannot spell, as `model.extraBody` but for this task alone. */
|
|
155
|
+
extraBody?: Record<string, unknown>;
|
|
154
156
|
}
|
|
155
157
|
/** A tool call bound to a lifecycle event, run by the host rather than the model. */
|
|
156
158
|
export interface AgentHook {
|
|
@@ -198,7 +200,7 @@ export interface SpecBundle {
|
|
|
198
200
|
* One side task, resolved: a model, and the endpoint it is reached through.
|
|
199
201
|
*
|
|
200
202
|
* @remarks
|
|
201
|
-
* The
|
|
203
|
+
* The settings are present only where a layer stated them, and nothing is filled in from
|
|
202
204
|
* the agent's own model — `taskCall` turns one into a call, and what is absent there is left to
|
|
203
205
|
* the side task's default.
|
|
204
206
|
*/
|
|
@@ -209,6 +211,8 @@ export interface ResolvedTask {
|
|
|
209
211
|
maxTokens?: number;
|
|
210
212
|
temperature?: number;
|
|
211
213
|
reasoningEffort?: string;
|
|
214
|
+
/** Merged key by key across the layers. `model.extraBody` is the agent's and never reaches it. */
|
|
215
|
+
extraBody?: Record<string, unknown>;
|
|
212
216
|
}
|
|
213
217
|
/**
|
|
214
218
|
* What layered documents resolve to: the flat shape the loop already takes, plus the parts of an
|
|
@@ -359,7 +363,47 @@ export interface ExportSpecOptions {
|
|
|
359
363
|
* The property worth keeping is that **a redacted spec is still a valid spec** — it parses, it
|
|
360
364
|
* resolves, and what comes back is the same agent pointed at the same servers, needing only the
|
|
361
365
|
* credentials the importing host supplies itself. So a bundled server loses its `env` and its
|
|
362
|
-
* `headers` and nothing else, and nothing outside `bundle` is touched:
|
|
363
|
-
*
|
|
366
|
+
* `headers` and nothing else, and nothing outside `bundle` is touched: those two are the only
|
|
367
|
+
* fields the format gives a credential. A server can still carry one where the format gives it no
|
|
368
|
+
* field — an `--api-key` among its `args`, a token in its `url` — and that is kept, because
|
|
369
|
+
* cutting it would point the agent at a different server. `findSpecSecrets` says where to look.
|
|
364
370
|
*/
|
|
365
371
|
export declare function exportSpec(spec: AgentSpec, { secrets }?: ExportSpecOptions): AgentSpec;
|
|
372
|
+
/** Why `findSpecSecrets` named a place. */
|
|
373
|
+
export declare const SecretSign: {
|
|
374
|
+
/** A URL with a user or a password before its host. */
|
|
375
|
+
readonly Userinfo: 'userinfo';
|
|
376
|
+
/** A URL query parameter whose name reads as a credential's. */
|
|
377
|
+
readonly Query: 'query';
|
|
378
|
+
/** The value of a command-line flag, or of a header written as `Name: value`, so named. */
|
|
379
|
+
readonly Flag: 'flag';
|
|
380
|
+
/** A value with the shape of a credential: a known key prefix, or `Bearer` and a token. */
|
|
381
|
+
readonly Value: 'value';
|
|
382
|
+
/** A key of the server itself whose name reads as a credential's. */
|
|
383
|
+
readonly Field: 'field';
|
|
384
|
+
};
|
|
385
|
+
/** Any one of the reasons in `SecretSign`. */
|
|
386
|
+
export type SecretSign = (typeof SecretSign)[keyof typeof SecretSign];
|
|
387
|
+
/** One place `findSpecSecrets` thinks a credential may be, and never the credential. */
|
|
388
|
+
export interface SecretHint {
|
|
389
|
+
/** Where, in `parseSpec`'s spelling: `bundle.mcpServers[0].args[2]`. */
|
|
390
|
+
path: string;
|
|
391
|
+
/** The server's slug, or its id where it has no slug. */
|
|
392
|
+
server: string;
|
|
393
|
+
sign: SecretSign;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Where a document's bundled servers may still carry a credential after `exportSpec`.
|
|
397
|
+
*
|
|
398
|
+
* @param spec - A document, exported or not: `env` and `headers` are not reported either way,
|
|
399
|
+
* since `exportSpec` strips them.
|
|
400
|
+
* @returns One hint for each suspect place, in document order, each naming where and why and
|
|
401
|
+
* none holding the value. Empty for a document with no bundle.
|
|
402
|
+
*
|
|
403
|
+
* @remarks
|
|
404
|
+
* A host calls this before it offers an export as safe to share, and warns with what comes back.
|
|
405
|
+
* It reads a server's `url`, its `command` and `args`, and the names of its other keys, and is a
|
|
406
|
+
* guess both ways: a credential in a positional argument with no telling prefix is missed, and a
|
|
407
|
+
* flag called `--keyboard` is named. A `${VAR}` is a reference and is never named.
|
|
408
|
+
*/
|
|
409
|
+
export declare function findSpecSecrets(spec: AgentSpec): SecretHint[];
|
package/dist/spec/spec.js
CHANGED
|
@@ -241,10 +241,6 @@ const parseSampling = (report, path, value) => ({
|
|
|
241
241
|
* @param value - What the document held under `model`.
|
|
242
242
|
* @returns The fields that survived — an empty object where none did — or `undefined` where the
|
|
243
243
|
* section is absent or is not an object.
|
|
244
|
-
*
|
|
245
|
-
* @remarks
|
|
246
|
-
* `extraBody` comes back as a new object without the `RESERVED_BODY_FIELDS`, each of which is
|
|
247
|
-
* warned about. An empty one survives as an empty one.
|
|
248
244
|
*/
|
|
249
245
|
function parseModel(report, value) {
|
|
250
246
|
if (absent(value)) {
|
|
@@ -253,31 +249,41 @@ function parseModel(report, value) {
|
|
|
253
249
|
if (!isRecord(value)) {
|
|
254
250
|
return void report.drop('model', 'must be an object');
|
|
255
251
|
}
|
|
256
|
-
let extraBody;
|
|
257
|
-
if (!absent(value.extraBody)) {
|
|
258
|
-
if (!isRecord(value.extraBody)) {
|
|
259
|
-
report.drop('model.extraBody', 'must be an object');
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
extraBody = {};
|
|
263
|
-
for (const [key, held] of Object.entries(value.extraBody)) {
|
|
264
|
-
// `buildBody` would overwrite them anyway; better to say so on import.
|
|
265
|
-
if (RESERVED_BODY_FIELDS.includes(key)) {
|
|
266
|
-
report.drop(`model.extraBody.${key}`, "is the loop's to set");
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
extraBody[key] = held;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
252
|
return kept({
|
|
275
253
|
model: report.string('model.model', value.model),
|
|
276
254
|
...parseSampling(report, 'model', value),
|
|
277
255
|
contextLength: report.number('model.contextLength', value.contextLength, 0),
|
|
278
|
-
extraBody,
|
|
256
|
+
extraBody: parseExtraBody(report, 'model.extraBody', value.extraBody),
|
|
279
257
|
});
|
|
280
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Reads an `extraBody`, the agent's model's or a side task's.
|
|
261
|
+
*
|
|
262
|
+
* @param report - Where what is dropped is written.
|
|
263
|
+
* @param path - Where it sits in the document: `model.extraBody`, or `tasks.title.extraBody`.
|
|
264
|
+
* @param value - What the document held there.
|
|
265
|
+
* @returns A new object without the `RESERVED_BODY_FIELDS`, each of which is warned about, or
|
|
266
|
+
* `undefined` where it is absent or is not an object. An empty one survives as an empty one.
|
|
267
|
+
*/
|
|
268
|
+
function parseExtraBody(report, path, value) {
|
|
269
|
+
if (absent(value)) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
if (!isRecord(value)) {
|
|
273
|
+
return void report.drop(path, 'must be an object');
|
|
274
|
+
}
|
|
275
|
+
const extraBody = {};
|
|
276
|
+
for (const [key, held] of Object.entries(value)) {
|
|
277
|
+
// The request is built around them and they would be overwritten anyway; better to say so on import.
|
|
278
|
+
if (RESERVED_BODY_FIELDS.includes(key)) {
|
|
279
|
+
report.drop(`${path}.${key}`, "is the loop's to set");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
extraBody[key] = held;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return extraBody;
|
|
286
|
+
}
|
|
281
287
|
/**
|
|
282
288
|
* Reads the `prompt` array, part by part.
|
|
283
289
|
*
|
|
@@ -429,6 +435,7 @@ function parseTasks(report, value) {
|
|
|
429
435
|
model: report.string(`${path}.model`, held.model),
|
|
430
436
|
endpoint: parseEndpoint(report, `${path}.endpoint`, held.endpoint),
|
|
431
437
|
...parseSampling(report, path, held),
|
|
438
|
+
extraBody: parseExtraBody(report, `${path}.extraBody`, held.extraBody),
|
|
432
439
|
});
|
|
433
440
|
}
|
|
434
441
|
return tasks;
|
|
@@ -767,16 +774,18 @@ function mergeById(items, idOf) {
|
|
|
767
774
|
* @param layers - The documents, weakest first. None is written to.
|
|
768
775
|
* @param endpoint - The agent's merged endpoint. Read, not written to.
|
|
769
776
|
* @returns The tasks whose merged `model` is not empty, keyed as the documents keyed them — one a
|
|
770
|
-
* layer declined with `model: ""` is not among them.
|
|
777
|
+
* layer declined with `model: ""` is not among them. A task's `extraBody` merges by key.
|
|
771
778
|
*/
|
|
772
779
|
function resolveTasks(layers, endpoint) {
|
|
773
780
|
const merged = new Map();
|
|
774
781
|
for (const layer of layers) {
|
|
775
782
|
for (const [key, task] of Object.entries(layer.tasks ?? {})) {
|
|
783
|
+
const below = merged.get(key);
|
|
776
784
|
merged.set(key, {
|
|
777
|
-
...
|
|
785
|
+
...below,
|
|
778
786
|
...task,
|
|
779
|
-
endpoint: mergeLayers([
|
|
787
|
+
endpoint: mergeLayers([below?.endpoint, task.endpoint]),
|
|
788
|
+
extraBody: mergeLayers([below?.extraBody, task.extraBody]),
|
|
780
789
|
});
|
|
781
790
|
}
|
|
782
791
|
}
|
|
@@ -793,6 +802,7 @@ function resolveTasks(layers, endpoint) {
|
|
|
793
802
|
maxTokens: task.maxTokens,
|
|
794
803
|
temperature: task.temperature,
|
|
795
804
|
reasoningEffort: task.reasoningEffort,
|
|
805
|
+
extraBody: task.extraBody && said(task.extraBody) ? task.extraBody : undefined,
|
|
796
806
|
});
|
|
797
807
|
}
|
|
798
808
|
return tasks;
|
|
@@ -896,8 +906,10 @@ export function resolveAgentSpec(layers) {
|
|
|
896
906
|
* The property worth keeping is that **a redacted spec is still a valid spec** — it parses, it
|
|
897
907
|
* resolves, and what comes back is the same agent pointed at the same servers, needing only the
|
|
898
908
|
* credentials the importing host supplies itself. So a bundled server loses its `env` and its
|
|
899
|
-
* `headers` and nothing else, and nothing outside `bundle` is touched:
|
|
900
|
-
*
|
|
909
|
+
* `headers` and nothing else, and nothing outside `bundle` is touched: those two are the only
|
|
910
|
+
* fields the format gives a credential. A server can still carry one where the format gives it no
|
|
911
|
+
* field — an `--api-key` among its `args`, a token in its `url` — and that is kept, because
|
|
912
|
+
* cutting it would point the agent at a different server. `findSpecSecrets` says where to look.
|
|
901
913
|
*/
|
|
902
914
|
export function exportSpec(spec, { secrets = false } = {}) {
|
|
903
915
|
if (secrets || !spec.bundle?.mcpServers) {
|
|
@@ -911,3 +923,160 @@ export function exportSpec(spec, { secrets = false } = {}) {
|
|
|
911
923
|
},
|
|
912
924
|
};
|
|
913
925
|
}
|
|
926
|
+
/** Why `findSpecSecrets` named a place. */
|
|
927
|
+
export const SecretSign = {
|
|
928
|
+
/** A URL with a user or a password before its host. */
|
|
929
|
+
Userinfo: 'userinfo',
|
|
930
|
+
/** A URL query parameter whose name reads as a credential's. */
|
|
931
|
+
Query: 'query',
|
|
932
|
+
/** The value of a command-line flag, or of a header written as `Name: value`, so named. */
|
|
933
|
+
Flag: 'flag',
|
|
934
|
+
/** A value with the shape of a credential: a known key prefix, or `Bearer` and a token. */
|
|
935
|
+
Value: 'value',
|
|
936
|
+
/** A key of the server itself whose name reads as a credential's. */
|
|
937
|
+
Field: 'field',
|
|
938
|
+
};
|
|
939
|
+
/** A name that reads as a credential's: `api-key`, `TOKEN`, `Authorization`. */
|
|
940
|
+
const SECRET_NAME = /key|token|secret|passw|auth|credential|bearer/i;
|
|
941
|
+
/** A value shaped like a credential, whatever it is called. */
|
|
942
|
+
const SECRET_VALUE = /^(sk-|pk-|rk-|ghp_|gho_|ghs_|github_pat_|glpat-|xox[abprs]-|AKIA)|\b(bearer|basic)\s+\S/i;
|
|
943
|
+
/** What an absolute URL opens with. */
|
|
944
|
+
const URL_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
945
|
+
/** A header as one string, `Name: value`, as `--header` takes it. */
|
|
946
|
+
const HEADER = /^([\w-]+):\s*(\S.*)$/;
|
|
947
|
+
/** A flag carrying its own value, `--name=value`. */
|
|
948
|
+
const FLAG_WITH_VALUE = /^(-{1,2}[^=\s]+)=(.+)$/;
|
|
949
|
+
/** The keys of a server `findSpecSecrets` reads for itself, or `exportSpec` strips. */
|
|
950
|
+
const SERVER_KEYS_READ = new Set(['id', 'slug', 'url', 'command', 'args', 'env', 'headers']);
|
|
951
|
+
/**
|
|
952
|
+
* Whether a string is a credential written out, as against a reference to one.
|
|
953
|
+
*
|
|
954
|
+
* @param value - What a field, a flag or a parameter holds.
|
|
955
|
+
* @returns `false` for an empty string and for one holding a `${VAR}`, which names where the
|
|
956
|
+
* credential is and is not it.
|
|
957
|
+
*/
|
|
958
|
+
const writtenOut = (value) => value !== '' && VARIABLE.test(value) === false;
|
|
959
|
+
/**
|
|
960
|
+
* Why a URL may carry a credential.
|
|
961
|
+
*
|
|
962
|
+
* @param text - A string, which need not be a URL.
|
|
963
|
+
* @returns `Userinfo`, `Query`, or `undefined` for a URL that shows neither and for a string
|
|
964
|
+
* that is not an absolute URL.
|
|
965
|
+
*/
|
|
966
|
+
function urlSign(text) {
|
|
967
|
+
if (URL_SCHEME.test(text) === false || URL.canParse(text) === false) {
|
|
968
|
+
return undefined;
|
|
969
|
+
}
|
|
970
|
+
const url = new URL(text);
|
|
971
|
+
if (writtenOut(url.username) || writtenOut(url.password)) {
|
|
972
|
+
return SecretSign.Userinfo;
|
|
973
|
+
}
|
|
974
|
+
for (const [name, value] of url.searchParams) {
|
|
975
|
+
if (SECRET_NAME.test(name) && writtenOut(value)) {
|
|
976
|
+
return SecretSign.Query;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return undefined;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Why one word of a command line may be a credential, read on its own.
|
|
983
|
+
*
|
|
984
|
+
* @param word - One argument.
|
|
985
|
+
* @returns A sign, or `undefined` for a word that shows none. A flag with no value of its own
|
|
986
|
+
* shows none here: `argumentSigns` reads the word after it.
|
|
987
|
+
*/
|
|
988
|
+
function wordSign(word) {
|
|
989
|
+
if (writtenOut(word) === false) {
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
const flag = FLAG_WITH_VALUE.exec(word);
|
|
993
|
+
if (flag?.[1] !== undefined && SECRET_NAME.test(flag[1])) {
|
|
994
|
+
return SecretSign.Flag;
|
|
995
|
+
}
|
|
996
|
+
const header = HEADER.exec(word);
|
|
997
|
+
if (header?.[1] !== undefined && SECRET_NAME.test(header[1])) {
|
|
998
|
+
return SecretSign.Flag;
|
|
999
|
+
}
|
|
1000
|
+
// A flag's own value is read as a word too: `--url=https://user:pass@host`.
|
|
1001
|
+
const value = flag?.[2] ?? word;
|
|
1002
|
+
return SECRET_VALUE.test(value) ? SecretSign.Value : urlSign(value);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* The words of a command line that may be credentials.
|
|
1006
|
+
*
|
|
1007
|
+
* @param words - The arguments in order. One that is not a string is passed over.
|
|
1008
|
+
* @returns Each suspect word's index and its sign, in order, at most one sign a word.
|
|
1009
|
+
*
|
|
1010
|
+
* @remarks
|
|
1011
|
+
* A flag named like a credential marks the word after it, unless that word is another flag.
|
|
1012
|
+
*/
|
|
1013
|
+
function argumentSigns(words) {
|
|
1014
|
+
const signs = [];
|
|
1015
|
+
let flagged = false;
|
|
1016
|
+
for (const [index, word] of words.entries()) {
|
|
1017
|
+
if (typeof word !== 'string') {
|
|
1018
|
+
flagged = false;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const isFlag = word.startsWith('-');
|
|
1022
|
+
const sign = flagged && isFlag === false && writtenOut(word) ? SecretSign.Flag : wordSign(word);
|
|
1023
|
+
if (sign) {
|
|
1024
|
+
signs.push([index, sign]);
|
|
1025
|
+
}
|
|
1026
|
+
flagged = isFlag && word.includes('=') === false && SECRET_NAME.test(word);
|
|
1027
|
+
}
|
|
1028
|
+
return signs;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* The places one bundled server may carry a credential, outside its `env` and `headers`.
|
|
1032
|
+
*
|
|
1033
|
+
* @param server - The server as the document holds it.
|
|
1034
|
+
* @param path - Where the server is, which each hint's path opens with.
|
|
1035
|
+
* @returns The hints in the order `url`, `command`, `args`, then the server's other keys.
|
|
1036
|
+
*/
|
|
1037
|
+
function serverSecrets(server, path) {
|
|
1038
|
+
const name = server.slug ?? server.id ?? '';
|
|
1039
|
+
const hints = [];
|
|
1040
|
+
const hint = (at, sign) => {
|
|
1041
|
+
if (sign) {
|
|
1042
|
+
hints.push({ path: `${path}.${at}`, server: name, sign });
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
if (typeof server.url === 'string') {
|
|
1046
|
+
hint('url', urlSign(server.url));
|
|
1047
|
+
}
|
|
1048
|
+
if (typeof server.command === 'string') {
|
|
1049
|
+
// A command written as one line is read as its words, and named once.
|
|
1050
|
+
hint('command', argumentSigns(server.command.split(/\s+/))[0]?.[1]);
|
|
1051
|
+
}
|
|
1052
|
+
if (Array.isArray(server.args)) {
|
|
1053
|
+
for (const [index, sign] of argumentSigns(server.args)) {
|
|
1054
|
+
hint(`args[${index}]`, sign);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
for (const [key, value] of Object.entries(server)) {
|
|
1058
|
+
const empty = absent(value) || value === '' || (typeof value === 'string' && writtenOut(value) === false);
|
|
1059
|
+
if (SERVER_KEYS_READ.has(key) === false && SECRET_NAME.test(key) && empty === false) {
|
|
1060
|
+
hint(key, SecretSign.Field);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return hints;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Where a document's bundled servers may still carry a credential after `exportSpec`.
|
|
1067
|
+
*
|
|
1068
|
+
* @param spec - A document, exported or not: `env` and `headers` are not reported either way,
|
|
1069
|
+
* since `exportSpec` strips them.
|
|
1070
|
+
* @returns One hint for each suspect place, in document order, each naming where and why and
|
|
1071
|
+
* none holding the value. Empty for a document with no bundle.
|
|
1072
|
+
*
|
|
1073
|
+
* @remarks
|
|
1074
|
+
* A host calls this before it offers an export as safe to share, and warns with what comes back.
|
|
1075
|
+
* It reads a server's `url`, its `command` and `args`, and the names of its other keys, and is a
|
|
1076
|
+
* guess both ways: a credential in a positional argument with no telling prefix is missed, and a
|
|
1077
|
+
* flag called `--keyboard` is named. A `${VAR}` is a reference and is never named.
|
|
1078
|
+
*/
|
|
1079
|
+
export function findSpecSecrets(spec) {
|
|
1080
|
+
const servers = spec.bundle?.mcpServers ?? [];
|
|
1081
|
+
return servers.flatMap((server, index) => serverSecrets(server, `bundle.mcpServers[${index}]`));
|
|
1082
|
+
}
|
|
@@ -158,6 +158,8 @@ export interface PreselectOptions {
|
|
|
158
158
|
temperature?: number;
|
|
159
159
|
/** As `SideTaskOptions.reasoningEffort`: absent keeps the no-thinking hints. */
|
|
160
160
|
reasoningEffort?: string;
|
|
161
|
+
/** As `SideTaskOptions.extraBody`: sampler fields the options cannot spell. */
|
|
162
|
+
extraBody?: Record<string, unknown>;
|
|
161
163
|
/**
|
|
162
164
|
* The most tools the choice may name.
|
|
163
165
|
*
|
|
@@ -181,8 +183,8 @@ export interface PreselectOptions {
|
|
|
181
183
|
* `toolSelectModel: settings.toolSelectModel || settings.model`.
|
|
182
184
|
* @param catalog - The servers to choose from.
|
|
183
185
|
* @param prompt - The request being planned for. Only its head is read; see `preselectInput`.
|
|
184
|
-
* @param [options] - Cancellation, notices, the reply ceiling, the temperature
|
|
185
|
-
* as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
186
|
+
* @param [options] - Cancellation, notices, the reply ceiling, the temperature, reasoning effort and
|
|
187
|
+
* extra fields as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
186
188
|
* @returns Names as the catalogue spells them, never more than `maxPerLoad`. Empty where `model` is
|
|
187
189
|
* empty, the catalogue has no tools, the call failed, or its reply named nothing in the catalogue.
|
|
188
190
|
*
|
|
@@ -198,4 +200,4 @@ export interface PreselectOptions {
|
|
|
198
200
|
* starting now and starting in a few seconds. The words have to be clear about it; see
|
|
199
201
|
* `preselectByKeywords` for what that means.
|
|
200
202
|
*/
|
|
201
|
-
export declare function preselect(config: Endpoint, model: string, catalog: CatalogServer[], prompt: string, { signal, onNotice, maxTokens, temperature, reasoningEffort, maxPerLoad, keywords, }?: PreselectOptions): Promise<string[]>;
|
|
203
|
+
export declare function preselect(config: Endpoint, model: string, catalog: CatalogServer[], prompt: string, { signal, onNotice, maxTokens, temperature, reasoningEffort, extraBody, maxPerLoad, keywords, }?: PreselectOptions): Promise<string[]>;
|
package/dist/tools/preselect.js
CHANGED
|
@@ -248,8 +248,8 @@ export function preselectByKeywords(catalog, prompt, { maxPerLoad = MAX_PER_LOAD
|
|
|
248
248
|
* `toolSelectModel: settings.toolSelectModel || settings.model`.
|
|
249
249
|
* @param catalog - The servers to choose from.
|
|
250
250
|
* @param prompt - The request being planned for. Only its head is read; see `preselectInput`.
|
|
251
|
-
* @param [options] - Cancellation, notices, the reply ceiling, the temperature
|
|
252
|
-
* as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
251
|
+
* @param [options] - Cancellation, notices, the reply ceiling, the temperature, reasoning effort and
|
|
252
|
+
* extra fields as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
253
253
|
* @returns Names as the catalogue spells them, never more than `maxPerLoad`. Empty where `model` is
|
|
254
254
|
* empty, the catalogue has no tools, the call failed, or its reply named nothing in the catalogue.
|
|
255
255
|
*
|
|
@@ -265,7 +265,7 @@ export function preselectByKeywords(catalog, prompt, { maxPerLoad = MAX_PER_LOAD
|
|
|
265
265
|
* starting now and starting in a few seconds. The words have to be clear about it; see
|
|
266
266
|
* `preselectByKeywords` for what that means.
|
|
267
267
|
*/
|
|
268
|
-
export async function preselect(config, model, catalog, prompt, { signal, onNotice, maxTokens = 256, temperature, reasoningEffort, maxPerLoad = MAX_PER_LOAD, keywords, } = {}) {
|
|
268
|
+
export async function preselect(config, model, catalog, prompt, { signal, onNotice, maxTokens = 256, temperature, reasoningEffort, extraBody, maxPerLoad = MAX_PER_LOAD, keywords, } = {}) {
|
|
269
269
|
if (!model || !catalog.some((server) => server.tools.length > 0)) {
|
|
270
270
|
return [];
|
|
271
271
|
}
|
|
@@ -285,6 +285,7 @@ export async function preselect(config, model, catalog, prompt, { signal, onNoti
|
|
|
285
285
|
maxTokens,
|
|
286
286
|
temperature,
|
|
287
287
|
reasoningEffort,
|
|
288
|
+
extraBody,
|
|
288
289
|
signal,
|
|
289
290
|
onNotice,
|
|
290
291
|
}), { onNotice, signal });
|
package/dist/turn/side-task.d.ts
CHANGED
|
@@ -55,6 +55,16 @@ export interface SideTaskOptions {
|
|
|
55
55
|
* refused it by value, and `chat_template_kwargs` is left out of the request altogether.
|
|
56
56
|
*/
|
|
57
57
|
reasoningEffort?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Request fields these options cannot spell, as `ModelParams.extraBody` is for the main model:
|
|
60
|
+
* `top_p`, `top_k`, `min_p` and the penalties a model card asks for.
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* Merged in after the ceiling, temperature and thinking fields, so it may override them, and
|
|
64
|
+
* before `askJson`'s `response_format`. `model`, `messages`, `stream` and `tools` are dropped. A
|
|
65
|
+
* field the endpoint refuses by name is latched for the model and the request sent again without it.
|
|
66
|
+
*/
|
|
67
|
+
extraBody?: Record<string, unknown>;
|
|
58
68
|
/** Abandons the call, usually because the run it supports has gone away. */
|
|
59
69
|
signal?: AbortSignal;
|
|
60
70
|
/**
|
|
@@ -97,6 +107,8 @@ export interface SideTask {
|
|
|
97
107
|
temperature?: number;
|
|
98
108
|
/** As `SideTaskOptions.reasoningEffort`: a level replaces the no-thinking hints. */
|
|
99
109
|
reasoningEffort?: string;
|
|
110
|
+
/** As `SideTaskOptions.extraBody`: the sampler fields a model card asks for, sent as given. */
|
|
111
|
+
extraBody?: Record<string, unknown>;
|
|
100
112
|
}
|
|
101
113
|
/**
|
|
102
114
|
* Runs a side task and returns the reply text, thinking stripped. Throws like any request.
|
|
@@ -105,7 +117,7 @@ export interface SideTask {
|
|
|
105
117
|
* @param model - The model to ask, usually smaller than the one running the work.
|
|
106
118
|
* @param system - The instruction.
|
|
107
119
|
* @param user - The input it applies to. Content parts where the model is being shown an image.
|
|
108
|
-
* @param [options] - Reply ceiling, temperature, a reasoning effort, cancellation, notices.
|
|
120
|
+
* @param [options] - Reply ceiling, temperature, a reasoning effort, extra fields, cancellation, notices.
|
|
109
121
|
* @returns The reply's content, trimmed. Where that is empty once the thinking is stripped, a
|
|
110
122
|
* `reasoning_content` the server sent beside it, stripped the same way, or an empty string.
|
|
111
123
|
*/
|
|
@@ -219,6 +231,7 @@ export declare const listLines: (text: string, max: number, maxChars: number) =>
|
|
|
219
231
|
* arrives through `options` without a host's call changing. What the task states wins over the
|
|
220
232
|
* same field in `options`: the host's code is written once and the task is what an operator
|
|
221
233
|
* configured over it, so a host passes its own ceiling as the value for a task that names none.
|
|
234
|
+
* `extraBody` is merged key by key, the task's keys winning, as layers merge it.
|
|
222
235
|
* A setting the task leaves out and `options` does too is absent from the result, which is what
|
|
223
236
|
* leaves it to the entry point's own default — 0.3, not the agent's temperature.
|
|
224
237
|
*
|
package/dist/turn/side-task.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { scoped } from '../core/scope.js';
|
|
2
|
-
import { capabilitiesFor, ceilingAndTemperature, EFFORT_NONE, effortFor, modelCapabilitiesFor, negotiate, } from '../endpoint/capabilities.js';
|
|
2
|
+
import { capabilitiesFor, ceilingAndTemperature, EFFORT_NONE, effortFor, extraFields, modelCapabilitiesFor, negotiate, } from '../endpoint/capabilities.js';
|
|
3
3
|
import { endpointId, getClient, modelKey, resolveApiKey, sameUrl } from '../endpoint/client.js';
|
|
4
4
|
import { relaxSchema, sanitizeSchema } from '../endpoint/schema-compat.js';
|
|
5
5
|
import { errorMessage } from '../wire/errors.js';
|
|
@@ -68,7 +68,7 @@ export const resetHints = () => noHints().clear();
|
|
|
68
68
|
* @param model - The model to ask, usually smaller than the one running the work.
|
|
69
69
|
* @param system - The instruction.
|
|
70
70
|
* @param user - The input it applies to. Content parts where the model is being shown an image.
|
|
71
|
-
* @param [options] - Reply ceiling, temperature, a reasoning effort, cancellation, notices.
|
|
71
|
+
* @param [options] - Reply ceiling, temperature, a reasoning effort, extra fields, cancellation, notices.
|
|
72
72
|
* @returns The reply's content, trimmed. Where that is empty once the thinking is stripped, a
|
|
73
73
|
* `reasoning_content` the server sent beside it, stripped the same way, or an empty string.
|
|
74
74
|
*/
|
|
@@ -86,13 +86,14 @@ export function ask(config, model, system, user, options = {}) {
|
|
|
86
86
|
* @param user - The input, sent as the user message as it was given.
|
|
87
87
|
* @param options - A side task's options. A `reasoningEffort` that is a level leaves the
|
|
88
88
|
* no-thinking hints out; absent, `""` or `"off"` sends them unless this pair has refused them.
|
|
89
|
+
* An `extraBody` goes out less the fields this model has refused by name.
|
|
89
90
|
* @param [format] - Builds the extra body fields from the endpoint's capabilities and the model's.
|
|
90
91
|
* Called on every send, so a re-send is built from what has been latched since. Absent adds none.
|
|
91
92
|
* @returns The first choice's message as `answerOf` reads it, empty when there was no answer.
|
|
92
93
|
* Throws what the request throws, except that a 400 or 422 `negotiate` did not answer, on a
|
|
93
94
|
* request that carried the hints or an effort, is retried once without either.
|
|
94
95
|
*/
|
|
95
|
-
async function complete(config, model, system, user, { maxTokens = 512, temperature = 0.3, reasoningEffort, signal, onNotice }, format) {
|
|
96
|
+
async function complete(config, model, system, user, { maxTokens = 512, temperature = 0.3, reasoningEffort, extraBody, signal, onNotice }, format) {
|
|
96
97
|
// The effort the caller chose, or empty where it chose none: `""` and `"off"` read as absent,
|
|
97
98
|
// since off is what the hints below already ask for.
|
|
98
99
|
const level = effortFor(undefined, reasoningEffort);
|
|
@@ -120,6 +121,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
120
121
|
// Not gated on `hints`: a model that refuses `chat_template_kwargs` may still read the
|
|
121
122
|
// effort, and the two latches would otherwise contradict each other.
|
|
122
123
|
...(sentEffort ? { reasoning_effort: asked } : {}),
|
|
124
|
+
...extraFields(refused, extraBody),
|
|
123
125
|
...(format && refused ? format(supports, refused) : {}),
|
|
124
126
|
}, { signal });
|
|
125
127
|
};
|
|
@@ -131,6 +133,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
131
133
|
const attempt = (hints, effort) => negotiate(supports, (latched, _produced, refused) => send(hints, effort, latched, refused), {
|
|
132
134
|
model,
|
|
133
135
|
onNotice,
|
|
136
|
+
droppable: Object.keys(extraBody ?? {}),
|
|
134
137
|
});
|
|
135
138
|
const key = modelKey(endpointId(config), model);
|
|
136
139
|
// A chosen effort replaces the template hint rather than riding beside it: `enable_thinking:
|
|
@@ -333,6 +336,7 @@ export const listLines = (text, max, maxChars) => text
|
|
|
333
336
|
* arrives through `options` without a host's call changing. What the task states wins over the
|
|
334
337
|
* same field in `options`: the host's code is written once and the task is what an operator
|
|
335
338
|
* configured over it, so a host passes its own ceiling as the value for a task that names none.
|
|
339
|
+
* `extraBody` is merged key by key, the task's keys winning, as layers merge it.
|
|
336
340
|
* A setting the task leaves out and `options` does too is absent from the result, which is what
|
|
337
341
|
* leaves it to the entry point's own default — 0.3, not the agent's temperature.
|
|
338
342
|
*
|
|
@@ -356,6 +360,9 @@ export function taskCall(task, options, agent) {
|
|
|
356
360
|
if (task.reasoningEffort !== undefined) {
|
|
357
361
|
stated.reasoningEffort = task.reasoningEffort;
|
|
358
362
|
}
|
|
363
|
+
if (task.extraBody !== undefined) {
|
|
364
|
+
stated.extraBody = { ...options?.extraBody, ...task.extraBody };
|
|
365
|
+
}
|
|
359
366
|
return {
|
|
360
367
|
endpoint: agent
|
|
361
368
|
? {
|
package/llms.txt
CHANGED
|
@@ -220,6 +220,7 @@ A JSON document that defines an agent, and the rules for reading one.
|
|
|
220
220
|
- `EndpointSpec` (type) — Where a model is reached, and how long to wait for it.
|
|
221
221
|
- `ExportSpecOptions` (type) — What `exportSpec` takes besides the document.
|
|
222
222
|
- `exportSpec` — A document fit to leave the host: the same agent, with the secrets of its bundled servers gone.
|
|
223
|
+
- `findSpecSecrets` — Where a document's bundled servers may still carry a credential after `exportSpec`.
|
|
223
224
|
- `ModelSpec` (type) — Which model answers and how, including the request fields this interface cannot spell.
|
|
224
225
|
- `ParsedSpec` (type) — What `parseSpec` hands back.
|
|
225
226
|
- `ParseSpecOptions` (type) — What `parseSpec` takes besides the document.
|
|
@@ -232,6 +233,8 @@ A JSON document that defines an agent, and the rules for reading one.
|
|
|
232
233
|
- `ResolvedTask` (type) — One side task, resolved: a model, and the endpoint it is reached through.
|
|
233
234
|
- `RetrySpec` (type) — What to do about a request lost before the model produced anything.
|
|
234
235
|
- `resolveAgentSpec` — Layers documents into the flat object the loop takes, weakest first.
|
|
236
|
+
- `SecretHint` (type) — One place `findSpecSecrets` thinks a credential may be, and never the credential.
|
|
237
|
+
- `SecretSign` — Any one of the reasons in `SecretSign`.
|
|
235
238
|
- `SPEC_EVENTS` — Every event a hook may be bound to: `HOOK_EVENTS`, under the name a document's reader knows.
|
|
236
239
|
- `SpecBundle` (type) — What an agent carries so it can travel whole: the definitions of the servers it names.
|
|
237
240
|
- `SpecServer` (type) — A bundled MCP server, validated only as far as a round trip needs and otherwise passed through.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai",
|