@sema-agent/core
Version:
Stateless, task-oriented AI agent core
80 lines (79 loc) • 5.17 kB
TypeScript
/**
* 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;