@cubicecho/agent-core 4.9.0 → 4.10.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
@@ -623,6 +623,29 @@ a `tool-call` and a `tool-result` event, told to `onToolCall` and `onToolResult`
623
623
  id is numbered by where the call lands in the transcript, so two questions do not share one. A name
624
624
  with no definition in `tools`, or outside the catalogue, is left out, and none left is no exchange.
625
625
 
626
+ ### What the servers said on the way
627
+
628
+ A server can log while a call runs — a warning that the cache was stale, an error it recovered
629
+ from — and that is often why the call went the way it did. `dispatch` is handed a third argument,
630
+ `report`, and `report.log(entry)` tells watchers of one such message as a `server-log` event for
631
+ the call: `id` the call's, `name` the tool, `level` the server's, and `text` the message's `data`,
632
+ as it was where a string and as JSON otherwise, cut by `preview`. The entry is MCP's
633
+ `notifications/message` params, `{ level, logger?, data }`, declared structurally, so
634
+ agent-mcp-pool's `onLog` takes `report.log` as it stands:
635
+
636
+ ```ts
637
+ dispatch: (call, signal, report) =>
638
+ pool.call(call.name, call.args, { servers, signal, onLog: report.log }),
639
+ ```
640
+
641
+ None of it reaches the transcript: it is for the person watching, not the model. A call the loop
642
+ answers itself never logs, a deduplicated repeat reports nothing of its own, and a message heard
643
+ after the call has answered is dropped rather than landing after the result it explains.
644
+ `runMetrics` counts `warning` as `serverWarnings` and `error` and above as `serverErrors`, so a
645
+ summary can say the server complained without the events. A dispatcher written for
646
+ `(call, signal)` needs no change. `report` is an object so that what a call can report — progress
647
+ is next — grows there without `dispatch` changing again.
648
+
626
649
  ### What the servers say about themselves
627
650
 
628
651
  An MCP server may send `instructions` when it connects — "resolve the library id before querying
package/dist/index.d.ts CHANGED
@@ -39,5 +39,5 @@ export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow,
39
39
  export { CacheBreakReason, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from './wire/stream.ts';
40
40
  export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, SPLIT_OUTPUT, SPLIT_REASONING, type Split, stripThinking, THINK_FENCE, } from './wire/thinking.ts';
41
41
  export { type ContextBreakdown, type ContextBreakdownOptions, compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, type TokenEstimateOptions, } from './wire/tokens.ts';
42
- export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, type ResourceLink, type RunArtifact, recoverToolCalls, type ToolAnswer, ToolArgumentsError, type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult, } from './wire/tool-calls.ts';
42
+ export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, type ResourceLink, type RunArtifact, recoverToolCalls, type ServerLogEntry, ServerLogLevel, type ToolAnswer, ToolArgumentsError, type ToolCall, type ToolCallOutcome, type ToolCallReport, type ToolCallRequest, type ToolCallResult, } from './wire/tool-calls.ts';
43
43
  export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire/wire.ts';
package/dist/index.js CHANGED
@@ -38,5 +38,5 @@ export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow,
38
38
  export { CacheBreakReason, streamTurn, } from './wire/stream.js';
39
39
  export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, SPLIT_OUTPUT, SPLIT_REASONING, stripThinking, THINK_FENCE, } from './wire/thinking.js';
40
40
  export { compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, } from './wire/tokens.js';
41
- export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, recoverToolCalls, ToolArgumentsError, } from './wire/tool-calls.js';
41
+ export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, recoverToolCalls, ServerLogLevel, ToolArgumentsError, } from './wire/tool-calls.js';
42
42
  export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire/wire.js';
@@ -1,7 +1,7 @@
1
1
  import type { TokenLedger } from '../context/ledger.ts';
2
2
  import type { TurnUsage } from '../wire/stream.ts';
3
3
  import { CacheBreakReason } from '../wire/stream.ts';
