UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

80 lines (79 loc) • 5.17 kB
/** * design/171 §5.2/§6.4 — THE single projection point for human-input speaker envelopes. * * One renderer covers every human-input carrier: the core-side five (objective / live steer / * nextTurn / the resume tail's parked-steer frames / the wake message) and a serving layer's own * carriers through the same contract. The division of labor is fixed (ruled 2026-08-05): the INGRESS * sanitizes text and mints the {@link ActorAssertion}; `projectHumanInput` only PROJECTS — it renders * the speaker label ahead of the text, and it never sanitizes, truncates, or rewrites the text itself. * * The module also owns the DELIVERY frame that tells the model where a human frame arrived * ({@link frameMidTurnUserInput}, backlog #389). That is a separate concern from attribution and runs * AFTER projection (the speaker label belongs to the text; the delivery frame belongs around it) — it is * housed here so every model-facing wrapper for human input has one home. * * Envelope contract: * - no `actor` ⇒ BYTE-IDENTICAL passthrough. Every pre-171 caller and every single-user host * supplies no actor, so the model-facing bytes of their runs are pinned unchanged. * - `source: "system"` ⇒ passthrough regardless of actor: machine text (hook wake buses, engine * notes) is never rendered as human speech. * - otherwise ⇒ `[from "<id>"]` (plus an explicit `(unverified)` mark when the host did not derive * the identity from ingress credentials) on its own line ahead of the text. The label is DISPLAY * ONLY — an attribution, never authority (the normative clause on {@link ActorAssertion}). * * The label goes through the same two neutralizers as the external notification's `from="…"` header * (`inlineUntrusted` + `attrEscape` — one-line, capped, break-out-defused, quote-escaped), so a * untrusted id can never close the `[from "…"]` frame, escape an enclosing `<system-reminder>`, or * carry across a fake `(unverified)`-free identity. The projection runs BEFORE trust framing at every * call site, so the annotation always sits INSIDE the frame it attributes (a trusted frame's * reminder wrapper, an untrusted frame's fence) and can never be separated from its text. */ import { type ActorAssertion } from "../internal/llm.js"; import type { HumanInputEvent, HumanInputSource, TaskEventIdentity } from "./types.js"; export type { HumanInputSource, HumanInputDelivery, HumanInputEvent } from "./types.js"; /** One human-input frame as the projection point receives it: ingress-sanitized text + the * ingress-minted assertion (absent = anonymous). */ export interface HumanInputFrame { /** The input text, ALREADY sanitized by the ingress that accepted it. Returned untouched. */ text: string; /** Who spoke — absent means the caller supplied no attribution (every pre-171 caller). */ actor?: ActorAssertion; source: HumanInputSource; } /** * backlog #389 伴生 (V-3 of the steer/interrupt/lifecycle anchor audit) — wrap ONE live caller steer in * the mid-turn delivery frame. * * Scope, deliberately: the UNTRUSTED caller lane only. A `trusted` steer already rides a * `<system-reminder>` (supervisor authority, CC's own non-user-source framing), and a delegated child's * operator steer carries its own explicit operator frame — the bare lane was exactly the most-travelled * one: a person typing while the run works. * * Static strings (no per-call interpolation), and a blank projection is returned VERBATIM so the * harness's empty-injection no-op stays a no-op — a frame must never materialize from nothing. */ export declare function frameMidTurnUserInput(projected: string): string; /** * Render the speaker envelope for one human-input frame. Pure; see the module contract above. * Call BEFORE any trust framing (`formatHookFeedback` / `delimitUntrusted`) so the label stays * inside the frame it attributes. */ export declare function projectHumanInput(frame: HumanInputFrame): string; /** design/171 §6.2 — the one constructor for the `human_input` TaskEvent arm: mints the `inputId` * when the caller has none and materializes NO absent optional keys (absence is the semantic — * see the three-valued attribution note on the arm in core/types.ts). */ export declare function buildHumanInputEvent(input: Omit<HumanInputEvent, "type" | "inputId" | keyof TaskEventIdentity> & { inputId?: string; }): HumanInputEvent; /** The one constructor of the `"applied"` frame for an input that entered a model turn on a core carrier: the resume replay * (a parked entry drained into the continuation prompt) and the live lane (a caller steer that became a transcript message) * both mint here, so the two paths say the same word with the same attribution — `issuer` mirrored from `actor.issuer`, * `principal` when the leg has one, no `entryId` (the frame precedes the commit). */ export declare function buildAppliedHumanInputEvent(input: { carrier: string; source: HumanInputSource; sessionSeq: number; inputId: string; actor?: ActorAssertion | undefined; principal?: string | undefined; }): HumanInputEvent;