@theoremjs/agents 0.3.4-next.482.g7e608f2 → 0.3.4-next.508.geda6db0

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 (71) hide show
  1. package/README.md +61 -51
  2. package/esm/mod.d.ts +2 -2
  3. package/esm/mod.js +1 -1
  4. package/esm/src/guardrails/credential-scan.d.ts +1 -1
  5. package/esm/src/guardrails/credential-scan.js +41 -18
  6. package/esm/src/guardrails/destinations.js +6 -2
  7. package/esm/src/guardrails/detect-at.d.ts +1 -3
  8. package/esm/src/guardrails/detect-at.js +6 -22
  9. package/esm/src/guardrails/egress-automata.js +203 -202
  10. package/esm/src/guardrails/egress-stream.js +10 -3
  11. package/esm/src/guardrails/egress-urls.js +4 -2
  12. package/esm/src/guardrails/injection-patterns.d.ts +3 -1
  13. package/esm/src/guardrails/injection-patterns.js +87 -4
  14. package/esm/src/guardrails/labels.d.ts +2 -2
  15. package/esm/src/guardrails/labels.js +7 -5
  16. package/esm/src/guardrails/mod.d.ts +1 -1
  17. package/esm/src/guardrails/normalize.js +4 -2
  18. package/esm/src/guardrails/prompt-echo.js +4 -2
  19. package/esm/src/guardrails/sensitive.d.ts +2 -2
  20. package/esm/src/guardrails/sensitive.js +1 -1
  21. package/esm/src/interface/profile-interface.js +13 -0
  22. package/esm/src/kernel/answer-tool.d.ts +14 -0
  23. package/esm/src/kernel/answer-tool.js +26 -0
  24. package/esm/src/kernel/engine/decision-references.d.ts +14 -0
  25. package/esm/src/kernel/engine/decision-references.js +55 -0
  26. package/esm/src/kernel/engine/decision.d.ts +3 -3
  27. package/esm/src/kernel/engine/decision.js +38 -22
  28. package/esm/src/kernel/engine/embedding.js +0 -2
  29. package/esm/src/kernel/engine/runner/gates.js +38 -4
  30. package/esm/src/kernel/engine/runner/steps.js +47 -10
  31. package/esm/src/kernel/engine/runner/stream.d.ts +14 -8
  32. package/esm/src/kernel/engine/runner/stream.js +25 -13
  33. package/esm/src/kernel/mod.d.ts +1 -1
  34. package/esm/src/kernel/mod.js +1 -1
  35. package/esm/src/kernel/profile-presence.js +12 -0
  36. package/esm/src/kernel/profile-scope.js +14 -14
  37. package/esm/src/kernel/protocols/binding.d.ts +3 -1
  38. package/esm/src/kernel/protocols/binding.js +6 -0
  39. package/esm/src/kernel/protocols/limits.d.ts +10 -11
  40. package/esm/src/kernel/protocols/limits.js +9 -23
  41. package/esm/src/kernel/protocols/runtime.d.ts +3 -2
  42. package/esm/src/kernel/protocols/runtime.js +1 -2
  43. package/esm/src/kernel/provider-budget.d.ts +2 -2
  44. package/esm/src/kernel/provider-budget.js +22 -12
  45. package/esm/src/kernel/provider-runtime.js +2 -2
  46. package/esm/src/kernel/registry/profiles.js +1 -1
  47. package/esm/src/kernel/registry/resolve.js +4 -0
  48. package/esm/src/kernel/registry/schemas.js +1 -1
  49. package/esm/src/kernel/schema.js +12 -2
  50. package/esm/src/kernel/types.d.ts +4 -2
  51. package/esm/src/observability/spans.js +7 -8
  52. package/esm/src/providers/embedding-limits.js +4 -4
  53. package/esm/src/providers/gemini/{embedding-protocol.d.ts → embeddings-protocol.d.ts} +3 -3
  54. package/esm/src/providers/gemini/{embedding-protocol.js → embeddings-protocol.js} +3 -3
  55. package/esm/src/providers/gemini/live-protocol.js +4 -1
  56. package/esm/src/providers/gemini/provider.js +2 -2
  57. package/esm/src/providers/local/embeddings-protocol.d.ts +7 -0
  58. package/esm/src/providers/local/{embedding-protocol.js → embeddings-protocol.js} +2 -2
  59. package/esm/src/providers/local/provider.js +2 -2
  60. package/esm/src/providers/mod.d.ts +3 -3
  61. package/esm/src/providers/mod.js +3 -3
  62. package/esm/src/providers/openrouter/decisions-protocol.d.ts +5 -3
  63. package/esm/src/providers/openrouter/decisions-protocol.js +55 -4
  64. package/esm/src/providers/openrouter/embeddings-protocol.d.ts +6 -0
  65. package/esm/src/providers/openrouter/{embedding-protocol.js → embeddings-protocol.js} +2 -2
  66. package/esm/src/providers/openrouter/provider.js +2 -2
  67. package/esm/src/providers/system-one/protocol.d.ts +4 -0
  68. package/esm/src/providers/system-one/protocol.js +25 -24
  69. package/package.json +1 -1
  70. package/esm/src/providers/local/embedding-protocol.d.ts +0 -7
  71. package/esm/src/providers/openrouter/embedding-protocol.d.ts +0 -6
