@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.
- package/README.md +7 -2
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +2 -1
- package/esm/src/guardrails/error.d.ts +1 -1
- package/esm/src/guardrails/error.js +3 -1
- package/esm/src/guardrails/labels.d.ts +2 -2
- package/esm/src/guardrails/labels.js +8 -1
- package/esm/src/guardrails/mod.d.ts +1 -1
- package/esm/src/guardrails/mod.js +1 -1
- package/esm/src/guardrails/quota.d.ts +3 -1
- package/esm/src/guardrails/quota.js +6 -1
- package/esm/src/guardrails/theorem-error.d.ts +1 -1
- package/esm/src/guardrails/theorem-error.js +3 -1
- package/esm/src/host/reply.js +1 -0
- package/esm/src/kernel/engine/decision.js +1 -0
- package/esm/src/kernel/engine/runner/gates.js +5 -0
- package/esm/src/kernel/engine/runner/stages.d.ts +14 -2
- package/esm/src/kernel/engine/runner/stages.js +80 -3
- package/esm/src/kernel/engine/runner/state.d.ts +13 -0
- package/esm/src/kernel/engine/runner/state.js +2 -0
- package/esm/src/kernel/engine/runner/steps.d.ts +3 -1
- package/esm/src/kernel/engine/runner/steps.js +49 -11
- package/esm/src/kernel/interaction-parts.d.ts +13 -1
- package/esm/src/kernel/interaction-parts.js +14 -1
- package/esm/src/kernel/mod.d.ts +2 -0
- package/esm/src/kernel/mod.js +1 -0
- package/esm/src/kernel/protocols/runtime.js +2 -2
- package/esm/src/kernel/provider-runtime.js +18 -16
- package/esm/src/kernel/schema.d.ts +1 -1
- package/esm/src/kernel/schema.js +2 -1
- package/esm/src/kernel/stages.d.ts +14 -3
- package/esm/src/kernel/stages.js +24 -7
- package/esm/src/kernel/tools/memory-execution.d.ts +11 -0
- package/esm/src/kernel/tools/memory-execution.js +59 -0
- package/esm/src/kernel/tools/operation.d.ts +2 -5
- package/esm/src/kernel/tools/operation.js +5 -1
- package/esm/src/kernel/turn-events.d.ts +9 -2
- package/esm/src/kernel/turn-events.js +29 -2
- package/esm/src/observability/trace-catalog.js +2 -1
- package/esm/src/providers/gemini/interactions-protocol.js +2 -5
- package/esm/src/providers/google/api-error.d.ts +2 -0
- package/esm/src/providers/google/api-error.js +26 -18
- package/esm/src/providers/google/live/stream.js +1 -1
- package/esm/src/providers/shared/retry.d.ts +1 -0
- package/esm/src/providers/shared/retry.js +6 -7
- 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`.
|
|
528
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
94
|
-
? '
|
|
95
|
-
: typeof
|
|
96
|
-
?
|
|
97
|
-
:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (typeof
|
|
101
|
-
return new TheoremError(
|
|
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
|
|
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(
|
|
113
|
+
return new TheoremError(kindOfHttpStatus(response.status), `HTTP ${response.status}`);
|
|
111
114
|
}
|
|
112
115
|
const parsed = parseRecord(text);
|
|
113
|
-
|
|
114
|
-
|
|
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 {
|
|
@@ -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 {
|
|
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
|
-
|
|
33
|
-
|
|
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.
|
|
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",
|