@theoremjs/agents 0.3.1 → 0.3.3

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.
Files changed (46) hide show
  1. package/README.md +7 -2
  2. package/esm/mod.d.ts +3 -1
  3. package/esm/mod.js +2 -1
  4. package/esm/src/guardrails/error.d.ts +1 -1
  5. package/esm/src/guardrails/error.js +3 -1
  6. package/esm/src/guardrails/labels.d.ts +2 -2
  7. package/esm/src/guardrails/labels.js +8 -1
  8. package/esm/src/guardrails/mod.d.ts +1 -1
  9. package/esm/src/guardrails/mod.js +1 -1
  10. package/esm/src/guardrails/quota.d.ts +3 -1
  11. package/esm/src/guardrails/quota.js +6 -1
  12. package/esm/src/guardrails/theorem-error.d.ts +1 -1
  13. package/esm/src/guardrails/theorem-error.js +3 -1
  14. package/esm/src/host/reply.js +1 -0
  15. package/esm/src/kernel/engine/decision.js +1 -0
  16. package/esm/src/kernel/engine/runner/gates.js +5 -0
  17. package/esm/src/kernel/engine/runner/stages.d.ts +14 -2
  18. package/esm/src/kernel/engine/runner/stages.js +80 -3
  19. package/esm/src/kernel/engine/runner/state.d.ts +13 -0
  20. package/esm/src/kernel/engine/runner/state.js +2 -0
  21. package/esm/src/kernel/engine/runner/steps.d.ts +3 -1
  22. package/esm/src/kernel/engine/runner/steps.js +49 -11
  23. package/esm/src/kernel/interaction-parts.d.ts +13 -1
  24. package/esm/src/kernel/interaction-parts.js +14 -1
  25. package/esm/src/kernel/mod.d.ts +2 -0
  26. package/esm/src/kernel/mod.js +1 -0
  27. package/esm/src/kernel/protocols/runtime.js +2 -2
  28. package/esm/src/kernel/provider-runtime.js +18 -16
  29. package/esm/src/kernel/schema.d.ts +1 -1
  30. package/esm/src/kernel/schema.js +2 -1
  31. package/esm/src/kernel/stages.d.ts +14 -3
  32. package/esm/src/kernel/stages.js +24 -7
  33. package/esm/src/kernel/tools/memory-execution.d.ts +11 -0
  34. package/esm/src/kernel/tools/memory-execution.js +59 -0
  35. package/esm/src/kernel/tools/operation.d.ts +2 -5
  36. package/esm/src/kernel/tools/operation.js +5 -1
  37. package/esm/src/kernel/turn-events.d.ts +9 -2
  38. package/esm/src/kernel/turn-events.js +29 -2
  39. package/esm/src/observability/trace-catalog.js +2 -1
  40. package/esm/src/providers/gemini/interactions-protocol.js +2 -5
  41. package/esm/src/providers/google/api-error.d.ts +2 -0
  42. package/esm/src/providers/google/api-error.js +26 -18
  43. package/esm/src/providers/google/live/stream.js +1 -1
  44. package/esm/src/providers/shared/retry.d.ts +1 -0
  45. package/esm/src/providers/shared/retry.js +6 -7
  46. package/package.json +1 -1
@@ -464,6 +464,7 @@ const TURN_EVENTS = {
464
464
  .array(z.object({ id: z.string().regex(/\S/) }))
465
465
  .min(1)
466
466
  .optional(),
467
+ released: z.number().int().nonnegative().optional(),
467
468
  }),
