@theoremjs/agents 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +7 -2
  2. package/esm/mod.d.ts +3 -1
  3. package/esm/mod.js +2 -1
  4. package/esm/src/guardrails/error.d.ts +1 -1
  5. package/esm/src/guardrails/error.js +3 -1
  6. package/esm/src/guardrails/labels.d.ts +2 -2
  7. package/esm/src/guardrails/labels.js +8 -1
  8. package/esm/src/guardrails/mod.d.ts +1 -1
  9. package/esm/src/guardrails/mod.js +1 -1
  10. package/esm/src/guardrails/quota.d.ts +3 -1
  11. package/esm/src/guardrails/quota.js +6 -1
  12. package/esm/src/guardrails/theorem-error.d.ts +1 -1
  13. package/esm/src/guardrails/theorem-error.js +3 -1
  14. package/esm/src/host/reply.js +1 -0
  15. package/esm/src/kernel/engine/decision.js +1 -0
  16. package/esm/src/kernel/engine/runner/gates.js +5 -0
  17. package/esm/src/kernel/engine/runner/stages.d.ts +14 -2
  18. package/esm/src/kernel/engine/runner/stages.js +80 -3
  19. package/esm/src/kernel/engine/runner/state.d.ts +13 -0
  20. package/esm/src/kernel/engine/runner/state.js +2 -0
  21. package/esm/src/kernel/engine/runner/steps.d.ts +3 -1
  22. package/esm/src/kernel/engine/runner/steps.js +49 -11
  23. package/esm/src/kernel/interaction-parts.d.ts +13 -1
  24. package/esm/src/kernel/interaction-parts.js +14 -1
  25. package/esm/src/kernel/mod.d.ts +2 -0
  26. package/esm/src/kernel/mod.js +1 -0
  27. package/esm/src/kernel/protocols/runtime.js +2 -2
  28. package/esm/src/kernel/provider-runtime.js +18 -16
  29. package/esm/src/kernel/schema.d.ts +1 -1
  30. package/esm/src/kernel/schema.js +2 -1
  31. package/esm/src/kernel/stages.d.ts +14 -3
  32. package/esm/src/kernel/stages.js +24 -7
  33. package/esm/src/kernel/tools/memory-execution.d.ts +11 -0
  34. package/esm/src/kernel/tools/memory-execution.js +59 -0
  35. package/esm/src/kernel/tools/operation.d.ts +2 -5
  36. package/esm/src/kernel/tools/operation.js +5 -1
  37. package/esm/src/kernel/turn-events.d.ts +9 -2
  38. package/esm/src/kernel/turn-events.js +29 -2
  39. package/esm/src/observability/trace-catalog.js +2 -1
  40. package/esm/src/providers/gemini/interactions-protocol.js +2 -5
  41. package/esm/src/providers/google/api-error.d.ts +2 -0
  42. package/esm/src/providers/google/api-error.js +26 -18
  43. package/esm/src/providers/google/live/stream.js +1 -1
  44. package/esm/src/providers/shared/retry.d.ts +1 -0
  45. package/esm/src/providers/shared/retry.js +6 -7
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -21,6 +21,7 @@
21
21
  <a href="https://jsr.io/@theoremjs/agents/score"><img src="https://jsr.io/badges/@theoremjs/agents/score" alt="JSR score"></a>
22
22
  <a href="https://www.npmjs.com/package/@theoremjs/agents"><img src="https://img.shields.io/npm/v/@theoremjs/agents?logo=npm&label=npm&color=cb3837" alt="npm"></a>
23
23
  <a href="https://github.com/masudl-hub/theoremai/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License"></a>
24
+ <a href="https://deepwiki.com/masudl-hub/theoremai"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
24
25
  </p>
25
26
 
26
27
  <p align="center">
@@ -385,6 +386,10 @@ The React server handler creates this service from its atomic session store.
385
386
  Use `toolInvocationHash` to compute the same canonical JSON digest as the coordinator.
386
387
  Use this evidence to check current resource access before you disclose a cached result.
387
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.
388
393
  `PageToolDispatch` contains the grant ID and a pending `PageAnswer`. Host code supplies result delivery.
389
394
  The browser receives the final prepared arguments, then returns a result against that grant.
390
395
  Neither profile JSON nor a provider event can supply this service.
@@ -402,7 +407,7 @@ Import from the narrowest entrypoint that has what you need. Each row gives the
402
407
 
403
408
  | Entrypoint | Purpose |
404
409
  | :--- | :--- |
