@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
|
@@ -7,7 +7,7 @@ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
|
|
|
7
7
|
import { replyIsJudged } from '../../../guardrails/progressive-yield.js';
|
|
8
8
|
import { recordTaint } from '../../../guardrails/tool-result.js';
|
|
9
9
|
import { waitDefault } from '../../../providers/shared/retry.js';
|
|
10
|
-
import {
|
|
10
|
+
import { stepReplyMessages } from '../../interaction-parts.js';
|
|
11
11
|
import { profileTurnOutputs } from '../../registry/profile-outputs.js';
|
|
12
12
|
import { providerCompleteRequest } from '../../registry/provider-request.js';
|
|
13
13
|
import { injectWouldExceedMaxSteps } from '../../stages.js';
|
|
@@ -19,7 +19,7 @@ import { profileToolsParallel } from '../../tools/resolve.js';
|
|
|
19
19
|
import { findLast } from '../../util/find-last.js';
|
|
20
20
|
import { startToolTrace } from '../tool-trace.js';
|
|
21
21
|
import { startCallTrace } from '../turn-trace.js';
|
|
22
|
-
import { applyStageInjects } from './stages.js';
|
|
22
|
+
import { applyStageInjects, recordReply } from './stages.js';
|
|
23
23
|
import { recordStepEvent } from './state.js';
|
|
24
24
|
import { inStep, runSideBySide, stepOrder } from './step-order.js';
|
|
25
25
|
import { isWithheldOnBlock, StructuredDrafts, yieldProviderEvents, } from './stream.js';
|
|
@@ -112,10 +112,28 @@ function tallyCallEvent(event, tally, call) {
|
|
|
112
112
|
call.guardrail(event.guardrail);
|
|
113
113
|
if (event.type === 'structured')
|
|
114
114
|
tally.latestStructured = event.structured;
|
|
115
|
+
if (event.type === 'text')
|
|
116
|
+
tally.text += event.text;
|
|
115
117
|
if (REPLY_EVENTS.has(event.type))
|
|
116
118
|
tally.replied = true;
|
|
117
119
|
return false;
|
|
118
120
|
}
|
|
121
|
+
/** What sets one model call's text apart from the reply text before it: a blank line. */
|
|
122
|
+
const CALL_BREAK = '\n\n';
|
|
123
|
+
/**
|
|
124
|
+
* The call's first text, set apart from the attempt's reply before it.
|
|
125
|
+
*
|
|
126
|
+
* why: Each model call writes as if it starts the message, so a reply that goes on after tools or
|
|
127
|
+
* an inject would otherwise run its first word into the last one ("…full picture.Here's").
|
|
128
|
+
*/
|
|
129
|
+
function withCallBreak(event, tally, state, generation) {
|
|
130
|
+
if (tally.text)
|
|
131
|
+
return event;
|
|
132
|
+
const before = state.replyText;
|
|
133
|
+
const breaks = before && generation.structured === null && !/\s$/.test(before) && !/^\s/.test(event.text);
|
|
134
|
+
tally.replyAt = before.length + (breaks ? CALL_BREAK.length : 0);
|
|
135
|
+
return breaks ? { ...event, text: `${CALL_BREAK}${event.text}` } : event;
|
|
136
|
+
}
|
|
119
137
|
/** Records an event the call gave. True when the host gets it now. */
|
|
120
138
|
function releasesNow(event, control, state, buffer) {
|
|
121
139
|
recordStepEvent(event, state);
|
|
@@ -125,8 +143,10 @@ function releasesNow(event, control, state, buffer) {
|
|
|
125
143
|
state.withheldVisible = true;
|
|
126
144
|
return false;
|
|
127
145
|
}
|
|
128
|
-
if (event.type === 'text')
|
|
146
|
+
if (event.type === 'text') {
|
|
129
147
|
state.released += event.text;
|
|
148
|
+
state.replyText += event.text;
|
|
149
|
+
}
|
|
130
150
|
// why: Text and media stream via progressive-yield under egress, thoughts
|
|
131
151
|
// stream unguarded; holdLate only buffers non-visible events (e.g.
|
|
132
152
|
// structured) for validation.
|
|
@@ -135,6 +155,7 @@ function releasesNow(event, control, state, buffer) {
|
|
|
135
155
|
}
|
|
136
156
|
// why: Every other kind is the provider's answer to the request itself (it refused a part, a key, a setting); the same call sent again gets the same answer.
|
|
137
157
|
const PASSING_ERROR_KINDS = new Set([
|
|
158
|
+
'busy',
|
|
138
159
|
'unavailable',
|
|
139
160
|
'bad_response',
|
|
140
161
|
'network',
|
|
@@ -174,7 +195,7 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
174
195
|
}) {
|
|
175
196
|
const { profile, generation, system, provider, signal } = args;
|
|
176
197
|
const { usage, genForStep, request, call } = startProviderCall(generation, system, state, isAnswerOnlyStep(profile, generation, state.stepCount));
|
|
177
|
-
const tally = { calls: [], reasoning: [], failures: [], replied: false };
|
|
198
|
+
const tally = { calls: [], reasoning: [], text: '', failures: [], replied: false };
|
|
178
199
|
const control = {
|
|
179
200
|
withholdVisible: false,
|
|
180
201
|
...(state.canaryCarry ? { canaryCarry: state.canaryCarry } : {}),
|
|
@@ -219,11 +240,13 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
219
240
|
yield event;
|
|
220
241
|
continue;
|
|
221
242
|
}
|
|
243
|
+
// why: The model's history keeps what it wrote; only the reply the host gets is set apart.
|
|
244
|
+
const next = event.type === 'text' ? withCallBreak(event, tally, state, genForStep) : event;
|
|
222
245
|
if (tallyCallEvent(event, tally, call))
|
|
223
246
|
continue;
|
|
224
|
-
if (!releasesNow(
|
|
247
|
+
if (!releasesNow(next, control, state, buffer))
|
|
225
248
|
continue;
|
|
226
|
-
yield* shown.show(
|
|
249
|
+
yield* shown.show(next);
|
|
227
250
|
}
|
|
228
251
|
}
|
|
229
252
|
catch (err) {
|
|
@@ -249,7 +272,9 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
249
272
|
return {
|
|
250
273
|
calls: tally.calls,
|
|
251
274
|
reasoning: tally.reasoning,
|
|
275
|
+
text: tally.text,
|
|
252
276
|
latestStructured: tally.latestStructured,
|
|
277
|
+
...(tally.replyAt === undefined ? {} : { replyAt: tally.replyAt }),
|
|
253
278
|
...(resume ? { resume } : {}),
|
|
254
279
|
};
|
|
255
280
|
}
|
|
@@ -356,6 +381,14 @@ function* applyToolSettlement(settlement, state, call) {
|
|
|
356
381
|
}
|
|
357
382
|
return 'continue';
|
|
358
383
|
}
|
|
384
|
+
/** A step's reply as the model reads it back: its text, then its calls with the reasoning before them. */
|
|
385
|
+
function stepReply(outcome) {
|
|
386
|
+
return stepReplyMessages({
|
|
387
|
+
text: outcome.text,
|
|
388
|
+
structured: outcome.latestStructured,
|
|
389
|
+
calls: outcome.calls.map((call, index) => index === 0 ? { ...call, reasoning: outcome.reasoning } : call),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
359
392
|
/**
|
|
360
393
|
* Run the model's calls. Each reaches the host as its raw call first, all of them before
|
|
361
394
|
* any runs; their phases then arrive as each tool gives them. With `tools.parallel` they
|
|
@@ -370,10 +403,10 @@ function* applyToolSettlement(settlement, state, call) {
|
|
|
370
403
|
* After the step, any gate stops the turn. A call a stage cancelled stops the turn too,
|
|
371
404
|
* once every result that did come back is recorded.
|
|
372
405
|
*/
|
|
373
|
-
async function* handleModelCalls(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
406
|
+
async function* handleModelCalls(outcome, generation, profile, state, system, { onStage, signal, credentials, resolveHost } = {}) {
|
|
407
|
+
const { calls: upstreamCalls, reasoning } = outcome;
|
|
408
|
+
if (upstreamCalls.length > 0)
|
|
409
|
+
recordReply(state, stepReply(outcome), outcome.replyAt);
|
|
377
410
|
const stepId = crypto.randomUUID();
|
|
378
411
|
const leakScope = {
|
|
379
412
|
canary: generation.canary,
|
|
@@ -485,6 +518,9 @@ async function* handleModelCalls({ calls: upstreamCalls, reasoning }, generation
|
|
|
485
518
|
async function* executeAttempt(args) {
|
|
486
519
|
const { profile, generation, system, provider, state } = args;
|
|
487
520
|
let latestStructured;
|
|
521
|
+
// why: The answer that ends the attempt is not in turn history; a `before_end` inject puts it there first.
|
|
522
|
+
let reply = [];
|
|
523
|
+
let replyAt;
|
|
488
524
|
// why: Text streams via progressive-yield under egress, thoughts unguarded; validation and egress
|
|
489
525
|
// both hold non-visible events (structured) until the attempt gate, so a policy
|
|
490
526
|
// sees the structured payload before any of it reaches the host.
|
|
@@ -505,6 +541,8 @@ async function* executeAttempt(args) {
|
|
|
505
541
|
if (stepResult.latestStructured !== undefined) {
|
|
506
542
|
latestStructured = stepResult.latestStructured;
|
|
507
543
|
}
|
|
544
|
+
reply = stepResult.calls.length > 0 ? [] : stepReply(stepResult);
|
|
545
|
+
replyAt = stepResult.replyAt;
|
|
508
546
|
const executed = yield* handleModelCalls(stepResult, generation, profile, state, system, args.safe);
|
|
509
547
|
if (!executed) {
|
|
510
548
|
break;
|
|
@@ -514,6 +552,6 @@ async function* executeAttempt(args) {
|
|
|
514
552
|
state.lastStop = { kind: 'step_limit' };
|
|
515
553
|
}
|
|
516
554
|
}
|
|
517
|
-
return { latestStructured };
|
|
555
|
+
return { latestStructured, reply, ...(replyAt === undefined ? {} : { replyAt }) };
|
|
518
556
|
}
|
|
519
557
|
export { executeAttempt };
|
|
@@ -9,5 +9,17 @@ export declare function isMediaRefPart(part: InteractionPart): part is Interacti
|
|
|
9
9
|
* Neither field replaces the other.
|
|
10
10
|
*/
|
|
11
11
|
export declare function historyMessageParts(msg: TurnHistoryMessage): InteractionPart[];
|
|
12
|
+
/**
|
|
13
|
+
* One model call's reply as the model reads it back later in its turn: what it
|
|
14
|
+
* wrote (its structured reply, when it gave one), then the calls it made. Turn
|
|
15
|
+
* history and a provider checkpoint both build it here, so the checkpoint
|
|
16
|
+
* covers exactly the history the turn's next call sends.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stepReplyMessages(reply: {
|
|
19
|
+
text: string;
|
|
20
|
+
structured?: unknown;
|
|
21
|
+
calls: Parameters<typeof historyToolCalls>[0];
|
|
22
|
+
}): TurnHistoryMessage[];
|
|
12
23
|
/** One step's calls as the assistant message that made them, with the reasoning written before them. */
|
|
13
|
-
|
|
24
|
+
declare function historyToolCalls(calls: readonly Pick<import('./turn-events.js').ToolCallRequest, 'callId' | 'name' | 'arguments' | 'reasoning'>[]): TurnHistoryMessage;
|
|
25
|
+
export {};
|
|
@@ -21,8 +21,21 @@ export function historyMessageParts(msg) {
|
|
|
21
21
|
const parts = msg.parts ?? [];
|
|
22
22
|
return msg.content ? [{ type: 'text', text: msg.content }, ...parts] : [...parts];
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* One model call's reply as the model reads it back later in its turn: what it
|
|
26
|
+
* wrote (its structured reply, when it gave one), then the calls it made. Turn
|
|
27
|
+
* history and a provider checkpoint both build it here, so the checkpoint
|
|
28
|
+
* covers exactly the history the turn's next call sends.
|
|
29
|
+
*/
|
|
30
|
+
export function stepReplyMessages(reply) {
|
|
31
|
+
const content = reply.structured !== undefined ? JSON.stringify(reply.structured) : reply.text;
|
|
32
|
+
return [
|
|
33
|
+
...(content ? [{ role: 'assistant', content }] : []),
|
|
34
|
+
...(reply.calls.length ? [historyToolCalls(reply.calls)] : []),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
24
37
|
/** One step's calls as the assistant message that made them, with the reasoning written before them. */
|
|
25
|
-
|
|
38
|
+
function historyToolCalls(calls) {
|
|
26
39
|
const reasoning = calls.flatMap((call) => call.reasoning ?? []);
|
|
27
40
|
const tool_calls = [];
|
|
28
41
|
for (const call of calls) {
|
package/esm/src/kernel/mod.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export { systemPromptProblem } from './system-parts.js';
|
|
|
60
60
|
export { activityLabelProblem } from './tools/activity-label.js';
|
|
61
61
|
export { agentToolInputFor, agentToolOutput } from './tools/agent.js';
|
|
62
62
|
export { type AnsweredGate, answerGatedCall, GATE_DECISIONS, type GateAnswerRequest, type GateDecision, gateExpired, type HeldGatedCall, resolveGateTtlMs, sessionPermissionsAfterApproval, type ToolGateAuth, } from './tools/gate-answer.js';
|
|
63
|
+
export type { MemoryToolExecutionOptions } from './tools/memory-execution.js';
|
|
64
|
+
export { createMemoryToolExecution } from './tools/memory-execution.js';
|
|
63
65
|
export type { CredentialQuery, McpProtocolVersion, McpRpcResponse, ToolRegistry, } from './tools/mod.js';
|
|
64
66
|
export { askUserTool, authorizeToolReplay, coerceToolResultParts, createToolRegistry, credentialPlacement, executeHttpTool, executeMcpTool, executeRegisteredTool, formatToolResult, isUnsupportedMcpProtocolError, leanToolResultData, MCP_PROTOCOL_VERSIONS, parseMcpRpcResponse, prepareTurnToolSnapshot, projectForModel, registerHarnessTools, resolveToolAuth, withCredentialQuery, } from './tools/mod.js';
|
|
65
67
|
export type { BeginToolOperationResult, PageToolDispatch, PageToolExecution, SavedToolInvocation, TerminalToolEvent, ToolExecutionCoordinator, ToolOperationClaim, ToolOperationIdentity, ToolOperationOutcome, ToolOperationUncertainty, } from './tools/operation.js';
|
package/esm/src/kernel/mod.js
CHANGED
|
@@ -36,6 +36,7 @@ export { systemPromptProblem } from './system-parts.js';
|
|
|
36
36
|
export { activityLabelProblem } from './tools/activity-label.js';
|
|
37
37
|
export { agentToolInputFor, agentToolOutput } from './tools/agent.js';
|
|
38
38
|
export { answerGatedCall, GATE_DECISIONS, gateExpired, resolveGateTtlMs, sessionPermissionsAfterApproval, } from './tools/gate-answer.js';
|
|
39
|
+
export { createMemoryToolExecution } from './tools/memory-execution.js';
|
|
39
40
|
export { askUserTool, authorizeToolReplay, coerceToolResultParts, createToolRegistry, credentialPlacement, executeHttpTool, executeMcpTool, executeRegisteredTool, formatToolResult, isUnsupportedMcpProtocolError, leanToolResultData, MCP_PROTOCOL_VERSIONS, parseMcpRpcResponse, prepareTurnToolSnapshot, projectForModel, registerHarnessTools, resolveToolAuth, withCredentialQuery, } from './tools/mod.js';
|
|
40
41
|
export { savedToolInvocationSchema, toolInvocationHash, toolOperationOutcomeSchema, } from './tools/operation.js';
|
|
41
42
|
export { jsonSchemaFromZod } from './tools/schema.js';
|
|
@@ -309,7 +309,7 @@ function isKind(error, kind) {
|
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
311
311
|
* Runs one call with the kernel's retries, all before the first event: once on the fallback slot
|
|
312
|
-
* when the first key is out of quota, and once after an `auth` refusal with the minted token
|
|
312
|
+
* when the first key is out of quota or its model is busy, and once after an `auth` refusal with the minted token
|
|
313
313
|
* dropped. A fallback starts clean: it may be another account, so it never sends the first key's
|
|
314
314
|
* state.
|
|
315
315
|
*/
|
|
@@ -330,7 +330,7 @@ async function* withKeys(setup, run) {
|
|
|
330
330
|
catch (error) {
|
|
331
331
|
if (started)
|
|
332
332
|
throw error;
|
|
333
|
-
if (isKind(error, 'rate_limit') &&
|
|
333
|
+
if ((isKind(error, 'rate_limit') || isKind(error, 'busy')) &&
|
|
334
334
|
fallback &&
|
|
335
335
|
slot !== fallback &&
|
|
336
336
|
holdsKey(setup, fallback)) {
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { isAbortError, TheoremError, throwIfAborted, } from '../guardrails/error.js';
|
|
3
3
|
import { labelText } from '../guardrails/labels.js';
|
|
4
4
|
import { historyHash, providerCheckpointSchema, } from './checkpoint.js';
|
|
5
|
-
import {
|
|
5
|
+
import { stepReplyMessages } from './interaction-parts.js';
|
|
6
6
|
import { jsonObjectSchema, jsonValueSchema } from './json.js';
|
|
7
7
|
import { keySlotsOf, protocolOf, } from './protocols/binding.js';
|
|
8
8
|
import { resolveProviderLimits } from './protocols/limits.js';
|
|
@@ -409,12 +409,7 @@ async function* consumeModelStep(events, signal) {
|
|
|
409
409
|
const pending = [];
|
|
410
410
|
const reasoning = [];
|
|
411
411
|
let text = '';
|
|
412
|
-
|
|
413
|
-
const flushText = () => {
|
|
414
|
-
if (text)
|
|
415
|
-
outputs.push({ role: 'assistant', content: text });
|
|
416
|
-
text = '';
|
|
417
|
-
};
|
|
412
|
+
let structured;
|
|
418
413
|
const iterator = events[Symbol.asyncIterator]();
|
|
419
414
|
let exhausted = false;
|
|
420
415
|
try {
|
|
@@ -435,8 +430,7 @@ async function* consumeModelStep(events, signal) {
|
|
|
435
430
|
text += event.text;
|
|
436
431
|
break;
|
|
437
432
|
case 'structured':
|
|
438
|
-
|
|
439
|
-
outputs.push({ role: 'assistant', content: JSON.stringify(event.structured) });
|
|
433
|
+
structured = event.structured;
|
|
440
434
|
break;
|
|
441
435
|
case 'reasoning': {
|
|
442
436
|
const { type: _type, ...part } = event;
|
|
@@ -446,7 +440,6 @@ async function* consumeModelStep(events, signal) {
|
|
|
446
440
|
case 'tool_call':
|
|
447
441
|
if (pending.some((call) => call.callId === event.call.callId))
|
|
448
442
|
throw new TheoremError('bad_response', labelText('provider.duplicate_call'));
|
|
449
|
-
flushText();
|
|
450
443
|
if (event.call.wireCallId)
|
|
451
444
|
wireCallIds[event.call.callId] = event.call.wireCallId;
|
|
452
445
|
pending.push(kernelCall(event.call));
|
|
@@ -467,8 +460,15 @@ async function* consumeModelStep(events, signal) {
|
|
|
467
460
|
if (!exhausted && iterator.return)
|
|
468
461
|
void Promise.resolve(iterator.return()).catch(() => undefined);
|
|
469
462
|
}
|
|
470
|
-
|
|
471
|
-
|
|
463
|
+
return {
|
|
464
|
+
terminal,
|
|
465
|
+
failed,
|
|
466
|
+
pending,
|
|
467
|
+
reasoning,
|
|
468
|
+
text,
|
|
469
|
+
...(structured !== undefined ? { structured } : {}),
|
|
470
|
+
wireCallIds,
|
|
471
|
+
};
|
|
472
472
|
}
|
|
473
473
|
/**
|
|
474
474
|
* A profile's model as the engine calls it: each step one guarded protocol call, with the
|
|
@@ -489,7 +489,7 @@ export function registeredTurnModel(provider, binding, profileType, host, initia
|
|
|
489
489
|
yield { type: 'provider_warning', warning: restored.warning };
|
|
490
490
|
}
|
|
491
491
|
const call = guardedCall(provider, binding, profileType, host, req);
|
|
492
|
-
const { terminal, failed, pending, reasoning,
|
|
492
|
+
const { terminal, failed, pending, reasoning, text, structured, wireCallIds } = yield* consumeModelStep(call.complete(protocolRequest(req, protocol, restored)), req.signal);
|
|
493
493
|
throwIfAborted(req.signal);
|
|
494
494
|
if (!terminal) {
|
|
495
495
|
yield { type: 'done', stop: { kind: failed ? 'provider_error' : 'stream_incomplete' } };
|
|
@@ -501,8 +501,6 @@ export function registeredTurnModel(provider, binding, profileType, host, initia
|
|
|
501
501
|
yield { type: 'done', stop: terminal.stop };
|
|
502
502
|
return;
|
|
503
503
|
}
|
|
504
|
-
if (pending.length)
|
|
505
|
-
outputs.push(historyToolCalls(pending.map((entry, index) => (index === 0 ? { ...entry, reasoning } : entry))));
|
|
506
504
|
const history = [...(req.history ?? [])];
|
|
507
505
|
if (req.input.length)
|
|
508
506
|
history.push(req.input.every((part) => part.type === 'text')
|
|
@@ -513,7 +511,11 @@ export function registeredTurnModel(provider, binding, profileType, host, initia
|
|
|
513
511
|
: { role: 'user', parts: req.input });
|
|
514
512
|
const checkpoint = await checkpointProviderState(provider, binding, call.keySlot(), terminal.state, [
|
|
515
513
|
...history,
|
|
516
|
-
...(
|
|
514
|
+
...stepReplyMessages({
|
|
515
|
+
text,
|
|
516
|
+
structured,
|
|
517
|
+
calls: pending.map((entry, index) => (index === 0 ? { ...entry, reasoning } : entry)),
|
|
518
|
+
}),
|
|
517
519
|
], { ...restored.wireCallIds, ...wireCallIds });
|
|
518
520
|
active = checkpoint;
|
|
519
521
|
for (const entry of pending)
|
|
@@ -126,7 +126,7 @@ export declare const AWAITING_USER_INPUT_KINDS: readonly ["confirm", "choice", "
|
|
|
126
126
|
/** One of {@linkcode AWAITING_USER_INPUT_KINDS}. */
|
|
127
127
|
export type AwaitingUserInputKind = (typeof AWAITING_USER_INPUT_KINDS)[number];
|
|
128
128
|
/** Why a stage handler's result was partly or wholly ignored. */
|
|
129
|
-
export declare const STAGE_APPLY_WARNING_CODES: readonly ["affordance_not_allowed", "inject_not_allowed", "inject_rejected_max_steps", "inject_invalid_messages", "inject_id_invalid", "deny_invalid", "confirm_invalid", "mutate_invalid", "label_failed_invalid", "abort_invalid", "wait_for_invalid", "unknown_field", "result_invalid"];
|
|
129
|
+
export declare const STAGE_APPLY_WARNING_CODES: readonly ["affordance_not_allowed", "inject_not_allowed", "inject_rejected_max_steps", "inject_invalid_messages", "inject_id_invalid", "inject_released_invalid", "deny_invalid", "confirm_invalid", "mutate_invalid", "label_failed_invalid", "abort_invalid", "wait_for_invalid", "unknown_field", "result_invalid"];
|
|
130
130
|
/** One of {@linkcode STAGE_APPLY_WARNING_CODES}. */
|
|
131
131
|
export type StageApplyWarningCode = (typeof STAGE_APPLY_WARNING_CODES)[number];
|
|
132
132
|
/** The `status` of a tool output that asks the user a question. */
|
package/esm/src/kernel/schema.js
CHANGED
|
@@ -140,6 +140,7 @@ export const STAGE_APPLY_WARNING_CODES = [
|
|
|
140
140
|
'inject_rejected_max_steps',
|
|
141
141
|
'inject_invalid_messages',
|
|
142
142
|
'inject_id_invalid',
|
|
143
|
+
'inject_released_invalid',
|
|
143
144
|
'deny_invalid',
|
|
144
145
|
'confirm_invalid',
|
|
145
146
|
'mutate_invalid',
|
|
@@ -513,7 +514,7 @@ export const PROFILE_FIELDS = withScopeAndPresence({
|
|
|
513
514
|
'models.*.protocol': field('string', 'The API shape this model is spoken to in.'),
|
|
514
515
|
'models.*.provider': field('string', 'The service that runs the model or routes requests to one, such as Gemini or OpenRouter. Its registration supplies the endpoint and authentication.'),
|
|
515
516
|
'models.*.keySlot': field('KeySlot', "The key slot this model's calls are signed with, instead of the provider's. Use it to bill one model to a different key. Absent: the provider's primary slot."),
|
|
516
|
-
'models.*.fallbackKeySlot': field('KeySlot', "The key slot tried once after a rate limit on the primary slot, instead of the provider's fallback. Must differ from the primary slot. Absent: the provider's fallback slot, if it has one."),
|
|
517
|
+
'models.*.fallbackKeySlot': field('KeySlot', "The key slot tried once after a rate limit or a busy model on the primary slot, instead of the provider's fallback. Must differ from the primary slot. Absent: the provider's fallback slot, if it has one."),
|
|
517
518
|
'models.*.options': field('JsonObject', "Settings only this model's protocol has, such as a task type or vector size. Each is checked when the profile loads. Any left out keep the provider's default."),
|
|
518
519
|
providerContinuation: field('ProviderContinuationPolicy', 'Who holds the conversation a saved provider state continues.'),
|
|
519
520
|
'providerContinuation.history': field("'host' | 'stateless' | 'provider'", 'Who holds the conversation a checkpoint continues: the host passes history back, the provider keeps it and the checkpoint travels alone, or the profile is stateless and every request stands alone.', ['host', 'stateless', 'provider']),
|
|
@@ -55,6 +55,13 @@ export interface StageResult {
|
|
|
55
55
|
* non-string id refuses the inject whole.
|
|
56
56
|
*/
|
|
57
57
|
injectId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* With `inject`, at `post_tool` or `before_end`: how many characters of the attempt's reply text,
|
|
60
|
+
* as the turn gave it, the user already has. The rest is a draft they never got: it leaves the
|
|
61
|
+
* turn's history and its reply when the inject lands, so the model answers everything afresh
|
|
62
|
+
* after the part they have. Left out, they have all of it.
|
|
63
|
+
*/
|
|
64
|
+
released?: number;
|
|
58
65
|
abort?: boolean | {
|
|
59
66
|
reason?: string;
|
|
60
67
|
};
|
|
@@ -105,6 +112,8 @@ export interface StageApplyInput {
|
|
|
105
112
|
export interface InjectUnit {
|
|
106
113
|
id?: string;
|
|
107
114
|
messages: TurnHistoryMessage[];
|
|
115
|
+
/** {@linkcode StageResult.released}: the reply text the user has when this inject lands. */
|
|
116
|
+
released?: number;
|
|
108
117
|
}
|
|
109
118
|
/** A stage handler's result after validation: the parts the turn will act on. */
|
|
110
119
|
export interface StageApplyOutput {
|
|
@@ -139,16 +148,18 @@ export type StageEventExtra = {
|
|
|
139
148
|
injected?: {
|
|
140
149
|
id: string;
|
|
141
150
|
}[];
|
|
151
|
+
released?: number;
|
|
142
152
|
};
|
|
143
153
|
/** Build a stream `stage` event. Unknown extra keys are not copied. */
|
|
144
154
|
export declare function stageEventFields(stage: TurnStage, extra?: Partial<StageEventExtra>): TurnEventOf<'stage'>;
|
|
145
155
|
export declare function injectMessages(units: readonly InjectUnit[]): TurnHistoryMessage[];
|
|
146
156
|
/**
|
|
147
157
|
* The `stage` event recording that `units` landed, naming the ones the host
|
|
148
|
-
* named
|
|
149
|
-
*
|
|
158
|
+
* named, and the reply text the user has when they dropped a draft (`released`);
|
|
159
|
+
* `undefined` when there is neither. Emit it where the messages land, never
|
|
160
|
+
* before: an inject that never lands is never reported.
|
|
150
161
|
*/
|
|
151
|
-
export declare function injectedStageEvent(stage: TurnStage, units: readonly InjectUnit[], extra?: Pick<StageEventExtra, 'callId' | 'toolName'>): TurnEventOf<'stage'> | undefined;
|
|
162
|
+
export declare function injectedStageEvent(stage: TurnStage, units: readonly InjectUnit[], extra?: Pick<StageEventExtra, 'callId' | 'toolName' | 'released'>): TurnEventOf<'stage'> | undefined;
|
|
152
163
|
export declare function injectWouldExceedMaxSteps(stepCount: number, maxSteps: number | undefined): boolean;
|
|
153
164
|
export interface RunStageArgs extends StageCallBag {
|
|
154
165
|
stage: TurnStage;
|
package/esm/src/kernel/stages.js
CHANGED
|
@@ -26,7 +26,7 @@ export const STAGE_AFFORDANCE_MATRIX = Object.freeze({
|
|
|
26
26
|
before_end: Object.freeze(['inject', 'abort']),
|
|
27
27
|
post_turn: Object.freeze([]),
|
|
28
28
|
});
|
|
29
|
-
const STAGE_RESULT_KEYS = new Set([...STAGE_AFFORDANCES, 'injectId']);
|
|
29
|
+
const STAGE_RESULT_KEYS = new Set([...STAGE_AFFORDANCES, 'injectId', 'released']);
|
|
30
30
|
/** True when the affordance is physically allowed at `stage` (ignores inject gate). */
|
|
31
31
|
export function stageAllowsAffordance(stage, affordance) {
|
|
32
32
|
return STAGE_AFFORDANCE_MATRIX[stage].includes(affordance);
|
|
@@ -102,15 +102,26 @@ function coerceHistoryMessages(raw, warnings) {
|
|
|
102
102
|
}
|
|
103
103
|
return out.length > 0 ? out : undefined;
|
|
104
104
|
}
|
|
105
|
+
/** `released` as a count of characters; anything else is a warning, and the user has the whole reply. */
|
|
106
|
+
function injectReleased(raw, warnings) {
|
|
107
|
+
if (raw === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 0)
|
|
110
|
+
return raw;
|
|
111
|
+
warn(warnings, 'inject_released_invalid', 'released', 'released must be a whole number of characters, 0 or more; ignored');
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
105
114
|
/** The inject's messages under its id; an unusable id refuses the inject whole. */
|
|
106
115
|
function injectUnit(result, warnings) {
|
|
107
116
|
const messages = coerceHistoryMessages(result.inject, warnings);
|
|
108
117
|
if (!messages)
|
|
109
118
|
return undefined;
|
|
119
|
+
const released = injectReleased(result.released, warnings);
|
|
120
|
+
const unit = { messages, ...(released === undefined ? {} : { released }) };
|
|
110
121
|
if (result.injectId === undefined)
|
|
111
|
-
return
|
|
122
|
+
return unit;
|
|
112
123
|
if (typeof result.injectId === 'string' && result.injectId.trim()) {
|
|
113
|
-
return { id: result.injectId,
|
|
124
|
+
return { id: result.injectId, ...unit };
|
|
114
125
|
}
|
|
115
126
|
warn(warnings, 'inject_id_invalid', 'injectId', 'injectId must be a non-empty string; inject refused');
|
|
116
127
|
return undefined;
|
|
@@ -120,6 +131,9 @@ function applyInjectField(result, input, out) {
|
|
|
120
131
|
if (result.injectId !== undefined) {
|
|
121
132
|
warn(out.warnings, 'inject_id_invalid', 'injectId', 'injectId without inject ignored'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
122
133
|
}
|
|
134
|
+
if (result.released !== undefined) {
|
|
135
|
+
warn(out.warnings, 'inject_released_invalid', 'released', 'released without inject ignored'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
136
|
+
}
|
|
123
137
|
return;
|
|
124
138
|
}
|
|
125
139
|
const { stage, injectAllowed, injectWouldExceedMaxSteps } = input;
|
|
@@ -305,6 +319,8 @@ export function stageEventFields(stage, extra) {
|
|
|
305
319
|
event.stop = extra.stop;
|
|
306
320
|
if (extra.injected !== undefined)
|
|
307
321
|
event.injected = extra.injected;
|
|
322
|
+
if (extra.released !== undefined)
|
|
323
|
+
event.released = extra.released;
|
|
308
324
|
return event;
|
|
309
325
|
}
|
|
310
326
|
export function injectMessages(units) {
|
|
@@ -312,14 +328,15 @@ export function injectMessages(units) {
|
|
|
312
328
|
}
|
|
313
329
|
/**
|
|
314
330
|
* The `stage` event recording that `units` landed, naming the ones the host
|
|
315
|
-
* named
|
|
316
|
-
*
|
|
331
|
+
* named, and the reply text the user has when they dropped a draft (`released`);
|
|
332
|
+
* `undefined` when there is neither. Emit it where the messages land, never
|
|
333
|
+
* before: an inject that never lands is never reported.
|
|
317
334
|
*/
|
|
318
335
|
export function injectedStageEvent(stage, units, extra) {
|
|
319
336
|
const injected = units.flatMap((unit) => (unit.id === undefined ? [] : [{ id: unit.id }]));
|
|
320
|
-
if (injected.length === 0)
|
|
337
|
+
if (injected.length === 0 && extra?.released === undefined)
|
|
321
338
|
return undefined;
|
|
322
|
-
return stageEventFields(stage, { ...extra, injected });
|
|
339
|
+
return stageEventFields(stage, { ...extra, ...(injected.length > 0 ? { injected } : {}) });
|
|
323
340
|
}
|
|
324
341
|
export function injectWouldExceedMaxSteps(stepCount, maxSteps) {
|
|
325
342
|
if (maxSteps === undefined || maxSteps <= 0)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ToolExecutionCoordinator } from './operation.js';
|
|
2
|
+
export interface MemoryToolExecutionOptions {
|
|
3
|
+
/** The most operations it holds. A full coordinator refuses dispatch and keeps what it has. */
|
|
4
|
+
maxOperations?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A coordinator that keeps claims and outcomes in process memory.
|
|
8
|
+
* Use one per turn or per process when nothing needs to resume after a restart.
|
|
9
|
+
* A host that resumes across requests or workers supplies its own `ToolExecutionCoordinator`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createMemoryToolExecution(options?: MemoryToolExecutionOptions): ToolExecutionCoordinator;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { operationUnavailable, } from './operation.js';
|
|
2
|
+
/** How many operations a memory coordinator tracks before it refuses new ones. */
|
|
3
|
+
const DEFAULT_MEMORY_EXECUTION_LIMIT = 256;
|
|
4
|
+
/**
|
|
5
|
+
* A coordinator that keeps claims and outcomes in process memory.
|
|
6
|
+
* Use one per turn or per process when nothing needs to resume after a restart.
|
|
7
|
+
* A host that resumes across requests or workers supplies its own `ToolExecutionCoordinator`.
|
|
8
|
+
*/
|
|
9
|
+
export function createMemoryToolExecution(options = {}) {
|
|
10
|
+
const limit = options.maxOperations ?? DEFAULT_MEMORY_EXECUTION_LIMIT;
|
|
11
|
+
const records = new Map();
|
|
12
|
+
// invariant: callId and operationId never contain a NUL byte, so the joined key is unambiguous.
|
|
13
|
+
const keyOf = (operation) => `${operation.callId}\0${operation.operationId}`;
|
|
14
|
+
const known = (operation) => {
|
|
15
|
+
const record = records.get(keyOf(operation));
|
|
16
|
+
if (!record)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (record.state === 'settled')
|
|
19
|
+
return { kind: 'settled', outcome: record.outcome };
|
|
20
|
+
return { kind: record.state };
|
|
21
|
+
};
|
|
22
|
+
const holds = (claim) => {
|
|
23
|
+
const record = records.get(keyOf(claim));
|
|
24
|
+
return record?.state === 'running' && record.claimToken === claim.claimToken;
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
lookup: (operation) => Promise.resolve(known(operation)),
|
|
28
|
+
begin: (operation) => {
|
|
29
|
+
const earlier = known(operation);
|
|
30
|
+
if (earlier)
|
|
31
|
+
return Promise.resolve(earlier);
|
|
32
|
+
if (records.size >= limit)
|
|
33
|
+
return Promise.reject(operationUnavailable());
|
|
34
|
+
const claimToken = crypto.randomUUID();
|
|
35
|
+
records.set(keyOf(operation), { state: 'running', claimToken });
|
|
36
|
+
return Promise.resolve({ kind: 'acquired', claimToken });
|
|
37
|
+
},
|
|
38
|
+
settle: (claim, outcome) => {
|
|
39
|
+
if (!holds(claim))
|
|
40
|
+
return Promise.reject(operationUnavailable());
|
|
41
|
+
records.set(keyOf(claim), { state: 'settled', outcome });
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
},
|
|
44
|
+
settleNotStarted: (operation, outcome) => {
|
|
45
|
+
if (records.has(keyOf(operation)))
|
|
46
|
+
return Promise.reject(operationUnavailable());
|
|
47
|
+
if (records.size >= limit)
|
|
48
|
+
return Promise.reject(operationUnavailable());
|
|
49
|
+
records.set(keyOf(operation), { state: 'settled', outcome });
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
},
|
|
52
|
+
markUncertain: (claim) => {
|
|
53
|
+
if (!holds(claim))
|
|
54
|
+
return Promise.reject(operationUnavailable());
|
|
55
|
+
records.set(keyOf(claim), { state: 'uncertain' });
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TheoremError } from '../../guardrails/error.js';
|
|
3
3
|
import { type JsonObject, type JsonValue } from '../json.js';
|
|
4
|
+
import type { InjectUnit } from '../stages.js';
|
|
4
5
|
import type { GateReference, TurnEventOf } from '../turn-events.js';
|
|
5
|
-
import type { TurnHistoryMessage } from '../types.js';
|
|
6
6
|
import type { ModelToolResult, PageAnswer, ToolFailure } from './types.js';
|
|
7
7
|
export type TerminalToolEvent = TurnEventOf<'tool'> & {
|
|
8
8
|
tool: Extract<TurnEventOf<'tool'>['tool'], {
|
|
@@ -43,10 +43,7 @@ export interface ToolOperationOutcome {
|
|
|
43
43
|
aborted?: boolean | {
|
|
44
44
|
reason?: string;
|
|
45
45
|
};
|
|
46
|
-
pendingInject?:
|
|
47
|
-
id?: string;
|
|
48
|
-
messages: TurnHistoryMessage[];
|
|
49
|
-
}[];
|
|
46
|
+
pendingInject?: InjectUnit[];
|
|
50
47
|
};
|
|
51
48
|
}
|
|
52
49
|
export type ToolOperationUncertainty = 'execution_interrupted' | 'settlement_write_failed' | 'worker_lost';
|
|
@@ -64,7 +64,11 @@ export const toolOperationOutcomeSchema = z.strictObject({
|
|
|
64
64
|
callNotStarted: z.boolean().optional(),
|
|
65
65
|
aborted: z.union([z.boolean(), z.strictObject({ reason: z.string().optional() })]).optional(),
|
|
66
66
|
pendingInject: z
|
|
67
|
-
.array(z.strictObject({
|
|
67
|
+
.array(z.strictObject({
|
|
68
|
+
id: z.string().optional(),
|
|
69
|
+
messages: z.array(turnHistoryMessageSchema),
|
|
70
|
+
released: z.number().int().nonnegative().optional(),
|
|
71
|
+
}))
|
|
68
72
|
.optional(),
|
|
69
73
|
}),
|
|
70
74
|
});
|
|
@@ -690,6 +690,12 @@ export type TurnEvent = {
|
|
|
690
690
|
injected?: {
|
|
691
691
|
id: string;
|
|
692
692
|
}[];
|
|
693
|
+
/**
|
|
694
|
+
* An inject at this stage dropped a draft the user never got: of the reply text since the
|
|
695
|
+
* turn began or its last `attempt`, only the first `released` characters stay the reply.
|
|
696
|
+
* {@linkcode keptReply} drops the rest.
|
|
697
|
+
*/
|
|
698
|
+
released?: number;
|
|
693
699
|
}
|
|
694
700
|
/**
|
|
695
701
|
* A tool call the model is still writing: the next piece of its arguments' JSON text, in order,
|
|
@@ -770,7 +776,8 @@ export declare const TURN_EVENT_SCHEMAS: {
|
|
|
770
776
|
};
|
|
771
777
|
/**
|
|
772
778
|
* `events` without the replies a retry threw away: each `attempt` drops the text, media and
|
|
773
|
-
* structured output before it, back to the turn's start or its last `done`.
|
|
774
|
-
*
|
|
779
|
+
* structured output before it, back to the turn's start or its last `done`. A `stage` event with
|
|
780
|
+
* `released` keeps only that much of the reply's text, and drops what followed it. Tool calls,
|
|
781
|
+
* thoughts and everything else stay, since they happened.
|
|
775
782
|
*/
|
|
776
783
|
export declare function keptReply(events: readonly TurnEvent[]): TurnEvent[];
|