468
469
  tool_draft: z.object({
469
470
  type: z.literal('tool_draft'),
@@ -522,10 +523,33 @@ function isReply(event) {
522
523
  event.type === 'structured' ||
523
524
  event.type === 'structured_draft');
524
525
  }
526
+ /**
527
+ * `reply` with only its first `released` characters of text: the text past them, and every
528
+ * media and structured output that came after, are a draft the user never got.
529
+ */
530
+ function cutReply(reply, released) {
531
+ const out = [];
532
+ let left = released;
533
+ for (const event of reply) {
534
+ if (!isReply(event))
535
+ out.push(event);
536
+ else if (event.type !== 'text') {
537
+ if (left > 0)
538
+ out.push(event);
539
+ }
540
+ else if (left > 0) {
541
+ const text = event.text.slice(0, left);
542
+ left -= text.length;
543
+ out.push(text === event.text ? event : { ...event, text });
544
+ }
545
+ }
546
+ return out;
547
+ }
525
548
  /**
526
549
  * `events` without the replies a retry threw away: each `attempt` drops the text, media and
527
- * structured output before it, back to the turn's start or its last `done`. Tool calls, thoughts
528
- * and everything else stay, since they happened.
550
+ * structured output before it, back to the turn's start or its last `done`. A `stage` event with
551
+ * `released` keeps only that much of the reply's text, and drops what followed it. Tool calls,
552
+ * thoughts and everything else stay, since they happened.
529
553
  */
530
554
  export function keptReply(events) {
531
555
  const kept = [];
@@ -535,6 +559,9 @@ export function keptReply(events) {
535
559
  const before = kept.splice(turnStart);
536
560
  kept.push(...before.filter((each) => !isReply(each)));
537
561
  }
562
+ if (event.type === 'stage' && event.released !== undefined) {
563
+ kept.push(...cutReply(kept.splice(turnStart), event.released));
564
+ }
538
565
  kept.push(event);
539
566
  if (event.type === 'done')
540
567
  turnStart = kept.length;
@@ -96,7 +96,8 @@ const ERROR_KIND_OPTIONS = {
96
96
  },
97
97
  rate_limit: { label: 'Rate limit', doc: 'Too many requests, or a quota used up.' },
98
98
  unsupported: { label: 'Unsupported', doc: 'The model or route cannot serve this request.' },
99
- unavailable: { label: 'Unavailable', doc: 'The provider is down or overloaded.' },
99
+ busy: { label: 'Busy', doc: 'The provider is up but too busy for the call.' },
100
+ unavailable: { label: 'Unavailable', doc: 'The provider is down.' },
100
101
  bad_response: {
101
102
  label: 'Unusable response',
102
103
  doc: 'The provider answered with something that cannot be used.',
@@ -1,9 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { kindOfHttpStatus, TheoremError } from '../../guardrails/error.js';
3
2
  import { asRecord } from '../../kernel/engine/record.js';
4
3
  import { jsonObjectSchema } from '../../kernel/json.js';
5
4
  import { defineProtocol, } from '../../kernel/protocols/mod.js';
6
- import { readGeminiApiError } from '../google/api-error.js';
5
+ import { readGeminiApiError, statedOrStatusError } from '../google/api-error.js';
7
6
  import { googleBuiltins } from '../google/builtins.js';
8
7
  import { thoughtSignature, toInteractionsBody, } from '../google/interactions/framing.js';
9
8
  import { finalizeStructured, foldBody, foldPayload, missingMediaError, newStreamFold, openStepEvents, } from '../google/interactions/stream.js';
@@ -68,10 +67,8 @@ export const geminiInteractions = defineProtocol({
68
67
  continuation: { version: 1, schema: state, keyOptions: ['store', 'persistViaInteractionId'] },
69
68
  reasoning: thoughtSignature,
70
69
  error(failure) {
71
- const kind = kindOfHttpStatus(failure.status);
72
70
  const record = asRecord(failure.body);
73
- const stated = record ? readGeminiApiError(record) : null;
74
- return new TheoremError(stated?.kind === 'auth' ? 'auth' : kind, stated?.message ?? `Gemini HTTP ${failure.status}`);
71
+ return statedOrStatusError(record ? readGeminiApiError(record) : null, failure.status, `Gemini HTTP ${failure.status}`);
75
72
  },
76
73
  create(context) {
77
74
  return {
@@ -6,3 +6,5 @@ import { TheoremError } from '../../guardrails/error.js';
6
6
  export declare function readGeminiApiError(record: Record<string, unknown>): TheoremError | null;
7
7
  /** The kind is the HTTP status's, unless the body names a refused key. */
8
8
  export declare function readNonOkError(response: Response): Promise<TheoremError>;
9
+ /** A non-OK reply's error: the body's own when it names a refused key, else the status's with the body's message. */
10
+ export declare function statedOrStatusError(stated: TheoremError | null, status: number, fallback: string): TheoremError;
@@ -72,13 +72,15 @@ const BLOCKED_CODES = new Set([
72
72
  * codes (`malformed_function_call`, `no_image`, …) and any code Google adds later
73
73
  * are `bad_response`.
74
74
  */
75
- function kindOfNamedCode(code) {
75
+ function namedCodeError(code, message) {
76
76
  if (code === 'cancelled')
77
- return 'cancelled';
77
+ return new TheoremError('cancelled', message);
78
78
  if (BLOCKED_CODES.has(code))
79
- return 'safety';
79
+ return new TheoremError('safety', message);
80
80
  const status = NAMED_CODE_STATUS[code];
81
- return status === undefined ? 'bad_response' : kindOfHttpStatus(status);
81
+ return status === undefined
82
+ ? new TheoremError('bad_response', message)
83
+ : new TheoremError(kindOfHttpStatus(status), message);
82
84
  }
83
85
  /**
84
86
  * Interactions (SSE `error` rows, non-OK bodies) and Live (error frames) both send
@@ -90,28 +92,34 @@ export function readGeminiApiError(record) {
90
92
  return null;
91
93
  }
92
94
  const { code, message, status, details } = error;
93
- const kind = refusesKey(details)
94
- ? 'auth'
95
- : typeof code === 'number' && code >= HTTP_STATUS_MIN && code <= HTTP_STATUS_MAX
96
- ? kindOfHttpStatus(code)
97
- : typeof code === 'string'
98
- ? kindOfNamedCode(code)
99
- : 'bad_response';
100
- if (typeof message !== 'string' || message.length === 0) {
101
- return new TheoremError(kind, 'Gemini returned an error.');
95
+ const text = typeof message !== 'string' || message.length === 0
96
+ ? 'Gemini returned an error.'
97
+ : typeof status === 'string'
98
+ ? `${status}: ${message}`
99
+ : message;
100
+ if (refusesKey(details))
101
+ return new TheoremError('auth', text);
102
+ if (typeof code === 'number' && code >= HTTP_STATUS_MIN && code <= HTTP_STATUS_MAX) {
103
+ return new TheoremError(kindOfHttpStatus(code), text);
102
104
  }
103
- return new TheoremError(kind, typeof status === 'string' ? `${status}: ${message}` : message);
105
+ return typeof code === 'string'
106
+ ? namedCodeError(code, text)
107
+ : new TheoremError('bad_response', text);
104
108
  }
105
109
  /** The kind is the HTTP status's, unless the body names a refused key. */
106
110
  export async function readNonOkError(response) {
107
- const kind = kindOfHttpStatus(response.status);
108
111
  const text = await response.text().catch(() => '');
109
112
  if (!text.trim()) {
110
- return new TheoremError(kind, `HTTP ${response.status}`);
113
+ return new TheoremError(kindOfHttpStatus(response.status), `HTTP ${response.status}`);
111
114
  }
112
115
  const parsed = parseRecord(text);
113
- const stated = parsed ? readGeminiApiError(parsed) : null;
114
- return new TheoremError(stated?.kind === 'auth' ? 'auth' : kind, stated?.message ?? `Gemini HTTP ${response.status}: ${text}`);
116
+ return statedOrStatusError(parsed ? readGeminiApiError(parsed) : null, response.status, `Gemini HTTP ${response.status}: ${text}`);
117
+ }
118
+ /** A non-OK reply's error: the body's own when it names a refused key, else the status's with the body's message. */
119
+ export function statedOrStatusError(stated, status, fallback) {
120
+ if (stated?.kind === 'auth')
121
+ return stated;
122
+ return new TheoremError(kindOfHttpStatus(status), stated?.message ?? fallback);
115
123
  }
116
124
  function parseRecord(text) {
117
125
  try {
@@ -8,7 +8,7 @@ const CLOSE_KINDS = {
8
8
  1007: 'unsupported',
9
9
  1008: 'unsupported',
10
10
  1011: 'unavailable',
11
- 1013: 'unavailable',
11
+ 1013: 'busy',
12
12
  };
13
13
  /**
14
14
  * Google refuses an over-quota Live key with code 1011 (`unavailable`) and a reason
@@ -4,6 +4,7 @@ export type Wait = (ms: number, signal?: AbortSignal | null) => Promise<void>;
4
4
  export declare const WAIT_BUDGET_MS = 5000;
5
5
  export declare const waitDefault: Wait;
6
6
  export declare function isTransientHttp(status: number): boolean;
7
+ /** A rejected send that never got a response: a network failure, by `networkError`'s rule. An abort, a timeout, or any other kind is not. */
7
8
  export declare function isTransientThrown(err: unknown): boolean;
8
9
  export declare function backoffMs(attempt: number): number;
9
10
  /** `retry-after-ms`, else `Retry-After` in seconds or as a date; undefined when absent or unreadable. */
@@ -1,12 +1,12 @@
1
- import { isAbortError, isTimeoutError } from '../../guardrails/error.js';
1
+ import { TheoremError } from '../../guardrails/error.js';
2
+ import { networkError } from './upstream-tap.js';
2
3
  const BACKOFF_MS = [1000, 2000, 4000, 8000];
3
4
  /** All the waiting one call may do between tries; a wait that would pass it sends the last answer back. */
4
5
  export const WAIT_BUDGET_MS = 5000;
5
6
  /** The most tries after the first: a `Retry-After: 0` costs no wait, so the budget alone never ends it. */
6
7
  const MAX_RETRIES = BACKOFF_MS.length;
7
8
  const MS_PER_SECOND = 1000;
8
- const TRANSIENT_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
9
- const TRANSIENT_THROWN_RE = /name resolution|dns|econnreset|econnrefused|etimedout|connection (?:refused|reset)|error sending request|network|fetch failed|temporarily unavailable|socket|503|502|504/i;
9
+ const TRANSIENT_STATUSES = new Set([408, 429, 500, 502, 503, 504, 529]);
10
10
  export const waitDefault = (ms, signal) => new Promise((resolve, reject) => {
11
11
  if (signal?.aborted) {
12
12
  reject(abortReason(signal));
@@ -28,11 +28,10 @@ function abortReason(signal) {
28
28
  export function isTransientHttp(status) {
29
29
  return TRANSIENT_STATUSES.has(status);
30
30
  }
31
+ /** A rejected send that never got a response: a network failure, by `networkError`'s rule. An abort, a timeout, or any other kind is not. */
31
32
  export function isTransientThrown(err) {
32
- if (isAbortError(err) || isTimeoutError(err)) {
33
- return false;
34
- }
35
- return TRANSIENT_THROWN_RE.test(String(err));
33
+ const failure = networkError(err);
34
+ return failure instanceof TheoremError && failure.kind === 'network';
36
35
  }
37
36
  export function backoffMs(attempt) {
38
37
  return BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/agents",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, registered tools, provider adapters, guardrails, and host-injected traces.",
5
5
  "keywords": [
6
6
  "agent",