@sema-agent/core
Version:
Stateless, task-oriented AI agent core
80 lines (79 loc) • 4.59 kB
TypeScript
/**
* The `Artifact` tool: one tool, `action`-multiplexed, assembled from the host's capability declaration.
* The action enum, the parameter set and the description are all functions of `capabilities` — the
* implementation behind the seam is never inspected. Session state (the version tracked per URL and the
* local-path → URL binding that makes "same file path ⇒ same page" hold) lives in this factory's closure,
* fresh per run, never checkpointed.
*/
import { type PermissionMode } from "../../core/ask-origin.js";
import { type EngineNotice } from "../../core/engine-notice.js";
import { type ArtifactHost, type ArtifactHostCapabilities } from "../../core/artifact-host.js";
import { type OnAsk } from "../../core/tool-policy.js";
import type { ToolSpec } from "../../core/types.js";
/** What the engine's local-file reader hands back: the canonical key (the path-binding key) and the bytes, or one of
* three refusals — the fence's own (`refused`, with its text and card), a file over the byte budget the caller
* passed (judged on metadata BEFORE any read), or a path that is not a regular file. */
export type ArtifactLocalRead = {
ok: true;
key: string;
bytes: Uint8Array;
} | {
ok: false;
reason: "refused";
text: string;
details?: unknown;
} | {
ok: false;
reason: "too_large";
size: number;
limit: number;
judgedOn: "metadata" | "bytes";
} | {
ok: false;
reason: "not_a_file";
kind: string;
};
export interface ArtifactToolOptions {
host: ArtifactHost;
/** The host's declaration, already validated by `readArtifactHostCapabilities`. */
capabilities: ArtifactHostCapabilities;
runId: string;
taskId: string;
sessionId?: string;
/** The spawned name this run carries (audit attribution on every call context). */
agentName?: string;
/** The engine's fs-gated single read of a model-named local file (one read per call; the bytes read are the bytes
* published); `maxBytes` is the budget left for this file, judged on metadata before the read. */
readLocalFile: (path: string, maxBytes: number, signal?: AbortSignal) => Promise<ArtifactLocalRead>;
/** The run's effective approver — the same frozen seat the tool gate resolves asks at (`frozenOnAsk` =
* `spec.onAsk ?? deps.onAsk`), riding the MOUNT dep under the same rule as the workflow mount's `parentOnAsk`: a core
* mount's execute ctx is minimal, so the seat never rides `ctx`. The shared-read consent gate resolves at it. Absent
* = the run has no ask face, and a shared read is then REFUSED fail-closed (`artifact.refused`, content withheld) —
* never silently delivered. */
onAsk?: OnAsk;
/** The run's effective permission mode, read at each shared-read consent (a steer moves it). The consent ask is
* arbitrated as the gate's asks are (class `artifact_shared_read`): `bypassPermissions` RELEASES the read — delivered,
* announced once per run through `onNotice`, and NOT remembered as an approval (a mode release is not a person's
* nod; a later `default` leg asks) — `dontAsk` refuses it, every other mode asks the approver. Absent ⇒ no arbitration
* (the approver decides, or the read is withheld with none). */
permissionMode?: () => PermissionMode;
/** The notice sink the release account rides (`permission.ask_released_by_mode`, once per run for this class). */
onNotice?: (notice: EngineNotice) => void;
}
/** Supporting files per publish (upstream cap); enforced in the schema AND in the body, for callers that bypass the schema. */
export declare const MAX_FILES_PER_PUBLISH = 255;
export declare function mediaTypeOf(path: string): string;
export declare function isTextMedia(mediaType: string | undefined): boolean;
/** One opaque key per HOST CALL (`occurrence` counts prior host calls made under that tool-call id in this run — so a
* gateway that reuses a call id across turns never replays, and neither do two host calls of ONE execution), stable
* for any retry the host itself makes of that call: the host's idempotency key. */
export declare function artifactAttemptId(runId: string, toolCallId: string, occurrence?: number): string;
/** A published path: relative, forward-slash, no dot segments, no NUL, no backslash; returns the NFC form or the refusal. */
export declare function normalizePublishedPath(raw: string): {
ok: true;
path: string;
} | {
ok: false;
reason: string;
};
export declare function createArtifactTool(opts: ArtifactToolOptions): ToolSpec;