4
- import type { ResourceLink } from '../wire/tool-calls.ts';
4
+ import { type ResourceLink } from '../wire/tool-calls.ts';
5
5
  /**
6
6
  * What a run is doing, while it is doing it.
7
7
  *
@@ -96,6 +96,13 @@ export declare const RunEventKind: {
96
96
  * per uri per run: a later call that links the same uri again only adds to its count.
97
97
  */
98
98
  readonly Artifact: 'artifact';
99
+ /**
100
+ * A tool's server logged something while a call was running, which the dispatcher passed on
101
+ * through `report.log`. `id` is the call's, `name` the tool, `level` the server's, and `text`
102
+ * the message's data — as it was where a string, as JSON otherwise — cut by `preview`. Never in
103
+ * the transcript, and never from a call the loop answers itself.
104
+ */
105
+ readonly ServerLog: 'server-log';
99
106
  /**
100
107
  * What the run has cost so far, at the end of every turn, and what that one turn did. Sent
101
108
  * whether or not the endpoint reported tokens, since the timings and the cache comparison are
@@ -187,6 +194,11 @@ export interface RunEvent {
187
194
  * an event built before it existed still compiles.
188
195
  */
189
196
  link?: ResourceLink;
197
+ /**
198
+ * The level the server logged at, on `server-log` and on no other kind. Optional for the reason
199
+ * `id` is.
200
+ */
201
+ level?: string;
190
202
  }
191
203
  /**
192
204
  * What `emit` is given: the run and the sequence are the bus's to assign.
@@ -314,6 +326,10 @@ export interface RunMetrics {
314
326
  loadCalls: number;
315
327
  /** `artifact` events: the distinct things the run's tool calls linked to. */
316
328
  artifacts: number;
329
+ /** `server-log` events at `warning`. */
330
+ serverWarnings: number;
331
+ /** `server-log` events at `error` or above: `critical`, `alert` and `emergency` with it. */
332
+ serverErrors: number;
317
333
  /**
318
334
  * Tools `load_tools` loaded that were not loaded already. Filled by `runAgentLoop`, which sees
319
335
  * the resolution; the events carry only its text.
@@ -3,6 +3,7 @@ import { ABORT_EVENT, MS_PER_MINUTE } from '../core/platform.js';
3
3
  import { assignSettings, scoped } from '../core/scope.js';
4
4
  import { LOAD_TOOLS } from '../tools/tool-loading.js';
5
5
  import { CacheBreakReason } from '../wire/stream.js';
6
+ import { ServerLogLevel } from '../wire/tool-calls.js';
6
7
  import { FinishReason } from '../wire/wire.js';
7
8
  /** How many minutes an unended run's events are kept, absent a `retainUnendedMs`. */
8
9
  const RETAIN_UNENDED_MINUTES = 30;