405
- | `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`. |
406
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`). |
407
412
  | `jsr:@theoremjs/agents/providers` / `@theoremjs/agents/providers` | Protocols, first-party providers, provider definitions, sign-in helpers, and vault contracts. |
408
413
  | `jsr:@theoremjs/agents/guardrails` / `@theoremjs/agents/guardrails` | Sanitization, canary/egress gates, public error mapping, inbound injection/sensitive-data primitives. |
@@ -441,7 +446,7 @@ Named exports from the root barrel (same symbols hosts get from `@theoremjs/agen
441
446
  | Guardrail vocabulary | `AdvisoryLevel`, `TrustLevel`, `GuardrailStage`, `Severity`, `GuardrailHit`, `Verdict`, `GuardrailAction`, `GuardrailContext`, `GuardrailEvent`, `guardrailEventSchema`, `OutboundPayload`, `Provenance`, `ToolOrigin`, `ScanText`, `BlockedReplySpec`, `BlockedReplyOnBlock`, `ResolvedBlockedReply`, `UrlAllow`, `NameAllow`, `UrlDetector`, `ResolvedAllow`, `ProfileGuardrailsSpec`, `HostGuardrailsSpec`, `NetworkGuardrailSpec`, `QuotaGuardrailSpec`, `ResolvedGuardrailPolicy`, `SensitiveGroup`, `SensitiveSelection`, `SensitiveSwitches`, `GuardedToolText`, `TurnTaint`, `TaintGate`, `TaintGuardrailSpec`, `TurnDestinations`, `DestinationGate`, `DestinationVerdict`, `TRUST_LEVELS`, `GUARDRAIL_STAGES`, `SEVERITIES`, `TOOL_ORIGINS` |
442
447
  | Guardrail policy | `resolveGuardrailPolicy`, `BLOCKED_REPLY_ON_BLOCK`, `hitRules`, `EGRESS_RULES` |
443
448
  | Tool boundary | `guardToolResult`, `guardToolFailureText`, `inspectToolArguments`, `toolCallEvent`, `wrapToolData`, `isRemoteOrigin`, `composeToolText`, `checkTaintGate`, `recordTaint`, `isTainted`, `isSuspicious`, `checkDestinationGate`, `turnDestinations`, `addGivenDestinations`, `addHistoryDestinations`, `addRequestDestinations`, `addResultDestinations`, `DESTINATION_GATES`, `directives`, `advisoryLevel`, `DIRECTIVE_SIGNALS`, `Directive`, `DirectiveSignal`, `ADVISORY_LEVELS`, `TOOL_CLOSE`, `TOOL_ORIGINS`, `TAINT_GATES`, `InspectedToolArguments`, `textForScan`, `scanTextOf` |
444
- | Quota | `QuotaSlotStatus`, `clientIp`, `quotaExhausted`, `releaseSlot`, `resetSlots`, `skipQuota`, `takeSlot` |
449
+ | Quota | `QuotaSlotStatus`, `clientIp`, `quotaBusy`, `quotaExhausted`, `releaseSlot`, `resetSlots`, `skipQuota`, `takeSlot` |
445
450
  | Labels | `LABEL_KEYS`, `LabelKey`, `CLIENT_LABEL_KEYS`, `ClientLabelKey`, `LabelOverrides`, `LabelParams`, `labelDefault`, `labelText`, `overrideLabels`, `resetLabels` |
446
451
  | Sanitize | `sanitizeProjectId`, `sanitizeTurnRequest`, `sanitizeTurnRequestWithEvents`, `SanitizedTurnRequest`, `guardrailFromHits`, `guardrailFromVerdict`, `guardrailTurnEvent`, `projectGuardrailTurnEvent`, `hitFromSpan`, `projectGuardrailEvent` |
447
452
  | Canary / egress | `mintCanary`, `bindCanary`, `wrapUserData`, `scanTextForCanaryLeak`, `scanTextForPromptEcho`, `PROMPT_ECHO_WORDS`, `redactCanary`, `OMIT_CANARY`, `createCanaryStreamGate`, `eventHasCanary`, `createCanaryGateSession`, `filterCanaryGatedEvents`, `CanaryGateResult`, `CanaryGateSession`, `CanaryStreamGate`, `GivenUrls`, `createOutboundProgressiveGate`, `createProgressiveYieldGate`, `createLiveOutboundGateSession`, `processLiveOutboundBatch`, `finalizeLiveOutboundTurn`, `abortLiveOutboundTurn`, `LiveHeldOutput`, `LiveOutboundBatchResult`, `LiveOutboundGateSession`, `ProgressiveYieldGate`, `ProgressiveYieldGateOptions`, `ProgressiveYieldResult` |
