@cubicecho/agent-core 4.2.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 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
@@ -1196,7 +1196,7 @@ comes from.
1196
1196
  ### Its side tasks
1197
1197
 
1198
1198
  `tasks.<key>` configures a side task apart from the main model — its own model, endpoint, ceiling,
1199
- temperature and reasoning effort — and resolution hands each back as a `ResolvedTask` holding only
1199
+ temperature, reasoning effort and `extraBody` — and resolution hands each back as a `ResolvedTask` holding only
1200
1200
  what a layer stated. `taskCall` turns one into the arguments `ask`, `askJson`, `summariser` and
1201
1201
  `preselect` already take, so a setting added to a task later arrives without the host's call
1202
1202
  changing:
@@ -1217,6 +1217,13 @@ no-thinking hints by default, and absent or `"off"` keeps them; a level — `"lo
1217
1217
  sent as `reasoning_effort` in their place, for a compaction model an operator pointed at a model
1218
1218
  that should deliberate.
1219
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
+
1220
1227
  The third argument is the endpoint the main turn uses. Given it, a task on the same endpoint is
1221
1228
  sent the agent's spelling of the URL and, having no key of its own, the agent's key, so the two
1222
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 { EndpointIdentity } from '../core/config.ts';
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.
@@ -1,5 +1,5 @@
1
1
  import type OpenAI from 'openai';
2
- import { type ModelParams } from '../core/config.ts';
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
  /**
@@ -1,5 +1,4 @@
1
- import { RESERVED_BODY_FIELDS } from '../core/config.js';
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
- ...Object.fromEntries(extra),
39
+ ...extraFields(refused, config.extraBody),
42
40
  model: config.model,
43
41
  messages,
44
42
  stream: true,
@@ -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 three settings are present only where a layer stated them, and nothing is filled in from
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
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
- ...(merged.get(key) ?? {}),
785
+ ...below,
778
786
  ...task,
779
- endpoint: mergeLayers([merged.get(key)?.endpoint, task.endpoint]),
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;
@@ -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 and reasoning effort
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[]>;
@@ -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 and reasoning effort
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 });
@@ -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
  *
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "4.2.0",
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",