@@ -62,6 +63,13 @@ export const RunEventKind = {
62
63
  * per uri per run: a later call that links the same uri again only adds to its count.
63
64
  */
64
65
  Artifact: 'artifact',
66
+ /**
67
+ * A tool's server logged something while a call was running, which the dispatcher passed on
68
+ * through `report.log`. `id` is the call's, `name` the tool, `level` the server's, and `text`
69
+ * the message's data — as it was where a string, as JSON otherwise — cut by `preview`. Never in
70
+ * the transcript, and never from a call the loop answers itself.
71
+ */
72
+ ServerLog: 'server-log',
65
73
  /**
66
74
  * What the run has cost so far, at the end of every turn, and what that one turn did. Sent
67
75
  * whether or not the endpoint reported tokens, since the timings and the cache comparison are
@@ -453,6 +461,10 @@ export function fold(events) {
453
461
  close();
454
462
  return blocks;
455
463
  }
464
+ /** The log levels quietest first, which is what makes "warning and above" a comparison. */
465
+ const SEVERITY = Object.values(ServerLogLevel);
466
+ /** Where `warning` sits in `SEVERITY`: a level past it is an error. */
467
+ const WARNING = SEVERITY.indexOf(ServerLogLevel.Warning);
456
468
  /** How a run ended, as `runMetrics` reads it. */
457
469
  export const RunOutcome = {
458
470
  /** The run did not fail, and its last turn was not cut off. */
@@ -507,6 +519,8 @@ export function runMetrics(events, { contextLength } = {}) {
507
519
  toolErrors: {},
508
520
  loadCalls: 0,
509
521
  artifacts: 0,
522
+ serverWarnings: 0,
523
+ serverErrors: 0,
510
524
  promptTokens: 0,
511
525
  completionTokens: 0,
512
526
  cachedTokens: 0,
@@ -550,6 +564,15 @@ export function runMetrics(events, { contextLength } = {}) {
550
564
  else if (event.kind === RunEventKind.Artifact) {
551
565
  metrics.artifacts++;
552
566
  }
567
+ else if (event.kind === RunEventKind.ServerLog) {
568
+ const severity = SEVERITY.indexOf(event.level ?? '');
569
+ if (severity === WARNING) {
570
+ metrics.serverWarnings++;
571
+ }
572
+ else if (severity > WARNING) {
573
+ metrics.serverErrors++;
574
+ }
575
+ }
553
576
  else if (event.kind === RunEventKind.Usage && event.usage?.turn) {
554
577
  const turn = event.usage.turn;
555
578
  last = turn;
@@ -1,7 +1,7 @@
1
1
  import type OpenAI from 'openai';
2
2
  import type { CatalogServer } from '../tools/catalog.ts';
3
3
  import type { Turn } from '../wire/stream.ts';
4
- import { type RunArtifact, type ToolAnswer, type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
4
+ import { type RunArtifact, type ToolAnswer, type ToolCall, type ToolCallOutcome, type ToolCallReport, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
5
5
  import type { RunEventInput } from './events.ts';
6
6
  /**
7
7
  * The tools between two turns: a step's calls read, run and written into the transcript.
@@ -66,8 +66,13 @@ export declare const definedAs: (definitions: ReadonlyMap<string, OpenAI.ChatCom
66
66
  /**
67
67
  * Runs one tool call and resolves to what the model reads, alone or with what the call produced;
68
68
  * what it throws, the model reads too.
69
+ *
70
+ * @remarks
71
+ * `report` is how the call tells watchers what its server said on the way, and a dispatcher with
72
+ * nothing to report leaves it off: one written for `(call, signal)` is a `ToolDispatch` still.
73
+ * `signal` is `undefined` rather than optional so that `report` can follow it.
69
74
  */
70
- export type ToolDispatch = (call: ToolCallRequest, signal?: AbortSignal) => Promise<string | ToolAnswer>;
75
+ export type ToolDispatch = (call: ToolCallRequest, signal: AbortSignal | undefined, report: ToolCallReport) => Promise<string | ToolAnswer>;
71
76
  /**
72
77
  * What running a step's calls reads of the run and writes back into it. One per run, shared by
73
78
  * every step: the sets and counts here are the run's own, not copies.
@@ -84,6 +84,46 @@ export const assistantMessage = (content, parsed) => ({
84
84
  * @returns A new array of the definitions themselves, not copies.
85
85
  */
86
86
  export const definedAs = (definitions, names) => [...names].flatMap((name) => definitions.get(name) ?? []);
87
+ /**
88
+ * A server's log message as the text of its event.
89
+ *
90
+ * @param data - What the server sent: a string, or any JSON value.
91
+ * @returns A string as it was, anything else as JSON, cut by `preview`. What JSON cannot spell is
92
+ * shown as `String` shows it.
93
+ */
94
+ function logText(data) {
95
+ if (typeof data === 'string') {
96
+ return preview(data);
97
+ }
98
+ try {
99
+ return preview(JSON.stringify(data) ?? String(data));
100
+ }
101
+ catch {
102
+ return preview(String(data));
103
+ }
104
+ }
105
+ /**
106
+ * What one call's dispatcher may report through, and the switch that closes it once it has answered.
107
+ *
108
+ * @param run - The run, whose watchers hear what is reported.
109
+ * @param id - The call's id, which every event it reports carries.
110
+ * @param name - The tool the call is shown as.
111
+ * @returns The report to hand `dispatch`, and `close`, after which it reports nothing.
112
+ */
113
+ function reportFor(run, id, name) {
114
+ let isOpen = true;
115
+ const log = ({ level, data }) => {
116
+ if (isOpen) {
117
+ run.onEvent({ kind: RunEventKind.ServerLog, id, name, level, text: logText(data) });
118
+ }
119
+ };
120
+ return {
121
+ report: { log },
122
+ close: () => {
123
+ isOpen = false;
124
+ },
125
+ };
126
+ }
87
127
  /**
88
128
  * A dispatcher's answer in the one shape, whichever it gave.
89
129
  *
@@ -293,6 +333,7 @@ async function runCall(run, prepared, answered, step) {
293
333
  const { name, raw } = request;
294
334
  run.onEvent({ kind: RunEventKind.ToolCall, id: call.id, name, text: preview(raw) });
295
335
  run.onToolCall?.(request);
336
+ const { report, close } = reportFor(run, call.id, name);
296
337
  let content;
297
338
  let links = [];
298
339
  let ok = true;
@@ -318,9 +359,11 @@ async function runCall(run, prepared, answered, step) {
318
359
  run.used.add(name);
319
360
  // Read before `once` looks, which it does before it awaits anything.
320
361
  shared = dedupable && answered.has(key);
362
+ // A repeat answered from the first call reports nothing of its own: the first call's
363
+ // report is the one its dispatcher holds.
321
364
  const answer = dedupable
322
- ? await once(answered, key, () => run.dispatch(request, signal))
323
- : await run.dispatch(request, signal);
365
+ ? await once(answered, key, () => run.dispatch(request, signal, report))
366
+ : await run.dispatch(request, signal, report);
324
367
  ({ text: content, links } = answerOf(answer));
325
368
  }
326
369
  }
@@ -331,6 +374,9 @@ async function runCall(run, prepared, answered, step) {
331
374
  content = errorMessage(error);
332
375
  ok = false;
333
376
  }
377
+ finally {
378
+ close();
379
+ }
334
380
  run.onEvent({ kind: RunEventKind.ToolResult, id: call.id, name, ok, text: preview(content) });
335
381
  const isFirstAnswer = shared === false;
336
382
  if (isFirstAnswer) {
@@ -189,6 +189,59 @@ export interface ToolAnswer {
189
189
  /** What the call produced, in the order the tool gave them. Never sent to the model. */
190
190
  links?: ResourceLink[];
191
191
  }
192
+ /**
193
+ * The levels a server's log message is sent at, quietest first: MCP's, which are syslog's.
194
+ *
195
+ * @remarks
196
+ * `runMetrics` reads `warning` and the four above it as a server complaining. A level outside
197
+ * this list is still shown, and is counted as neither.
198
+ */
199
+ export declare const ServerLogLevel: {
200
+ readonly Debug: 'debug';
201
+ readonly Info: 'info';
202
+ readonly Notice: 'notice';
203
+ readonly Warning: 'warning';
204
+ readonly Error: 'error';
205
+ readonly Critical: 'critical';
206
+ readonly Alert: 'alert';
207
+ readonly Emergency: 'emergency';
208
+ };
209
+ /** Any one of the levels in `ServerLogLevel`. */
210
+ export type ServerLogLevel = (typeof ServerLogLevel)[keyof typeof ServerLogLevel];
211
+ /**
212
+ * One log message a tool's server sent while the call was running, as `ToolCallReport.log` is
213
+ * handed it.
214
+ *
215
+ * @remarks
216
+ * The params of MCP's `notifications/message`, declared structurally rather than imported, as
217
+ * `ResourceLink` is: agent-mcp-pool's `ServerLogEntry` has these field names, so its `onLog`
218
+ * takes `report.log` as it stands. `level` is a plain string so a server's own word is shown
219
+ * rather than refused.
220
+ */
221
+ export interface ServerLogEntry {
222
+ /** How serious the server says it is: one of `ServerLogLevel`, by the spec. */
223
+ level: string;
224
+ /** The server's name for the part of itself that logged, where it gave one. */
225
+ logger?: string;
226
+ /** Whatever the server sent: a string usually, any JSON value by the spec. */
227
+ data: unknown;
228
+ }
229
+ /**
230
+ * What a dispatcher may tell the run about the call it is making, besides its answer.
231
+ *
232
+ * @remarks
233
+ * An object rather than a fourth and fifth argument, so what a call can report grows here —
234
+ * progress is the next one — without `dispatch` changing again. Every member is a bound function,
235
+ * so it can be handed on alone: `onLog: report.log`. What is reported reaches the run's events
236
+ * and never the transcript: it is for the person watching, not the model.
237
+ */
238
+ export interface ToolCallReport {
239
+ /**
240
+ * Tells watchers of a server's log message, as a `server-log` event for this call. Heard after
241
+ * the call has answered, it is dropped: the event would land after the result it explains.
242
+ */
243
+ log: (entry: ServerLogEntry) => void;
244
+ }
192
245
  /**
193
246
  * One thing a run's tool calls produced: the first link to a uri, and where it came from.
194
247
  *
@@ -472,3 +472,20 @@ export function recoverToolCalls(content, { names = [] } = {}) {
472
472
  rest += tail.slice(cursor);
473
473
  return { content: `${content.slice(0, from)}${rest}`.trim(), toolCalls };
474
474
  }
475
+ /**
476
+ * The levels a server's log message is sent at, quietest first: MCP's, which are syslog's.
477
+ *
478
+ * @remarks
479
+ * `runMetrics` reads `warning` and the four above it as a server complaining. A level outside
480
+ * this list is still shown, and is counted as neither.
481
+ */
482
+ export const ServerLogLevel = {
483
+ Debug: 'debug',
484
+ Info: 'info',
485
+ Notice: 'notice',
486
+ Warning: 'warning',
487
+ Error: 'error',
488
+ Critical: 'critical',
489
+ Alert: 'alert',
490
+ Emergency: 'emergency',
491
+ };
package/llms.txt CHANGED
@@ -406,10 +406,13 @@ Reading what a model meant by a tool call when it did not write one cleanly.
406
406
  - `ResourceLink` (type) — A handle to something a tool call made or found — a document it wrote, a file it saved.
407
407
  - `RunArtifact` (type) — One thing a run's tool calls produced: the first link to a uri, and where it came from.
408
408
  - `recoverToolCalls` — Tool calls a model wrote into its reply as text, taken out of it and made into calls.
409
+ - `ServerLogEntry` (type) — One log message a tool's server sent while the call was running, as `ToolCallReport.log` is handed it.
410
+ - `ServerLogLevel` — Any one of the levels in `ServerLogLevel`.
409
411
  - `ToolAnswer` (type) — A dispatcher's answer when it has more to say than the text: what the model reads, and what the call produced for the host.
410
412
  - `ToolArgumentsError` — Tool arguments that could not be read as an object, with why.
411
413
  - `ToolCall` (type) — A tool call as the loop handles it, recovered or streamed.
412
414
  - `ToolCallOutcome` (type) — What one tool call did, in the order the model asked.
415
+ - `ToolCallReport` (type) — What a dispatcher may tell the run about the call it is making, besides its answer.
413
416
  - `ToolCallRequest` (type) — One call the model made, as `dispatch` and `onToolCall` are handed it.
414
417
  - `ToolCallResult` (type) — One call's whole answer, as `onToolResult` is handed it.
415
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "4.9.0",
3
+ "version": "4.10.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",