@theoremjs/agents 0.3.2 → 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 +5 -1
- package/esm/mod.d.ts +2 -0
- package/esm/mod.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 +48 -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/provider-runtime.js +18 -16
- package/esm/src/kernel/schema.d.ts +1 -1
- package/esm/src/kernel/schema.js +1 -0
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -386,6 +386,10 @@ The React server handler creates this service from its atomic session store.
|
|
|
386
386
|
Use `toolInvocationHash` to compute the same canonical JSON digest as the coordinator.
|
|
387
387
|
Use this evidence to check current resource access before you disclose a cached result.
|
|
388
388
|
For browser effects, `PageToolExecution` issues a dispatch grant only after the claim.
|
|
389
|
+
A page-answered tool needs a claim, so pass `execution` together with `pageExecution`.
|
|
390
|
+
Use `createMemoryToolExecution` when one process serves the whole turn and nothing resumes after a restart.
|
|
391
|
+
It keeps claims and outcomes in memory. Its `MemoryToolExecutionOptions` set `maxOperations` (256 by default), after which it refuses new operations.
|
|
392
|
+
Write your own `ToolExecutionCoordinator` when a turn can resume on another worker.
|
|
389
393
|
`PageToolDispatch` contains the grant ID and a pending `PageAnswer`. Host code supplies result delivery.
|
|
390
394
|
The browser receives the final prepared arguments, then returns a result against that grant.
|
|
391
395
|
Neither profile JSON nor a provider event can supply this service.
|
|
@@ -403,7 +407,7 @@ Import from the narrowest entrypoint that has what you need. Each row gives the
|
|
|
403
407
|
|
|
404
408
|
| Entrypoint | Purpose |
|
|
405
409
|
| :--- | :--- |
|
|
406
|
-
| `jsr:@theoremjs/agents` / `@theoremjs/agents` | Main kernel API: profiles, schemas, runner, core types, provider constructors, declarative HTTP/MCP tool execution
|
|
410
|
+
| `jsr:@theoremjs/agents` / `@theoremjs/agents` | Main kernel API: profiles, schemas, runner, core types, provider constructors, declarative HTTP/MCP tool execution, `createMemoryToolExecution`. |
|
|
407
411
|
| `jsr:@theoremjs/agents/kernel` / `@theoremjs/agents/kernel` | Profile/turn types, tool catalog, `requireModelBinding`, thinking clamps over host model maps, OAuth 2.1 PKCE helpers (`createOAuthPkceFlow`, `exchangeOAuthPkce`, `refreshOAuthToken`). |
|
|
408
412
|
| `jsr:@theoremjs/agents/providers` / `@theoremjs/agents/providers` | Protocols, first-party providers, provider definitions, sign-in helpers, and vault contracts. |
|
|
409
413
|
| `jsr:@theoremjs/agents/guardrails` / `@theoremjs/agents/guardrails` | Sanitization, canary/egress gates, public error mapping, inbound injection/sensitive-data primitives. |
|
package/esm/mod.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export type { MediaTurnBehaviourSpec, ProfileTurnBehaviourSpec, ProfileTurnResum
|
|
|
65
65
|
export { AUTO_CONTINUE_DELAY_MS, DEFAULT_ALLOW_CONTINUE, DEFAULT_AUTO_CONTINUE, GenerationStopError, isContinueStopKind, isGenerationStopError, isResumeableStop, isUserCancelledStop, profileAllowsInject, profileAllowsSteering, profileTurnResumption, shouldAutoContinue, turnStopFromClientStreamEnd, turnStopFromInteractionStatus, turnStopFromOpenAiFinishReason, } from './src/kernel/stop.js';
|
|
66
66
|
export { authorizeToolReplay } from './src/kernel/tools/execute.js';
|
|
67
67
|
export { GATE_DECISIONS, type GateDecision } from './src/kernel/tools/gate-answer.js';
|
|
68
|
+
export type { MemoryToolExecutionOptions } from './src/kernel/tools/memory-execution.js';
|
|
69
|
+
export { createMemoryToolExecution } from './src/kernel/tools/memory-execution.js';
|
|
68
70
|
export type { CredentialQuery, McpProtocolVersion, McpRpcResponse, ToolRegistry, } from './src/kernel/tools/mod.js';
|
|
69
71
|
export { askUserTool, buildHttpToolTarget, createToolRegistry, credentialPlacement, executeHttpTool, executeMcpTool, formatToolResult, isUnsupportedMcpProtocolError, MCP_PROTOCOL_VERSIONS, parseMcpRpcResponse, prepareTurnToolSnapshot, registerHarnessTools, resolveToolAuth, UncheckedOutput, uncheckedOutput, withCredentialQuery, } from './src/kernel/tools/mod.js';
|
|
70
72
|
export type { BeginToolOperationResult, PageToolDispatch, PageToolExecution, SavedToolInvocation, TerminalToolEvent, ToolExecutionCoordinator, ToolOperationClaim, ToolOperationIdentity, ToolOperationOutcome, ToolOperationUncertainty, } from './src/kernel/tools/operation.js';
|
package/esm/mod.js
CHANGED
|
@@ -41,6 +41,7 @@ export { applyStageResult, STAGE_AFFORDANCE_MATRIX, STAGE_AFFORDANCES, stageAllo
|
|
|
41
41
|
export { AUTO_CONTINUE_DELAY_MS, DEFAULT_ALLOW_CONTINUE, DEFAULT_AUTO_CONTINUE, GenerationStopError, isContinueStopKind, isGenerationStopError, isResumeableStop, isUserCancelledStop, profileAllowsInject, profileAllowsSteering, profileTurnResumption, shouldAutoContinue, turnStopFromClientStreamEnd, turnStopFromInteractionStatus, turnStopFromOpenAiFinishReason, } from './src/kernel/stop.js';
|
|
42
42
|
export { authorizeToolReplay } from './src/kernel/tools/execute.js';
|
|
43
43
|
export { GATE_DECISIONS } from './src/kernel/tools/gate-answer.js';
|
|
44
|
+
export { createMemoryToolExecution } from './src/kernel/tools/memory-execution.js';
|
|
44
45
|
export { askUserTool, buildHttpToolTarget, createToolRegistry, credentialPlacement, executeHttpTool, executeMcpTool, formatToolResult, isUnsupportedMcpProtocolError, MCP_PROTOCOL_VERSIONS, parseMcpRpcResponse, prepareTurnToolSnapshot, registerHarnessTools, resolveToolAuth, UncheckedOutput, uncheckedOutput, withCredentialQuery, } from './src/kernel/tools/mod.js';
|
|
45
46
|
export { savedToolInvocationSchema, toolInvocationHash, toolOperationOutcomeSchema, } from './src/kernel/tools/operation.js';
|
|
46
47
|
export { awaitingUserInputSchema, gateReferenceSchema, isAwaitingUserInput, keptReply, reasoningPartSchema, TURN_EVENT_SCHEMAS, toolGateSchema, turnDoneOf, turnEventSchema, turnHistoryMessageSchema, turnToolSnapshotSchema, } from './src/kernel/turn-events.js';
|
|
@@ -409,6 +409,9 @@ async function* executeSingleAttemptCycle(args) {
|
|
|
409
409
|
// invariant: Fresh maxSteps budget per validation/egress attempt. before_end inject
|
|
410
410
|
// re-entry inside this cycle still accumulates stepCount (do not reset there).
|
|
411
411
|
state.stepCount = 0;
|
|
412
|
+
// invariant: The attempt's reply spans its before_end re-entries; only a new attempt starts it again.
|
|
413
|
+
state.replyText = '';
|
|
414
|
+
state.replyMessages = [];
|
|
412
415
|
let latestStructured;
|
|
413
416
|
for (;;) {
|
|
414
417
|
state.attemptEvents = [];
|
|
@@ -439,6 +442,8 @@ async function* executeSingleAttemptCycle(args) {
|
|
|
439
442
|
state,
|
|
440
443
|
stage: 'before_end',
|
|
441
444
|
step: Math.max(state.stepCount, 1),
|
|
445
|
+
reply: attempt.reply,
|
|
446
|
+
...(attempt.replyAt === undefined ? {} : { replyAt: attempt.replyAt }),
|
|
442
447
|
onStage: flow.currentReq.onStage,
|
|
443
448
|
signal: flow.currentReq.signal,
|
|
444
449
|
host: flow.currentGen.host,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type InjectUnit, type StageApplyWarning, type StageCallBag, type StageContext, type StageHandler } from '../../stages.js';
|
|
2
|
-
import type { Profile, ResolvedGeneration, TurnEvent, TurnStage } from '../../types.js';
|
|
2
|
+
import type { Profile, ResolvedGeneration, TurnEvent, TurnHistoryMessage, TurnStage } from '../../types.js';
|
|
3
3
|
import type { StepExecutionState } from './state.js';
|
|
4
4
|
export interface ApplyTurnStageArgs extends StageCallBag {
|
|
5
5
|
profile: Profile;
|
|
@@ -12,7 +12,16 @@ export interface ApplyTurnStageArgs extends StageCallBag {
|
|
|
12
12
|
/** Another provider step would exceed maxSteps — reject inject. */
|
|
13
13
|
injectWouldExceedMaxSteps?: boolean;
|
|
14
14
|
host?: unknown;
|
|
15
|
+
/** `before_end`: the answer the model just wrote, which goes into turn history ahead of an inject. */
|
|
16
|
+
reply?: readonly TurnHistoryMessage[];
|
|
17
|
+
/** Where that answer's text begins in the attempt's reply. */
|
|
18
|
+
replyAt?: number;
|
|
15
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A model call's reply joins turn history. Its text message, whose text begins at `at` in the
|
|
22
|
+
* attempt's reply, is the one an inject's drop cuts.
|
|
23
|
+
*/
|
|
24
|
+
export declare function recordReply(state: StepExecutionState, reply: readonly TurnHistoryMessage[], at: number | undefined): void;
|
|
16
25
|
export interface ApplyTurnStageResult {
|
|
17
26
|
abort?: boolean | {
|
|
18
27
|
reason?: string;
|
|
@@ -20,7 +29,10 @@ export interface ApplyTurnStageResult {
|
|
|
20
29
|
injectCount: number;
|
|
21
30
|
warnings: StageApplyWarning[];
|
|
22
31
|
}
|
|
23
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Sanitized stage injects extend portable history. An inject whose host says the user has only
|
|
34
|
+
* part of the attempt's reply drops the rest first (`StageResult.released`).
|
|
35
|
+
*/
|
|
24
36
|
export declare function applyStageInjects(state: StepExecutionState, stage: TurnStage, units: readonly InjectUnit[], extra?: {
|
|
25
37
|
callId?: string;
|
|
26
38
|
toolName?: string;
|
|
@@ -1,13 +1,87 @@
|
|
|
1
1
|
import { throwIfAborted } from '../../../guardrails/error.js';
|
|
2
2
|
import { injectedStageEvent, injectMessages, runStage, } from '../../stages.js';
|
|
3
3
|
import { profileAllowsInject } from '../../stop.js';
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A model call's reply joins turn history. Its text message, whose text begins at `at` in the
|
|
6
|
+
* attempt's reply, is the one an inject's drop cuts.
|
|
7
|
+
*/
|
|
8
|
+
export function recordReply(state, reply, at) {
|
|
9
|
+
state.currentHistory.push(...reply);
|
|
10
|
+
if (at === undefined)
|
|
11
|
+
return;
|
|
12
|
+
for (const message of reply) {
|
|
13
|
+
if (typeof message.content === 'string')
|
|
14
|
+
state.replyMessages.push({ message, at });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The least of the reply text any of `units` says the user has; `undefined` when none says. */
|
|
18
|
+
function releasedOf(units) {
|
|
19
|
+
const counts = units.flatMap((unit) => (unit.released === undefined ? [] : [unit.released]));
|
|
20
|
+
return counts.length > 0 ? Math.min(...counts) : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** `events` less their last `count` characters of reply text, cut from the end. */
|
|
23
|
+
function withoutTextTail(events, count) {
|
|
24
|
+
const out = [...events];
|
|
25
|
+
let left = count;
|
|
26
|
+
for (let index = out.length - 1; index >= 0 && left > 0; index -= 1) {
|
|
27
|
+
const event = out[index];
|
|
28
|
+
if (event?.type !== 'text')
|
|
29
|
+
continue;
|
|
30
|
+
const keep = Math.max(0, event.text.length - left);
|
|
31
|
+
left -= event.text.length - keep;
|
|
32
|
+
if (keep === 0)
|
|
33
|
+
out.splice(index, 1);
|
|
34
|
+
else
|
|
35
|
+
out[index] = { ...event, text: event.text.slice(0, keep) };
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Drops the draft past the `released` characters of the attempt's reply the user has: from its
|
|
41
|
+
* history messages, from what the attempt gate reads, and from the reply. Returns `released` when
|
|
42
|
+
* a draft was dropped.
|
|
43
|
+
*
|
|
44
|
+
* invariant: A message that keeps no text leaves history; a call it made stays on its own message.
|
|
45
|
+
*/
|
|
46
|
+
function dropDraft(state, released) {
|
|
47
|
+
if (released === undefined || released >= state.replyText.length)
|
|
48
|
+
return undefined;
|
|
49
|
+
const dropped = state.replyText.length - released;
|
|
50
|
+
state.replyText = state.replyText.slice(0, released);
|
|
51
|
+
state.released = state.released.slice(0, Math.max(0, state.released.length - dropped));
|
|
52
|
+
state.attemptEvents = withoutTextTail(state.attemptEvents, dropped);
|
|
53
|
+
const kept = [];
|
|
54
|
+
for (const { message, at } of state.replyMessages) {
|
|
55
|
+
const text = typeof message.content === 'string' ? message.content : '';
|
|
56
|
+
const keep = text.slice(0, Math.max(0, released - at));
|
|
57
|
+
const index = state.currentHistory.indexOf(message);
|
|
58
|
+
if (index < 0)
|
|
59
|
+
continue;
|
|
60
|
+
if (keep) {
|
|
61
|
+
const cut = keep === text ? message : { ...message, content: keep };
|
|
62
|
+
state.currentHistory[index] = cut;
|
|
63
|
+
kept.push({ message: cut, at });
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
state.currentHistory.splice(index, 1);
|
|
67
|
+
}
|
|
68
|
+
state.replyMessages = kept;
|
|
69
|
+
return released;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Sanitized stage injects extend portable history. An inject whose host says the user has only
|
|
73
|
+
* part of the attempt's reply drops the rest first (`StageResult.released`).
|
|
74
|
+
*/
|
|
5
75
|
export function* applyStageInjects(state, stage, units, extra) {
|
|
6
76
|
const inject = injectMessages(units);
|
|
7
77
|
if (inject.length === 0)
|
|
8
78
|
return 0;
|
|
79
|
+
const released = dropDraft(state, releasedOf(units));
|
|
9
80
|
state.currentHistory.push(...inject);
|
|
10
|
-
const landed = injectedStageEvent(stage, units,
|
|
81
|
+
const landed = injectedStageEvent(stage, units, {
|
|
82
|
+
...extra,
|
|
83
|
+
...(released === undefined ? {} : { released }),
|
|
84
|
+
});
|
|
11
85
|
if (landed) {
|
|
12
86
|
state.allEmittedEvents.push(landed);
|
|
13
87
|
yield landed;
|
|
@@ -15,7 +89,7 @@ export function* applyStageInjects(state, stage, units, extra) {
|
|
|
15
89
|
return inject.length;
|
|
16
90
|
}
|
|
17
91
|
export async function* applyTurnStage(args) {
|
|
18
|
-
const { profile, generation, state, onStage, host, ...call } = args;
|
|
92
|
+
const { profile, generation, state, onStage, host, reply, replyAt, ...call } = args;
|
|
19
93
|
throwIfAborted(call.signal);
|
|
20
94
|
const run = runStage({
|
|
21
95
|
...call,
|
|
@@ -33,6 +107,9 @@ export async function* applyTurnStage(args) {
|
|
|
33
107
|
next = await run.next();
|
|
34
108
|
}
|
|
35
109
|
const out = next.value;
|
|
110
|
+
// why: The model goes on after the inject, so it reads the answer it wrote before it.
|
|
111
|
+
if (reply && injectMessages(out.inject).length > 0)
|
|
112
|
+
recordReply(state, reply, replyAt);
|
|
36
113
|
return {
|
|
37
114
|
abort: out.abort,
|
|
38
115
|
injectCount: yield* applyStageInjects(state, call.stage, out.inject),
|
|
@@ -35,6 +35,19 @@ interface StepExecutionState {
|
|
|
35
35
|
withheldVisible?: boolean;
|
|
36
36
|
/** The reply text the stream's gate released this attempt, as it released it. */
|
|
37
37
|
released: string;
|
|
38
|
+
/**
|
|
39
|
+
* The attempt's reply text as the host has it, across `before_end` re-entries, less any draft an
|
|
40
|
+
* inject dropped (`InjectUnit.released`).
|
|
41
|
+
*/
|
|
42
|
+
replyText: string;
|
|
43
|
+
/**
|
|
44
|
+
* The messages in `currentHistory` that hold the model's text of `replyText`, each with where its
|
|
45
|
+
* text begins there: what an inject's drop cuts.
|
|
46
|
+
*/
|
|
47
|
+
replyMessages: {
|
|
48
|
+
message: TurnHistoryMessage;
|
|
49
|
+
at: number;
|
|
50
|
+
}[];
|
|
38
51
|
/** The canary opening the last provider call ended on, read in front of the next call's reply. */
|
|
39
52
|
canaryCarry?: string;
|
|
40
53
|
/** What the last provider call's thoughts ended on, read in front of the next call's thoughts. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BoundSystem } from '../../system-parts.js';
|
|
2
|
-
import type { Profile, ResolvedGeneration, TurnEvent, TurnModel, TurnRequest } from '../../types.js';
|
|
2
|
+
import type { Profile, ResolvedGeneration, TurnEvent, TurnHistoryMessage, TurnModel, TurnRequest } from '../../types.js';
|
|
3
3
|
import { type StepExecutionState } from './state.js';
|
|
4
4
|
declare function executeAttempt(args: {
|
|
5
5
|
safe: TurnRequest;
|
|
@@ -10,5 +10,7 @@ declare function executeAttempt(args: {
|
|
|
10
10
|
state: StepExecutionState;
|
|
11
11
|
}): AsyncGenerator<TurnEvent, {
|
|
12
12
|
latestStructured?: unknown;
|
|
13
|
+
reply: TurnHistoryMessage[];
|
|
14
|
+
replyAt?: number;
|
|
13
15
|
}>;
|
|
14
16
|
export { executeAttempt };
|
|
@@ -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.
|
|
@@ -175,7 +195,7 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
175
195
|
}) {
|
|
176
196
|
const { profile, generation, system, provider, signal } = args;
|
|
177
197
|
const { usage, genForStep, request, call } = startProviderCall(generation, system, state, isAnswerOnlyStep(profile, generation, state.stepCount));
|
|
178
|
-
const tally = { calls: [], reasoning: [], failures: [], replied: false };
|
|
198
|
+
const tally = { calls: [], reasoning: [], text: '', failures: [], replied: false };
|
|
179
199
|
const control = {
|
|
180
200
|
withholdVisible: false,
|
|
181
201
|
...(state.canaryCarry ? { canaryCarry: state.canaryCarry } : {}),
|
|
@@ -220,11 +240,13 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
220
240
|
yield event;
|
|
221
241
|
continue;
|
|
222
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;
|
|
223
245
|
if (tallyCallEvent(event, tally, call))
|
|
224
246
|
continue;
|
|
225
|
-
if (!releasesNow(
|
|
247
|
+
if (!releasesNow(next, control, state, buffer))
|
|
226
248
|
continue;
|
|
227
|
-
yield* shown.show(
|
|
249
|
+
yield* shown.show(next);
|
|
228
250
|
}
|
|
229
251
|
}
|
|
230
252
|
catch (err) {
|
|
@@ -250,7 +272,9 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
250
272
|
return {
|
|
251
273
|
calls: tally.calls,
|
|
252
274
|
reasoning: tally.reasoning,
|
|
275
|
+
text: tally.text,
|
|
253
276
|
latestStructured: tally.latestStructured,
|
|
277
|
+
...(tally.replyAt === undefined ? {} : { replyAt: tally.replyAt }),
|
|
254
278
|
...(resume ? { resume } : {}),
|
|
255
279
|
};
|
|
256
280
|
}
|
|
@@ -357,6 +381,14 @@ function* applyToolSettlement(settlement, state, call) {
|
|
|
357
381
|
}
|
|
358
382
|
return 'continue';
|
|
359
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
|
+
}
|
|
360
392
|
/**
|
|
361
393
|
* Run the model's calls. Each reaches the host as its raw call first, all of them before
|
|
362
394
|
* any runs; their phases then arrive as each tool gives them. With `tools.parallel` they
|
|
@@ -371,10 +403,10 @@ function* applyToolSettlement(settlement, state, call) {
|
|
|
371
403
|
* After the step, any gate stops the turn. A call a stage cancelled stops the turn too,
|
|
372
404
|
* once every result that did come back is recorded.
|
|
373
405
|
*/
|
|
374
|
-
async function* handleModelCalls(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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);
|
|
378
410
|
const stepId = crypto.randomUUID();
|
|
379
411
|
const leakScope = {
|
|
380
412
|
canary: generation.canary,
|
|
@@ -486,6 +518,9 @@ async function* handleModelCalls({ calls: upstreamCalls, reasoning }, generation
|
|
|
486
518
|
async function* executeAttempt(args) {
|
|
487
519
|
const { profile, generation, system, provider, state } = args;
|
|
488
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;
|
|
489
524
|
// why: Text streams via progressive-yield under egress, thoughts unguarded; validation and egress
|
|
490
525
|
// both hold non-visible events (structured) until the attempt gate, so a policy
|
|
491
526
|
// sees the structured payload before any of it reaches the host.
|
|
@@ -506,6 +541,8 @@ async function* executeAttempt(args) {
|
|
|
506
541
|
if (stepResult.latestStructured !== undefined) {
|
|
507
542
|
latestStructured = stepResult.latestStructured;
|
|
508
543
|
}
|
|
544
|
+
reply = stepResult.calls.length > 0 ? [] : stepReply(stepResult);
|
|
545
|
+
replyAt = stepResult.replyAt;
|
|
509
546
|
const executed = yield* handleModelCalls(stepResult, generation, profile, state, system, args.safe);
|
|
510
547
|
if (!executed) {
|
|
511
548
|
break;
|
|
@@ -515,6 +552,6 @@ async function* executeAttempt(args) {
|
|
|
515
552
|
state.lastStop = { kind: 'step_limit' };
|
|
516
553
|
}
|
|
517
554
|
}
|
|
518
|
-
return { latestStructured };
|
|
555
|
+
return { latestStructured, reply, ...(replyAt === undefined ? {} : { replyAt }) };
|
|
519
556
|
}
|
|
520
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';
|
|
@@ -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
|
@@ -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[];
|
|
@@ -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;
|
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",
|