package/esm/mod.d.ts CHANGED
@@ -19,7 +19,7 @@ export { ADVISORY_LEVELS, abortLiveOutboundTurn, addGivenDestinations, addHistor
19
19
  export type { DnsOverHttpsOptions, GuardedFetchOptions, ResolveHost, } from './src/guardrails/network.js';
20
20
  export { assertSafeUrl, dnsOverHttpsResolver, fetchGuarded, isLocalhostName, isPrivateOrLocalAddress, } from './src/guardrails/network.js';
21
21
  export type { QuotaSlotStatus } from './src/guardrails/quota.js';
22
- export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
22
+ export { clientIp, quotaBusy, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
23
23
  export type { SanitizedTurnRequest } from './src/guardrails/sanitize.js';
24
24
  export { sanitizeProjectId, sanitizeTurnRequest, sanitizeTurnRequestWithEvents, } from './src/guardrails/sanitize.js';
25
25
  export type { ProviderStateRebuilt } from './src/kernel/checkpoint.js';
@@ -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
@@ -14,7 +14,7 @@ export { hitFromSpan, projectGuardrailEvent, } from './src/guardrails/hits.js';
14
14
  export { CLIENT_LABEL_KEYS, LABEL_KEYS, labelDefault, labelText, overrideLabels, resetLabels, } from './src/guardrails/labels.js';
15
15
  export { ADVISORY_LEVELS, abortLiveOutboundTurn, addGivenDestinations, addHistoryDestinations, addRequestDestinations, addResultDestinations, BLOCKED_REPLY_ON_BLOCK, bindCanary, checkDestinationGate, checkTaintGate, composeToolText, createCanaryGateSession, createCanaryStreamGate, createLiveOutboundGateSession, createOutboundProgressiveGate, createProgressiveYieldGate, DESTINATION_GATES, DIRECTIVE_SIGNALS, directives, EGRESS_RULES, eventHasCanary, filterCanaryGatedEvents, finalizeLiveOutboundTurn, GUARDRAIL_STAGES, guardToolFailureText, guardToolResult, hitRules, inspectToolArguments, isRemoteOrigin, isSuspicious, isTainted, mintCanary, OMIT_CANARY, processLiveOutboundBatch, recordTaint, redactCanary, resolveGuardrailPolicy, SEVERITIES, scanTextForCanaryLeak, scanTextOf, TAINT_GATES, TOOL_CLOSE, TOOL_ORIGINS, TRUST_LEVELS, textForScan, toolCallEvent, turnDestinations, wrapToolData, wrapUserData, } from './src/guardrails/mod.js';
16
16
  export { assertSafeUrl, dnsOverHttpsResolver, fetchGuarded, isLocalhostName, isPrivateOrLocalAddress, } from './src/guardrails/network.js';
17
- export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
17
+ export { clientIp, quotaBusy, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
18
18
  export { sanitizeProjectId, sanitizeTurnRequest, sanitizeTurnRequestWithEvents, } from './src/guardrails/sanitize.js';
19
19
  export { historyHash, providerCheckpointSchema, providerContinuationSchema, providerWarningSchema, } from './src/kernel/checkpoint.js';
20
20
  export { clearProfiles, compactHistory, getProfile, getProvider, getStructured, getTool, hasProfile, hasTool, invokeTool, invokeToolStep, listProfiles, listProtocols, listProviders, listTools, projectProfile, registerProfile, registerProfiles, registerProvider, registerStructured, registerTool, registerTools, requireTool, resetTools, resolveTurn, runDecision, runEmbedding, runSession, runTool, runToolStep, runTurn, } from './src/kernel/default-scope.js';
@@ -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';
@@ -10,7 +10,7 @@ declare function isTimeoutError(err: unknown): boolean;
10
10
  declare function throwIfAborted(signal?: AbortSignal): void;
11
11
  /** Anything that is not a `TheoremError` or an abort escaped every boundary that names kinds: a THEOREM bug. */
12
12
  declare function errorKind(err: unknown): ErrorKind;
13
- /** The error kind for an HTTP status: 401, 402 and 403 are `auth`; 408, 504 and 524 `timeout`; 429 `rate_limit`; 500 and above `unavailable`; the rest `unsupported`. */
13
+ /** The error kind for an HTTP status: 401, 402 and 403 are `auth`; 408, 504 and 524 `timeout`; 429 `rate_limit`; 503 and 529 (overloaded) `busy`; 500 and above `unavailable`; the rest `unsupported`. */
14
14
  declare function kindOfHttpStatus(status: number): ErrorKind;
15
15
  /** The named reasons an error carries, one line each, in the profile's wording. */
16
16
  declare function errorCopyText(copy: ErrorCopies, labels?: LabelOverrides): string;
@@ -32,7 +32,7 @@ function errorKind(err) {
32
32
  return 'cancelled';
33
33
  return 'internal';
34
34
  }
35
- /** The error kind for an HTTP status: 401, 402 and 403 are `auth`; 408, 504 and 524 `timeout`; 429 `rate_limit`; 500 and above `unavailable`; the rest `unsupported`. */
35
+ /** The error kind for an HTTP status: 401, 402 and 403 are `auth`; 408, 504 and 524 `timeout`; 429 `rate_limit`; 503 and 529 (overloaded) `busy`; 500 and above `unavailable`; the rest `unsupported`. */
36
36
  function kindOfHttpStatus(status) {
37
37
  if (status === 401 || status === 402 || status === 403)
38
38
  return 'auth';
@@ -40,6 +40,8 @@ function kindOfHttpStatus(status) {
40
40
  return 'timeout';
41
41
  if (status === 429)
42
42
  return 'rate_limit';
43
+ if (status === 503 || status === 529)
44
+ return 'busy';
43
45
  if (status >= 500)
44
46
  return 'unavailable';
45
47
  return 'unsupported';
@@ -1,7 +1,7 @@
1
1
  /** Values that fill a label template's `{param}` placeholders. */
2
2
  export type LabelParams = Record<string, string | number>;
3
3
  /** Keys are stable API. */
4
- export declare const LABEL_KEYS: readonly ["provider.decision_failed", "provider.decision_unavailable", "provider.decision_model_missing", "provider.decision_ended", "provider.decision_images_off", "provider.decision_box_model", "provider.budget_exhausted", "provider.adapter_version", "provider.adapter_methods", "provider.adapter_schemas", "provider.fallback_slot", "provider.model_fallback_slot", "provider.host_model", "provider.turn_profile", "provider.model_binding_missing", "provider.session_closed", "provider.settled_call", "provider.session_unavailable", "provider.live_input", "provider.tool_cancel_missing", "provider.kernel_gate", "provider.event_invalid", "provider.response_identity", "provider.credential_resolution", "provider.credential_shape", "provider.stored_continuation", "provider.continuation_mismatch", "provider.continuation_contract", "provider.stateless_history", "provider.events_after_completion", "provider.duplicate_call", "provider.tool_cancel_live", "provider.complete_unavailable", "provider.parallel_tools", "provider.binding_identity", "provider.profile_type", "provider.event_type", "provider.feature", "provider.input_kind", "provider.output_kind", "provider.builtin", "provider.credential_missing", "provider.profile_configuration", "continue.instruction", "canary.bind_note", "user_data.note", "context.note", "taint.blocked", "taint.reason_steered", "taint.reason_tainted", "taint.destination_blocked", "taint.destination_confirm", "advisory.notice_elevated", "advisory.notice_high", "advisory.guidance", "attachments.too_many_files", "attachments.too_many_of_kind", "attachments.file_too_large", "attachments.turn_too_large", "attachments.not_accepted", "attachments.mime_not_allowed", "input.too_long", "attachments.limits_unconfigured", "quota.exhausted", "error.config", "error.request", "error.input", "error.action", "error.auth", "error.rate_limit", "error.unsupported", "error.unavailable", "error.bad_response", "error.network", "error.timeout", "error.safety", "error.blocked", "error.declined", "error.failed", "error.cancelled", "error.internal", "repair.default_guidance", "repair.items_guidance", "repair.prompt_header", "repair.prompt_intro", "repair.prompt_instructions", "repair.history_heading", "repair.section_previous_output", "repair.section_validator_rejection", "repair.section_repair_guidance", "repair.section_instructions", "compaction.request", "compaction.tool_call", "compaction.tool_result", "decision.box_image", "detect.blocked", "detect.call_blocked", "detect.output_blocked", "detect.shown_blocked", "detect.hint.ids", "detect.hint.financial", "detect.hint.network", "detect.hint.credentials", "detect.hint.injection", "detect.hint.tool_instructions", "detect.hint.canary_leak", "detect.hint.prompt_leak", "detect.hint.marker_leak", "detect.hint.ungiven_images", "detect.hint.ungiven_links", "detect.hint.tool_leak", "detect.hint.own", "egress.default_repair_guidance", "egress.refusal", "egress.refusal_cut", "egress.rejection", "egress.rejection_found", "egress.hint_unscannable", "egress.hint_provider_tool_leak", "thought.omitted_image", "thought.omitted_link", "thought.omitted_instructions", "session.abandon_gated", "session.tool_denied", "session.tool_aborted", "session.sign_in", "sign_in.link", "sign_in.pending", "sign_in.done", "sign_in.declined", "sign_in.expired", "sign_in.out_of_scope", "session.gate_expired", "session.provider_state_unowned", "session.conversation_unknown", "session.turn_ended", "session.gate_pending", "session.part_skipped", "live.session_ended", "voice.unsupported", "voice.permission", "voice.unavailable", "voice.failed", "voice.empty", "stop.cancelled", "stop.interrupted", "stop.length", "stop.filtered", "stop.provider_error", "stop.stream_incomplete", "stop.step_limit", "composer.model", "composer.effort", "tool.awaiting_user", "tool.completed_hidden", "tool.t2_loader_needs_snapshot", "tool.t2_loader_shape", "tool.t2_loader_output_invalid", "tool.execution_uncertain", "tool.input_invalid", "tool.input_invalid_after_mutate", "tool.output_invalid_after_mutate", "tool.handler_no_output", "tool.did_not_run", "tool.page_no_answer", "tool.page_timed_out", "tool.output_invalid", "tool.not_loaded_t2", "tool.wait_unknown", "tool.wait_cycle", "tool.not_visible", "tool.builtin_not_enabled", "tool.provider_native", "tool.not_registered", "tool.builtin_needs_snapshot", "tool.not_allowed", "tool.not_eligible", "tool.unsupported_type", "tool.agent_call_limit", "tool.agent_failed", "tool.activity_failed", "protocol.not_served", "protocol.profile_type", "protocol.options", "protocol.option_clash", "protocol.base_options", "protocol.id_clash", "protocol.ws_headers", "protocol.builtin_unwritten", "protocol.provider_missing", "protocol.connection", "protocol.part_schema", "protocol.part_invalid", "protocol.key_invalid", "protocol.no_key", "protocol.reply_shape", "protocol.endpoint_url", "protocol.endpoint_host", "protocol.http", "protocol.no_operation", "protocol.definition", "protocol.provider_id_clash", "protocol.history_needed"];
4
+ export declare const LABEL_KEYS: readonly ["provider.decision_failed", "provider.decision_unavailable", "provider.decision_model_missing", "provider.decision_ended", "provider.decision_images_off", "provider.decision_box_model", "provider.budget_exhausted", "provider.adapter_version", "provider.adapter_methods", "provider.adapter_schemas", "provider.fallback_slot", "provider.model_fallback_slot", "provider.host_model", "provider.turn_profile", "provider.model_binding_missing", "provider.session_closed", "provider.settled_call", "provider.session_unavailable", "provider.live_input", "provider.tool_cancel_missing", "provider.kernel_gate", "provider.event_invalid", "provider.response_identity", "provider.credential_resolution", "provider.credential_shape", "provider.stored_continuation", "provider.continuation_mismatch", "provider.continuation_contract", "provider.stateless_history", "provider.events_after_completion", "provider.duplicate_call", "provider.tool_cancel_live", "provider.complete_unavailable", "provider.parallel_tools", "provider.binding_identity", "provider.profile_type", "provider.event_type", "provider.feature", "provider.input_kind", "provider.output_kind", "provider.builtin", "provider.credential_missing", "provider.profile_configuration", "continue.instruction", "canary.bind_note", "user_data.note", "context.note", "taint.blocked", "taint.reason_steered", "taint.reason_tainted", "taint.destination_blocked", "taint.destination_confirm", "advisory.notice_elevated", "advisory.notice_high", "advisory.guidance", "attachments.too_many_files", "attachments.too_many_of_kind", "attachments.file_too_large", "attachments.turn_too_large", "attachments.not_accepted", "attachments.mime_not_allowed", "input.too_long", "attachments.limits_unconfigured", "quota.exhausted", "quota.busy", "error.config", "error.request", "error.input", "error.action", "error.auth", "error.rate_limit", "error.busy", "error.unsupported", "error.unavailable", "error.bad_response", "error.network", "error.timeout", "error.safety", "error.blocked", "error.declined", "error.failed", "error.cancelled", "error.internal", "repair.default_guidance", "repair.items_guidance", "repair.prompt_header", "repair.prompt_intro", "repair.prompt_instructions", "repair.history_heading", "repair.section_previous_output", "repair.section_validator_rejection", "repair.section_repair_guidance", "repair.section_instructions", "compaction.request", "compaction.tool_call", "compaction.tool_result", "decision.box_image", "detect.blocked", "detect.call_blocked", "detect.output_blocked", "detect.shown_blocked", "detect.hint.ids", "detect.hint.financial", "detect.hint.network", "detect.hint.credentials", "detect.hint.injection", "detect.hint.tool_instructions", "detect.hint.canary_leak", "detect.hint.prompt_leak", "detect.hint.marker_leak", "detect.hint.ungiven_images", "detect.hint.ungiven_links", "detect.hint.tool_leak", "detect.hint.own", "egress.default_repair_guidance", "egress.refusal", "egress.refusal_cut", "egress.rejection", "egress.rejection_found", "egress.hint_unscannable", "egress.hint_provider_tool_leak", "thought.omitted_image", "thought.omitted_link", "thought.omitted_instructions", "session.abandon_gated", "session.tool_denied", "session.tool_aborted", "session.sign_in", "sign_in.link", "sign_in.pending", "sign_in.done", "sign_in.declined", "sign_in.expired", "sign_in.out_of_scope", "session.gate_expired", "session.provider_state_unowned", "session.conversation_unknown", "session.turn_ended", "session.gate_pending", "session.part_skipped", "live.session_ended", "voice.unsupported", "voice.permission", "voice.unavailable", "voice.failed", "voice.empty", "stop.cancelled", "stop.interrupted", "stop.length", "stop.filtered", "stop.provider_error", "stop.stream_incomplete", "stop.step_limit", "composer.model", "composer.effort", "tool.awaiting_user", "tool.completed_hidden", "tool.t2_loader_needs_snapshot", "tool.t2_loader_shape", "tool.t2_loader_output_invalid", "tool.execution_uncertain", "tool.input_invalid", "tool.input_invalid_after_mutate", "tool.output_invalid_after_mutate", "tool.handler_no_output", "tool.did_not_run", "tool.page_no_answer", "tool.page_timed_out", "tool.output_invalid", "tool.not_loaded_t2", "tool.wait_unknown", "tool.wait_cycle", "tool.not_visible", "tool.builtin_not_enabled", "tool.provider_native", "tool.not_registered", "tool.builtin_needs_snapshot", "tool.not_allowed", "tool.not_eligible", "tool.unsupported_type", "tool.agent_call_limit", "tool.agent_failed", "tool.activity_failed", "protocol.not_served", "protocol.profile_type", "protocol.options", "protocol.option_clash", "protocol.base_options", "protocol.id_clash", "protocol.ws_headers", "protocol.builtin_unwritten", "protocol.provider_missing", "protocol.connection", "protocol.part_schema", "protocol.part_invalid", "protocol.key_invalid", "protocol.no_key", "protocol.reply_shape", "protocol.endpoint_url", "protocol.endpoint_host", "protocol.http", "protocol.no_operation", "protocol.definition", "protocol.provider_id_clash", "protocol.history_needed"];
5
5
  /** One of {@linkcode LABEL_KEYS}. */
6
6
  export type LabelKey = (typeof LABEL_KEYS)[number];
7
7
  /**
@@ -10,7 +10,7 @@ export type LabelKey = (typeof LABEL_KEYS)[number];
10
10
  * puts into history. Only these overrides travel to the browser; the rest
11
11
  * (repair prompts, canary, taint, egress) stay on the host.
12
12
  */
13
- export declare const CLIENT_LABEL_KEYS: readonly ["attachments.too_many_files", "attachments.too_many_of_kind", "attachments.file_too_large", "attachments.turn_too_large", "attachments.not_accepted", "attachments.mime_not_allowed", "input.too_long", "attachments.limits_unconfigured", "detect.blocked", "error.config", "error.request", "error.input", "error.action", "error.auth", "error.rate_limit", "error.unsupported", "error.unavailable", "error.bad_response", "error.network", "error.timeout", "error.safety", "error.blocked", "error.declined", "error.failed", "error.cancelled", "error.internal", "session.abandon_gated", "session.tool_denied", "session.sign_in", "session.gate_expired", "session.turn_ended", "session.gate_pending", "session.part_skipped", "live.session_ended", "tool.awaiting_user", "tool.completed_hidden", "tool.execution_uncertain", "voice.unsupported", "voice.permission", "voice.unavailable", "voice.failed", "voice.empty", "stop.cancelled", "stop.interrupted", "stop.length", "stop.filtered", "stop.provider_error", "stop.stream_incomplete", "stop.step_limit", "composer.model", "composer.effort"];
13
+ export declare const CLIENT_LABEL_KEYS: readonly ["attachments.too_many_files", "attachments.too_many_of_kind", "attachments.file_too_large", "attachments.turn_too_large", "attachments.not_accepted", "attachments.mime_not_allowed", "input.too_long", "attachments.limits_unconfigured", "detect.blocked", "error.config", "error.request", "error.input", "error.action", "error.auth", "error.rate_limit", "error.busy", "error.unsupported", "error.unavailable", "error.bad_response", "error.network", "error.timeout", "error.safety", "error.blocked", "error.declined", "error.failed", "error.cancelled", "error.internal", "session.abandon_gated", "session.tool_denied", "session.sign_in", "session.gate_expired", "session.turn_ended", "session.gate_pending", "session.part_skipped", "live.session_ended", "tool.awaiting_user", "tool.completed_hidden", "tool.execution_uncertain", "voice.unsupported", "voice.permission", "voice.unavailable", "voice.failed", "voice.empty", "stop.cancelled", "stop.interrupted", "stop.length", "stop.filtered", "stop.provider_error", "stop.stream_incomplete", "stop.step_limit", "composer.model", "composer.effort"];
14
14
  /** One of {@linkcode CLIENT_LABEL_KEYS}. */
15
15
  export type ClientLabelKey = (typeof CLIENT_LABEL_KEYS)[number];
16
16
  /** Host-supplied replacement templates, `{param}` placeholders included. */
@@ -67,12 +67,14 @@ export const LABEL_KEYS = [
67
67
  'input.too_long',
68
68
  'attachments.limits_unconfigured',
69
69
  'quota.exhausted',
70
+ 'quota.busy',
70
71
  'error.config',
71
72
  'error.request',
72
73
  'error.input',
73
74
  'error.action',
74
75
  'error.auth',
75
76
  'error.rate_limit',
77
+ 'error.busy',
76
78
  'error.unsupported',
77
79
  'error.unavailable',
78
80
  'error.bad_response',
@@ -229,6 +231,7 @@ export const CLIENT_LABEL_KEYS = [
229
231
  'error.action',
230
232
  'error.auth',
231
233
  'error.rate_limit',
234
+ 'error.busy',
232
235
  'error.unsupported',
233
236
  'error.unavailable',
234
237
  'error.bad_response',
@@ -382,12 +385,14 @@ const DEFAULTS = {
382
385
  'quota.exhausted': (params) => params.perDay === 1
383
386
  ? "You've reached today's limit of 1 message."
384
387
  : `You've reached today's limit of ${String(params.perDay)} messages.`,
388
+ 'quota.busy': 'Sorry, your last message is still being answered. Please wait for it to finish.',
385
389
  'error.config': 'Sorry, something went wrong.',
386
390
  'error.request': 'Sorry, something went wrong.',
387
391
  'error.input': "Sorry, that file can't be used here.",
388
392
  'error.action': "Sorry, that isn't available here.",
389
393
  'error.auth': "Sorry, the assistant can't connect at the moment.",
390
394
  'error.rate_limit': 'Sorry, the usage limit has been reached. Please try again later.',
395
+ 'error.busy': 'Sorry, the model is busy right now. Please try again in a moment.',
391
396
  'error.unsupported': "Sorry, that isn't something the assistant can do.",
392
397
  'error.unavailable': "Sorry, the model isn't available at the moment. Please try again shortly.",
393
398
  'error.bad_response': "Sorry, that reply didn't come through properly. Please try again.",
@@ -592,14 +597,16 @@ export const LABEL_NOTES = {
592
597
  'input.too_long': 'Shown to the user when one text a turn sends, such as the message or a text file, is longer than Theorem reads. Takes {maxChars}.',
593
598
  'attachments.limits_unconfigured': 'Shown to the user who sends a file to an agent whose file limits are not set.',
594
599
  'quota.exhausted': 'Shown to the user who has used up the daily message cap. Takes {perDay}.',
600
+ 'quota.busy': 'Shown to the user who sends a message while their last one is still being answered (`quotaBusy`).',
595
601
  'error.config': "Shown to the user when the turn fails because the agent's profile, a tool, or a schema is set up wrong. Takes {tool} when one step failed.",
596
602
  'error.request': 'Shown to the user when the turn fails because the host called Theorem wrongly. Takes {tool} when one step failed.',
597
603
  'error.input': 'Shown to the user when they sent something the agent does not accept. Takes {tool} when one step failed.',
598
604
  'error.action': 'Shown to the user when they asked for something the agent does not allow. Takes {tool} when one step failed.',
599
605
  'error.auth': 'Shown to the user when a model key is missing or rejected, or its account cannot be billed. Takes {tool} when one step failed.',
600
606
  'error.rate_limit': 'Shown to the user when the model provider reports too many requests, or a quota is used up. Takes {tool} when one step failed.',
607
+ 'error.busy': 'Shown to the user when the model provider is up but too busy for the call (HTTP 503 or 529). Takes {tool} when one step failed.',
601
608
  'error.unsupported': 'Shown to the user when the model or its route cannot serve the request. Takes {tool} when one step failed.',
602
- 'error.unavailable': 'Shown to the user when the model provider is down or overloaded. Takes {tool} when one step failed.',
609
+ 'error.unavailable': 'Shown to the user when the model provider is down. Takes {tool} when one step failed.',
603
610
  'error.bad_response': 'Shown to the user when the model provider answers with something that cannot be used. Takes {tool} when one step failed.',
604
611
  'error.network': 'Shown to the user when the request never reached the model provider. Takes {tool} when one step failed.',
605
612
  'error.timeout': 'Shown to the user when the model takes longer than the host allows. Takes {tool} when one step failed.',
@@ -36,7 +36,7 @@ export type { ProgressiveYieldGate, ProgressiveYieldGateOptions, ProgressiveYiel
36
36
  export { createOutboundProgressiveGate, createProgressiveYieldGate } from './progressive-yield.js';
37
37
  export { PROMPT_ECHO_WORDS, scanTextForPromptEcho } from './prompt-echo.js';
38
38
  export type { QuotaSlotStatus } from './quota.js';
39
- export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
39
+ export { clientIp, quotaBusy, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
40
40
  export type { GuardrailRule } from './rules.js';
41
41
  export { DETECT_RULES, detectRule, EGRESS_RULES, NETWORK_RULES, TOOL_RULES, } from './rules.js';
42
42
  export type { SanitizedTurnRequest } from './sanitize.js';
@@ -23,7 +23,7 @@ export { abortLiveOutboundTurn, createLiveOutboundGateSession, finalizeLiveOutbo
23
23
  export { resolveGuardrailPolicy } from './policy.js';
24
24
  export { createOutboundProgressiveGate, createProgressiveYieldGate } from './progressive-yield.js';
25
25
  export { PROMPT_ECHO_WORDS, scanTextForPromptEcho } from './prompt-echo.js';
26
- export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
26
+ export { clientIp, quotaBusy, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
27
27
  export { DETECT_RULES, detectRule, EGRESS_RULES, NETWORK_RULES, TOOL_RULES, } from './rules.js';
28
28
  export { sanitizeHistory, sanitizeProjectId, sanitizeTurnRequest, sanitizeTurnRequestWithEvents, } from './sanitize.js';
29
29
  export { sensitiveSpans } from './sensitive.js';
@@ -15,7 +15,9 @@ declare function takeSlot(profile: Profile, ip: string, now: number): QuotaSlotS
15
15
  declare function releaseSlot(profile: Profile, ip: string): void;
16
16
  /** `undefined` when the profile has no quota configured. */
17
17
  declare function quotaExhausted(profile: Profile): TheoremError | undefined;
18
+ /** A turn already holds this client's slot (`takeSlot` returned `busy`): worded by the labels' `quota.busy`. */
19
+ declare function quotaBusy(profile: Profile): TheoremError;
18
20
  /** Clears all process-local quota counters; intended for tests or host resets. */
19
21
  declare function resetSlots(): void;
20
22
  export type { QuotaSlotStatus };
21
- export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot };
23
+ export { clientIp, quotaBusy, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot };
@@ -73,8 +73,13 @@ function quotaExhausted(profile) {
73
73
  return new TheoremError('rate_limit', `${profile.id} used its daily quota of ${quota.perDay} turns`, // labels-exempt: internal diagnostic; the user reads the copy key
74
74
  { copy: { key: 'quota.exhausted', params: { perDay: quota.perDay } } });
75
75
  }
76
+ /** A turn already holds this client's slot (`takeSlot` returned `busy`): worded by the labels' `quota.busy`. */
77
+ function quotaBusy(profile) {
78
+ return new TheoremError('rate_limit', `${profile.id} is already running a turn for this client`, // labels-exempt: internal diagnostic; the user reads the copy key
79
+ { copy: { key: 'quota.busy' } });
80
+ }
76
81
  /** Clears all process-local quota counters; intended for tests or host resets. */
77
82
  function resetSlots() {
78
83
  slots.clear();
79
84
  }
80
- export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot };
85
+ export { clientIp, quotaBusy, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot };
@@ -5,7 +5,7 @@ export type { ErrorCopies, ErrorCopy };
5
5
  * the builder in code and traces (`errorKind`, `error.type`), the user through
6
6
  * the kind's wording (`error.<kind>` in the labels).
7
7
  */
8
- export declare const ERROR_KINDS: readonly ["config", "request", "input", "action", "auth", "rate_limit", "unsupported", "unavailable", "bad_response", "network", "timeout", "safety", "blocked", "declined", "failed", "cancelled", "internal"];
8
+ export declare const ERROR_KINDS: readonly ["config", "request", "input", "action", "auth", "rate_limit", "busy", "unsupported", "unavailable", "bad_response", "network", "timeout", "safety", "blocked", "declined", "failed", "cancelled", "internal"];
9
9
  /** Why an operation failed; the user's wording and the HTTP status follow from it. */
10
10
  export type ErrorKind = (typeof ERROR_KINDS)[number];
11
11
  /** Options for a `TheoremError`: the cause and, optionally, wording for the user. */
@@ -17,9 +17,11 @@ export const ERROR_KINDS = [
17
17
  'auth',
18
18
  /** Too many requests, or a quota used up. */
19
19
  'rate_limit',
20
+ /** The provider is up but too busy for the call; another key or a moment later may serve it. */
21
+ 'busy',
20
22
  /** The model or route cannot serve this request. */
21
23
  'unsupported',
22
- /** The provider is down or overloaded. */
24
+ /** The provider is down. */
23
25
  'unavailable',
24
26
  /** The provider answered with something that cannot be used. */
25
27
  'bad_response',
@@ -14,6 +14,7 @@ const STATUS_BY_KIND = {
14
14
  action: 403,
15
15
  auth: 401,
16
16
  rate_limit: HTTP_BUSY,
17
+ busy: 503,
17
18
  unsupported: 422,
18
19
  unavailable: 503,
19
20
  bad_response: 502,
@@ -370,6 +370,7 @@ function decisionProviderError(error) {
370
370
  auth: 'authentication',
371
371
  rate_limit: 'rate_limited',
372
372
  network: 'network',
373
+ busy: 'unavailable',
373
374
  unavailable: 'unavailable',
374
375
  timeout: 'timeout',
375
376
  cancelled: 'cancelled',
@@ -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
- /** Sanitized stage injects extend portable history. */
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
- /** Sanitized stage injects extend portable history. */
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, extra);
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. */
@@ -34,6 +34,8 @@ function openTurnState(args) {
34
34
  allEmittedEvents: args.allEmittedEvents ?? [],
35
35
  attemptEvents: [],
36
36
  released: '',
37
+ replyText: '',
38
+ replyMessages: [],
37
39
  givenUrls: givenUrlSets(),
38
40
  destinations: turnDestinations(),
39
41
  canaryGiven: false,
@@ -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 };