@@ -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", "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"];
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", "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", "structured.answer_tool", "structured.answer_missing", "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.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"];
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", "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. */
@@ -64,7 +64,6 @@ export const LABEL_KEYS = [
64
64
  'attachments.turn_too_large',
65
65
  'attachments.not_accepted',
66
66
  'attachments.mime_not_allowed',
67
- 'input.too_long',
68
67
  'attachments.limits_unconfigured',
69
68
  'quota.exhausted',
70
69
  'quota.busy',
@@ -96,6 +95,8 @@ export const LABEL_KEYS = [
96
95
  'repair.section_validator_rejection',
97
96
  'repair.section_repair_guidance',
98
97
  'repair.section_instructions',
98
+ 'structured.answer_tool',
99
+ 'structured.answer_missing',
99
100
  'compaction.request',
100
101
  'compaction.tool_call',
101
102
  'compaction.tool_result',
@@ -222,7 +223,6 @@ export const CLIENT_LABEL_KEYS = [
222
223
  'attachments.turn_too_large',
223
224
  'attachments.not_accepted',
224
225
  'attachments.mime_not_allowed',
225
- 'input.too_long',
226
226
  'attachments.limits_unconfigured',
227
227
  'detect.blocked',
228
228
  'error.config',
@@ -380,7 +380,6 @@ const DEFAULTS = {
380
380
  ? `Sorry, ${name} is a file type that can't be used here.`
381
381
  : "Sorry, that file type can't be used here.";
382
382
  },
383
- 'input.too_long': (params) => `Sorry, that message is too long. Please keep it under ${Number(params.maxChars).toLocaleString('en-US')} characters.`,
384
383
  'attachments.limits_unconfigured': "Sorry, files can't be used here at the moment.",
385
384
  'quota.exhausted': (params) => params.perDay === 1
386
385
  ? "You've reached today's limit of 1 message."
@@ -414,6 +413,8 @@ const DEFAULTS = {
414
413
  'repair.section_validator_rejection': '### VALIDATOR REJECTION',
415
414
  'repair.section_repair_guidance': '### REPAIR GUIDANCE',
416
415
  'repair.section_instructions': '### INSTRUCTIONS',
416
+ 'structured.answer_tool': 'Call this once your work is done, with your final answer as its input. Text you write outside this call is shown as progress, not taken as the answer.',
417
+ 'structured.answer_missing': 'The reply did not call {tool}. Call {tool} with your final answer.',
417
418
  'compaction.request': 'Summarize the conversation above, including any earlier summary in it. Call the participants the user and the assistant, and keep every name, number, fact, and decision either may need later. Reply with the summary only.',
418
419
  'compaction.tool_call': 'Called {tool} with {arguments}',
419
420
  'compaction.tool_result': '{tool} returned: {result}',
@@ -594,7 +595,6 @@ export const LABEL_NOTES = {
594
595
  'attachments.turn_too_large': "Shown to the user when a message's files together are larger than Turn bytes. Takes {maxTurnBytes}.",
595
596
  'attachments.not_accepted': "Shown to the user who sends files or a voice note to an agent that takes none. Takes {channel}: 'voice' or 'attachment'.",
596
597
  'attachments.mime_not_allowed': "Shown to the user when a file's type is not in the agent's accepted types. Takes {mimeType}, {channel} ('voice' or 'attachment'), and {fileName} when the file has one.",
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}.',
598
598
  'attachments.limits_unconfigured': 'Shown to the user who sends a file to an agent whose file limits are not set.',
599
599
  'quota.exhausted': 'Shown to the user who has used up the daily message cap. Takes {perDay}.',
600
600
  'quota.busy': 'Shown to the user who sends a message while their last one is still being answered (`quotaBusy`).',
@@ -626,6 +626,8 @@ export const LABEL_NOTES = {
626
626
  'repair.section_validator_rejection': "The heading over the validator's or egress check's reason in the repair request.",
627
627
  'repair.section_repair_guidance': 'The heading over the repair guidance in the repair request.',
628
628
  'repair.section_instructions': 'The heading over the numbered steps in the repair request.',
629
+ 'structured.answer_tool': "The description of the answer tool: on a structured turn that has tools, the model calls it with its final answer, and the tool's input is the structured output.",
630
+ 'structured.answer_missing': 'The validator rejection in the repair request when a structured turn with tools ended without calling the answer tool. Takes {tool}.',
629
631
  'compaction.request': 'Sent to the compactor after the messages it compacts, asking for the summary.',
630
632
  'compaction.tool_call': 'How a tool call reads to the compactor. Takes {tool} and {arguments}, the call as JSON.',
631
633
  'compaction.tool_result': 'How a tool result reads to the compactor. Takes {tool} and {result}, the output as sent to the model.',
@@ -759,9 +761,9 @@ const LABEL_PLACEHOLDERS = {
759
761
  'attachments.turn_too_large': ['maxTurnBytes'],
760
762
  'attachments.not_accepted': ['channel'],
761
763
  'attachments.mime_not_allowed': ['mimeType', 'channel', 'fileName'],
762
- 'input.too_long': ['maxChars'],
763
764
  'quota.exhausted': ['perDay'],
764
765
  'repair.history_heading': ['count'],
766
+ 'structured.answer_missing': TOOL,
765
767
  'compaction.tool_call': ['tool', 'arguments'],
766
768
  'compaction.tool_result': ['tool', 'result'],
767
769
  'decision.box_image': ['image'],
@@ -50,5 +50,5 @@ export type { Directive, DirectiveSignal } from './tool-directives.js';
50
50
  export { advisoryLevel, DIRECTIVE_SIGNALS, directives } from './tool-directives.js';
51
51
  export type { GuardedToolText, InspectedToolArguments } from './tool-result.js';
52
52
  export { checkTaintGate, composeToolText, guardToolFailureText, guardToolResult, inspectToolArguments, isRemoteOrigin, isSuspicious, isTainted, recordTaint, TOOL_CLOSE, toolCallEvent, wrapToolData, } from './tool-result.js';
53
- export type { AdvisoryLevel, BlockedReplyOnBlock, BlockedReplySpec, DestinationGate, GuardrailAction, GuardrailContext, GuardrailEvent, GuardrailHit, GuardrailStage, HostGuardrailsSpec, NetworkGuardrailSpec, OutboundPayload, ProfileGuardrailsSpec, Provenance, QuotaGuardrailSpec, ResolvedBlockedReply, ResolvedGuardrailPolicy, Severity, TaintGate, TaintGuardrailSpec, ToolOrigin, TrustLevel, TurnTaint, Verdict, } from './types.js';
53
+ export type { AdvisoryLevel, BlockedReplyOnBlock, BlockedReplySpec, DecisionDisclosureEnforcer, DecisionDisclosureVerdict, DecisionGuardrailsSpec, DestinationGate, GuardrailAction, GuardrailContext, GuardrailEvent, GuardrailHit, GuardrailStage, HostGuardrailsSpec, NetworkGuardrailSpec, OutboundPayload, ProfileGuardrailsSpec, Provenance, QuotaGuardrailSpec, ResolvedBlockedReply, ResolvedGuardrailPolicy, Severity, TaintGate, TaintGuardrailSpec, ToolOrigin, TrustLevel, TurnTaint, Verdict, } from './types.js';
54
54
  export { ADVISORY_LEVELS, BLOCKED_REPLY_ON_BLOCK, DESTINATION_GATES, GUARDRAIL_STAGES, SEVERITIES, TAINT_GATES, TOOL_ORIGINS, TRUST_LEVELS, } from './types.js';
@@ -134,8 +134,10 @@ function withoutMatches(view, pattern) {
134
134
  let last = 0;
135
135
  const keep = (to) => {
136
136
  kept.text += view.text.slice(last, to);
137
- kept.start.push(...view.start.slice(last, to));
138
- kept.end.push(...view.end.slice(last, to));
137
+ for (let at = last; at < to; at++) {
138
+ kept.start.push(view.start[at]);
139
+ kept.end.push(view.end[at]);
140
+ }
139
141
  };
140
142
  for (const match of view.text.matchAll(pattern)) {
141
143
  keep(match.index);
@@ -61,8 +61,10 @@ function promptReadings(stretches, canary) {
61
61
  const readings = { plain: [], slotted: [] };
62
62
  for (const view of PROMPT_VIEWS) {
63
63
  const read = viewReadings(stretches.map(view), canary === undefined ? undefined : view(canary));
64
- readings.plain.push(...read.plain);
65
- readings.slotted.push(...read.slotted);
64
+ for (const each of read.plain)
65
+ readings.plain.push(each);
66
+ for (const each of read.slotted)
67
+ readings.slotted.push(each);
66
68
  }
67
69
  return readings;
68
70
  }
@@ -4,8 +4,8 @@ declare const CARD_CANDIDATE: RegExp;
4
4
  interface SensitivePattern {
5
5
  group: SensitiveGroup;
6
6
  pattern: RegExp;
7
- /** Whether a match at `at` of `text` counts. */
8
- hit?: (match: string, text: string, at: number) => boolean;
7
+ /** Whether a match at `at` of `text` counts; `lower` is `text` in lower case. */
8
+ hit?: (match: string, text: string, at: number, lower?: () => string) => boolean;
9
9
  }
10
10
  /** Every pattern, with its group. */
11
11
  declare const SENSITIVE_PATTERNS: readonly SensitivePattern[];
@@ -81,7 +81,7 @@ const SENSITIVE_PATTERNS = [
81
81
  ...CREDENTIALS.map((rule) => ({
82
82
  group: 'credentials',
83
83
  pattern: new RegExp(rule.pattern.source, `${rule.pattern.flags}g`),
84
- hit: (match, text, at) => credentialHit(rule, GLOBAL_ALLOWLIST, match, text, at),
84
+ hit: (match, text, at, lower) => credentialHit(rule, GLOBAL_ALLOWLIST, match, text, at, lower?.()),
85
85
  })),
86
86
  ];
87
87
  const ALL_GROUPS = {
@@ -29,6 +29,19 @@ const modelBinding = z.object({
29
29
  meter: z.enum(COMPACTION_METERS).optional(),
30
30
  })
31
31
  .optional(),
32
+ limits: z
33
+ .object({
34
+ operationTimeoutMs: z.number().optional(),
35
+ connectTimeoutMs: z.number().optional(),
36
+ liveStepTimeoutMs: z.number().optional(),
37
+ maxWireRecordBytes: z.number().optional(),
38
+ maxWireResponseBytes: z.number().optional(),
39
+ maxEventBytes: z.number().optional(),
40
+ maxStepBytes: z.number().optional(),
41
+ maxStepEvents: z.number().optional(),
42
+ maxPendingToolCalls: z.number().optional(),
43
+ })
44
+ .optional(),
32
45
  });
33
46
  const outputs = z.object({
34
47
  structured: z
@@ -0,0 +1,14 @@
1
+ import { type LabelOverrides } from '../guardrails/labels.js';
2
+ import type { ModelRequest, ResolvedGeneration } from './types.js';
3
+ /** The tool a structured turn with tools answers through. */
4
+ export declare const ANSWER_TOOL = "final_answer";
5
+ /**
6
+ * True when the turn's structured output comes from the answer tool, not from a response format.
7
+ *
8
+ * why: Many endpoints refuse a response format beside tools, and many models given both answer in
9
+ * JSON at once without calling a tool. Read it off the turn's generation, not one step's: the
10
+ * step that must answer has its tools taken away, and still answers through this tool.
11
+ */
12
+ export declare function answersByTool(generation: Pick<ResolvedGeneration, 'structured' | 'tools'>): boolean;
13
+ /** The request with the answer tool in place of the response format, when the turn answers by tool. */
14
+ export declare function withAnswerTool(request: ModelRequest, generation: Pick<ResolvedGeneration, 'structured' | 'tools'>, labels: LabelOverrides | undefined): ModelRequest;
@@ -0,0 +1,26 @@
1
+ import { labelText } from '../guardrails/labels.js';
2
+ /** The tool a structured turn with tools answers through. */
3
+ export const ANSWER_TOOL = 'final_answer';
4
+ /**
5
+ * True when the turn's structured output comes from the answer tool, not from a response format.
6
+ *
7
+ * why: Many endpoints refuse a response format beside tools, and many models given both answer in
8
+ * JSON at once without calling a tool. Read it off the turn's generation, not one step's: the
9
+ * step that must answer has its tools taken away, and still answers through this tool.
10
+ */
11
+ export function answersByTool(generation) {
12
+ return generation.structured !== null && generation.tools.wire.length > 0;
13
+ }
14
+ /** The request with the answer tool in place of the response format, when the turn answers by tool. */
15
+ export function withAnswerTool(request, generation, labels) {
16
+ const { structured } = generation;
17
+ if (!structured || !answersByTool(generation))
18
+ return request;
19
+ const tool = {
20
+ type: 'function',
21
+ name: ANSWER_TOOL,
22
+ description: labelText('structured.answer_tool', {}, labels),
23
+ parameters: structured.jsonSchema,
24
+ };
25
+ return { ...request, structured: null, wireTools: [...(request.wireTools ?? []), tool] };
26
+ }
@@ -0,0 +1,14 @@
1
+ import type { DecisionAsset } from '../types.js';
2
+ /** A run of text, or the image a reference in it stands for. */
3
+ export type ReferencePiece = string | {
4
+ reference: string;
5
+ asset: DecisionAsset;
6
+ };
7
+ /**
8
+ * A string split at the image references in it, as TypeSafe reads them: a string that is exactly a
9
+ * reference is that image; otherwise references match as plain text, left to right, never
10
+ * overlapping, the longest where two start together. A string with no reference comes back whole.
11
+ */
12
+ export declare function splitReferences(text: string, assets: Record<string, DecisionAsset>): ReferencePiece[];
13
+ /** The images the strings of `values` reference, in the order of `assets`. */
14
+ export declare function referencedAssets(values: readonly unknown[], assets: Record<string, DecisionAsset>): Record<string, DecisionAsset>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * A string split at the image references in it, as TypeSafe reads them: a string that is exactly a
3
+ * reference is that image; otherwise references match as plain text, left to right, never
4
+ * overlapping, the longest where two start together. A string with no reference comes back whole.
5
+ */
6
+ export function splitReferences(text, assets) {
7
+ if (Object.hasOwn(assets, text))
8
+ return [{ reference: text, asset: assets[text] }];
9
+ const references = Object.keys(assets);
10
+ const pieces = [];
11
+ let from = 0;
12
+ while (from < text.length) {
13
+ let at = -1;
14
+ let found = '';
15
+ for (const reference of references) {
16
+ const index = text.indexOf(reference, from);
17
+ if (index < 0)
18
+ continue;
19
+ if (at < 0 || index < at || (index === at && reference.length > found.length)) {
20
+ at = index;
21
+ found = reference;
22
+ }
23
+ }
24
+ if (at < 0)
25
+ break;
26
+ if (at > from)
27
+ pieces.push(text.slice(from, at));
28
+ pieces.push({ reference: found, asset: assets[found] });
29
+ from = at + found.length;
30
+ }
31
+ if (from < text.length)
32
+ pieces.push(text.slice(from));
33
+ return pieces;
34
+ }
35
+ /** Every string value in a JSON value, keys left out: the only places a reference is read. */
36
+ function* stringValues(value) {
37
+ if (typeof value === 'string')
38
+ yield value;
39
+ else if (Array.isArray(value))
40
+ for (const item of value)
41
+ yield* stringValues(item);
42
+ else if (value !== null && typeof value === 'object')
43
+ for (const item of Object.values(value))
44
+ yield* stringValues(item);
45
+ }
46
+ /** The images the strings of `values` reference, in the order of `assets`. */
47
+ export function referencedAssets(values, assets) {
48
+ const used = new Set();
49
+ for (const value of values)
50
+ for (const text of stringValues(value))
51
+ for (const piece of splitReferences(text, assets))
52
+ if (typeof piece !== 'string')
53
+ used.add(piece.reference);
54
+ return Object.fromEntries(Object.entries(assets).filter(([reference]) => used.has(reference)));
55
+ }
@@ -3,7 +3,7 @@ import type { TraceSink } from '../../observability/trace-sink.js';
3
3
  import type { ProviderHostOptions } from '../protocols/runtime.js';
4
4
  import type { KernelRegistry } from '../registry/kernel-registry.js';
5
5
  import type { DecisionProfile, DecisionRequest, DecisionResult } from '../types.js';
6
- /** Where the provider takes decision requests, as its preset states. */
6
+ /** Each decision failure code, and the error kind it reads as. */
7
7
  declare const DECISION_ERROR_KINDS: {
8
8
  readonly invalid_request: "request";
9
9
  readonly authentication: "auth";
@@ -36,8 +36,8 @@ export declare function validateDecisionRequest(request: Omit<DecisionRequest, '
36
36
  questions: unknown;
37
37
  }, profile: DecisionProfile): asserts request is DecisionRequest;
38
38
  /**
39
- * Exactly one decision request, traced however it ends. An ambiguous POST is never retried,
40
- * and a trace write failure never changes the decision's outcome.
39
+ * One decision, traced however it ends. A transient failure (408, 429, a 5xx gateway or a network
40
+ * error) is retried within five seconds; a trace write failure never changes the decision's outcome.
41
41
  */
42
42
  export declare function runDecisionInRegistry(registry: KernelRegistry, request: DecisionRequest, options: RunDecisionOptions): Promise<DecisionResult>;
43
43
  export {};
@@ -5,7 +5,8 @@ import { guardedCall } from '../provider-runtime.js';
5
5
  import { soleModelId } from '../registry/sole-model.js';
6
6
  import { isRecord } from '../util/record.js';
7
7
  import { tracedCall } from './call-trace.js';
8
- /** Where the provider takes decision requests, as its preset states. */
8
+ import { referencedAssets } from './decision-references.js';
9
+ /** Each decision failure code, and the error kind it reads as. */
9
10
  const DECISION_ERROR_KINDS = {
10
11
  invalid_request: 'request',
11
12
  authentication: 'auth',
@@ -65,7 +66,8 @@ function distribution(value, labels, path) {
65
66
  for (const label of labels)
66
67
  out[label] = finite(value[label], `${path}.${label}`);
67
68
  const total = Object.values(out).reduce((sum, item) => sum + item, 0);
68
- if (Math.abs(total - 1) > 0.001) {
69
+ // why: Answers come rounded to hundredths, so each label may be off by half of one.
70
+ if (Math.abs(total - 1) > 0.005 * labels.length + 1e-9) {
69
71
  throw new DecisionError('malformed_response', `${path} must sum to one`); // labels-exempt: upstream contract error
70
72
  }
71
73
  return out;
@@ -310,19 +312,19 @@ function isDecisionEntry(value) {
310
312
  }
311
313
  return true;
312
314
  }
313
- async function enforceDisclosure(profile, model, provider, request) {
315
+ async function enforceDisclosure(profile, model, provider, request, assets) {
314
316
  const verdict = await profile.guardrails?.disclosure?.enforce(request.state, {
315
317
  destination: provider,
316
318
  profileId: profile.id,
317
319
  model,
318
320
  questionIds: Object.keys(request.questions),
319
- ...(request.assets ? { assets: request.assets } : {}),
321
+ ...(assets ? { assets } : {}),
320
322
  });
321
323
  if (verdict?.action === 'block') {
322
324
  throw new DecisionError('disclosure_blocked', 'Decision disclosure was blocked'); // labels-exempt: developer contract error
323
325
  }
324
326
  }
325
- function decisionSpanAttributes(profile, modelId, binding, request) {
327
+ function decisionSpanAttributes(profile, modelId, binding, request, assets) {
326
328
  return {
327
329
  'gen_ai.operation.name': 'decide',
328
330
  'gen_ai.provider.name': binding.provider,
@@ -332,10 +334,10 @@ function decisionSpanAttributes(profile, modelId, binding, request) {
332
334
  'theorem.decision.contract': profile.decision.contract,
333
335
  'theorem.decision.state': traceJson(request.state),
334
336
  'theorem.decision.questions': traceJson(request.questions),
335
- ...(request.assets
337
+ ...(assets
336
338
  ? {
337
339
  // invariant: Image bytes never reach a trace; it keeps each image's type and size.
338
- 'theorem.decision.assets': traceJson(Object.fromEntries(Object.entries(request.assets).map(([reference, { mimeType, data }]) => [
340
+ 'theorem.decision.assets': traceJson(Object.fromEntries(Object.entries(assets).map(([reference, { mimeType, data }]) => [
339
341
  reference,
340
342
  { mimeType, bytes: data.length },
341
343
  ]))),
@@ -384,25 +386,38 @@ function decisionProviderError(error) {
384
386
  // why: A framework failure's message holds no response body, and says what stopped the decision.
385
387
  error instanceof TheoremError ? error.message : labelText('provider.decision_failed'), isRecord(error) && typeof error.status === 'number' ? error.status : undefined, { cause: error });
386
388
  }
387
- async function decide(registry, profile, [modelId, binding], request, options) {
389
+ /** What the provider gets: each box's image said in its instructions, and only the images a string references. */
390
+ function sentCall(request) {
391
+ const questions = Object.fromEntries(Object.entries(request.questions).map(([id, question]) => [
392
+ id,
393
+ providerQuestion(question, request.assets),
394
+ ]));
395
+ const assets = request.assets
396
+ ? referencedAssets([
397
+ request.state,
398
+ ...Object.values(questions).flatMap((question) => [
399
+ question.instructions,
400
+ 'criteria' in question ? question.criteria : undefined,
401
+ ]),
402
+ ], request.assets)
403
+ : {};
404
+ return {
405
+ state: request.state,
406
+ questions,
407
+ ...(Object.keys(assets).length ? { assets } : {}),
408
+ };
409
+ }
410
+ async function decide(registry, profile, [modelId, binding], request, sent, options) {
388
411
  throwIfAborted(request.signal);
389
412
  const provider = registry.providers.require(binding.provider, binding.apiId);
390
- await enforceDisclosure(profile, modelId, binding.provider, request);
413
+ await enforceDisclosure(profile, modelId, binding.provider, request, sent.assets);
391
414
  throwIfAborted(request.signal);
392
415
  try {
393
416
  const call = guardedCall(provider, binding, 'decision', options, {
394
417
  ...(request.signal ? { signal: request.signal } : {}),
395
418
  ...(binding.timeoutMs ? { timeoutMs: binding.timeoutMs } : {}),
396
419
  });
397
- const result = await call.decide({
398
- apiId: binding.apiId,
399
- state: request.state,
400
- questions: Object.fromEntries(Object.entries(request.questions).map(([id, question]) => [
401
- id,
402
- providerQuestion(question, request.assets),
403
- ])),
404
- ...(request.assets ? { assets: request.assets } : {}),
405
- });
420
+ const result = await call.decide({ apiId: binding.apiId, ...sent });
406
421
  if (typeof result.model !== 'string' || !result.model.trim())
407
422
  throw new DecisionError('malformed_response', labelText('provider.decision_model_missing'));
408
423
  return { ...result, answers: validateAnswers(result.answers, request.questions) };
@@ -414,17 +429,18 @@ async function decide(registry, profile, [modelId, binding], request, options) {
414
429
  }
415
430
  }
416
431
  /**
417
- * Exactly one decision request, traced however it ends. An ambiguous POST is never retried,
418
- * and a trace write failure never changes the decision's outcome.
432
+ * One decision, traced however it ends. A transient failure (408, 429, a 5xx gateway or a network
433
+ * error) is retried within five seconds; a trace write failure never changes the decision's outcome.
419
434
  */
420
435
  export async function runDecisionInRegistry(registry, request, options) {
421
436
  const profile = requireDecisionProfile(registry, request.profile);
422
437
  validateDecisionRequest(request, profile);
423
438
  const model = decisionModel(profile);
424
439
  const [modelId, binding] = model;
440
+ const sent = sentCall(request);
425
441
  return await tracedCall({
426
442
  name: `decide ${binding.apiId}`,
427
- attributes: decisionSpanAttributes(profile, modelId, binding, request),
443
+ attributes: decisionSpanAttributes(profile, modelId, binding, request, sent.assets),
428
444
  traceparent: request.traceparent,
429
445
  metadata: request.metadata,
430
446
  writer: {
@@ -433,7 +449,7 @@ export async function runDecisionInRegistry(registry, request, options) {
433
449
  guardrails: profile.guardrails,
434
450
  },
435
451
  }, async (root) => {
436
- const result = await decide(registry, profile, model, request, options);
452
+ const result = await decide(registry, profile, model, request, sent, options);
437
453
  recordResult(root, result);
438
454
  return result;
439
455
  });
@@ -1,4 +1,3 @@
1
- import { refuseOverlong } from '../../guardrails/detect-at.js';
2
1
  import { TheoremError, throwIfAborted } from '../../guardrails/error.js';
3
2
  import { protocolOf } from '../protocols/binding.js';
4
3
  import { guardedCall } from '../provider-runtime.js';
@@ -47,7 +46,6 @@ function validateInputs(profile, modelId, inputs, model) {
47
46
  const files = [];
48
47
  for (const part of parts) {
49
48
  if (part.type === 'text') {
50
- refuseOverlong(part.text, 'embedding input');
51
49
  if (!rules.text)
52
50
  throw new TheoremError('unsupported', `Profile ${profile.id} does not accept text embedding input`);
53
51
  continue;
@@ -8,6 +8,7 @@ import { labelText } from '../../../guardrails/labels.js';
8
8
  import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
9
9
  import { replyIsJudged } from '../../../guardrails/progressive-yield.js';
10
10
  import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
11
+ import { ANSWER_TOOL, answersByTool } from '../../answer-tool.js';
11
12
  import { resolveInputParts } from '../../registry/ingress.js';
12
13
  import { profileTurnOutputs } from '../../registry/profile-outputs.js';
13
14
  import { injectWouldExceedMaxSteps } from '../../stages.js';
@@ -15,7 +16,7 @@ import { findLast } from '../../util/find-last.js';
15
16
  import { guardrailCheckAttributes } from '../turn-trace.js';
16
17
  import { collectItemFailures, collectValidationFailures, formatValidationFailures, itemSchemaAt, valueAt, withListAt, } from './schema-validation.js';
17
18
  import { applyTurnStage } from './stages.js';
18
- import { appendUserInput, } from './state.js';
19
+ import { appendUserInput, recordStepEvent, } from './state.js';
19
20
  import { executeAttempt } from './steps.js';
20
21
  import { isWithheldOnBlock, shownToHost } from './stream.js';
21
22
  /** A turn whose final output the egress check blocked (withheld or replaced by policy copy). */
@@ -386,6 +387,31 @@ async function handleValidationGate(validation, flow, state, profile, latestStru
386
387
  holdReply(state, withoutItems(reply, refused), refused, outcome.failed);
387
388
  return 'pass';
388
389
  }
390
+ /**
391
+ * An attempt on a turn that answers by tool, ended without calling the answer tool: a repair while
392
+ * `outputs.validation` has a retry left, else a `bad_response` failure.
393
+ */
394
+ function* handleMissingAnswer(flow, state, profile, latestStructured,
395
+ /** True when the attempt's events go out at its end (`yieldBufferedAttemptEvents`). */
396
+ held) {
397
+ if (latestStructured !== undefined || state.lastStop?.kind !== 'completed')
398
+ return 'pass';
399
+ if (!answersByTool(flow.currentGen))
400
+ return 'pass';
401
+ const validation = profileTurnOutputs(profile)?.validation;
402
+ if (validation && flow.currentAttempt < (validation.maxRetries ?? 0)) {
403
+ const rejection = labelText('structured.answer_missing', { tool: ANSWER_TOOL }, profile.labels);
404
+ const nextRequest = buildRepairRequest(flow.currentReq, collectAttemptText(state.attemptEvents), rejection);
405
+ yield updateFlowForRetry(flow, state, profile, nextRequest, 'validation');
406
+ return 'continue';
407
+ }
408
+ const failure = toErrorEvent(new TheoremError('bad_response', `the model ended without calling ${ANSWER_TOOL}`));
409
+ recordStepEvent(failure, state);
410
+ state.lastStop = { kind: 'provider_error' };
411
+ if (!held)
412
+ yield failure;
413
+ return 'pass';
414
+ }
389
415
  /** Puts on the attempt's structured output the fields that still failed. */
390
416
  function markFailed(state, failed) {
391
417
  const held = findLast(state.attemptEvents, (event) => event.type === 'structured');
@@ -403,9 +429,6 @@ function gateStatusToAction(status) {
403
429
  }
404
430
  async function* executeSingleAttemptCycle(args) {
405
431
  const { flow, state, profile, system, provider } = args;
406
- const validation = profileTurnOutputs(profile)?.validation;
407
- const policy = resolveGuardrailPolicy(profile.guardrails);
408
- const judged = replyIsJudged(policy, TURN_REPLY);
409
432
  // invariant: Fresh maxSteps budget per validation/egress attempt. before_end inject
410
433
  // re-entry inside this cycle still accumulates stepCount (do not reset there).
411
434
  state.stepCount = 0;
@@ -465,6 +488,14 @@ async function* executeSingleAttemptCycle(args) {
465
488
  }
466
489
  break;
467
490
  }
491
+ return yield* checkAttemptReply(flow, state, profile, system, latestStructured);
492
+ }
493
+ /** The checks on an attempt's finished reply: the egress judge, the answer, then validation. */
494
+ async function* checkAttemptReply(flow, state, profile, system, structured) {
495
+ const validation = profileTurnOutputs(profile)?.validation;
496
+ const policy = resolveGuardrailPolicy(profile.guardrails);
497
+ const judged = replyIsJudged(policy, TURN_REPLY);
498
+ let latestStructured = structured;
468
499
  if (judged) {
469
500
  const status = yield* handleEgressGate(policy, flow, state, profile, system.private);
470
501
  const action = gateStatusToAction(status);
@@ -473,6 +504,9 @@ async function* executeSingleAttemptCycle(args) {
473
504
  }
474
505
  latestStructured = projectOutbound(state.attemptEvents).structured ?? latestStructured;
475
506
  }
507
+ if ((yield* handleMissingAnswer(flow, state, profile, latestStructured, Boolean(validation) || judged)) === 'continue') {
508
+ return { status: 'continue' };
509
+ }
476
510
  if (validation) {
477
511
  const status = await handleValidationGate(validation, flow, state, profile, latestStructured);
478
512
  if (status !== 'pass') {