@nathapp/nax-agent 0.91.7-canary.8 → 0.91.7

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 (38) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/config/catalog-overrides.d.ts +2 -0
  3. package/dist/cost/standard-types.d.ts +1 -1
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +1 -0
  6. package/dist/internal/atomic-write.d.ts +2 -2
  7. package/dist/internal/git-exec.d.ts +1 -1
  8. package/dist/internal/git-exec.js +2 -2
  9. package/dist/internal.d.ts +1 -0
  10. package/dist/internal.js +1 -0
  11. package/dist/native/models.js +1 -0
  12. package/dist/native/session/compaction.js +9 -1
  13. package/dist/native/session/image-context.d.ts +5 -0
  14. package/dist/native/session/image-context.js +46 -0
  15. package/dist/native/session/loop-events/external-handler.d.ts +27 -6
  16. package/dist/native/session/loop-events/external-handler.js +30 -10
  17. package/dist/native/session/loop-events/registry.js +1 -1
  18. package/dist/native/session/loop-events/types.d.ts +33 -0
  19. package/dist/native/session/transcript-images.d.ts +20 -0
  20. package/dist/native/session/transcript-images.js +247 -0
  21. package/dist/native/session/transcript-store.d.ts +6 -0
  22. package/dist/native/session/transcript-store.js +39 -6
  23. package/dist/native/session/turn-compaction-step.js +2 -1
  24. package/dist/native/session/turn-complete-step.js +4 -1
  25. package/dist/native/session/turn-loop.js +83 -4
  26. package/dist/native/session-adapter.d.ts +2 -0
  27. package/dist/native/session-adapter.js +14 -0
  28. package/dist/session/agent-session-errors.d.ts +1 -1
  29. package/dist/session/agent-session-turn.d.ts +2 -1
  30. package/dist/session/agent-session-turn.js +6 -5
  31. package/dist/session/agent-session-types.d.ts +16 -1
  32. package/dist/session/agent-session.js +58 -2
  33. package/dist/session/image-input.d.ts +15 -0
  34. package/dist/session/image-input.js +46 -0
  35. package/dist/session/native-backend.js +1 -0
  36. package/dist/session/session-backend.d.ts +5 -1
  37. package/dist/session/session-types.d.ts +3 -1
  38. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
5
5
  release may change the public API.
6
6
 
7
+ ## [0.91.7] - 2026-11-01
8
+
9
+ - No changes in this package. Released in lockstep with the other nax packages.
10
+
11
+ ## [0.91.7-canary.12] - 2026-11-01
12
+
13
+ - No changes in this package. Released in lockstep with the other nax packages.
14
+
15
+ ## [0.91.7-canary.11] - 2026-10-31
16
+
17
+ - No changes in this package. Released in lockstep with the other nax packages.
18
+
19
+ ## [0.91.7-canary.10] - 2026-10-30
20
+
21
+ - No changes in this package. Released in lockstep with the other nax packages.
22
+
23
+ ## [0.91.7-canary.9] - 2026-10-30
24
+
25
+ - No changes in this package. Released in lockstep with the other nax packages.
26
+
7
27
  ## [0.91.7-canary.8] - 2026-10-29
8
28
 
9
29
  - No changes in this package. Released in lockstep with the other nax packages.
@@ -82,6 +82,8 @@ export interface CatalogModelOverride {
82
82
  * than the newer model's real one (nax-ai 0.1.11, nax#1982).
83
83
  */
84
84
  maxTokens?: number;
85
+ /** Accepted input modalities; omit to inherit known catalog metadata. */
86
+ input?: ("text" | "image")[];
85
87
  supportsTools: boolean;
86
88
  thinkingLevels: ThinkingLevel[];
87
89
  pricing: CatalogPricing;
@@ -6,4 +6,4 @@
6
6
  * keeps a single allow-listed file. In S1-5 this becomes a re-export in
7
7
  * packages/nax-agent and the allow-list entry is removed.
8
8
  */
9
- export type { Pricing, PricingRates, PricingTier, TokenUsage } from "@nathapp/nax-ai";
9
+ export type { ImageAttachment, Pricing, PricingRates, PricingTier, TokenUsage } from "@nathapp/nax-ai";
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export type { CommandInterceptor, InterceptRequest, InterceptResult, ShellInterc
11
11
  export { appendCommandSafetyRow, type BuildCommandShadowOptions, buildCommandShadow, buildRequest, type Classify, COMMAND_SAFETY_DIR, type CommandGuard, type CommandSafetyRow, type CommandShadow, type CommandShadowOptions, createCommandGuard, createCommandShadow, createSystemOneClient, detectTmpWrite, type ExecRun, type FinalOutcome, type GuardDecision, type GuardInput, HARM_OPTIONS, HARM_QUESTION_ID, type HarmOption, isTempOnly, type LedgerOutcome, type MechanicalVerdict, type ModelAnswers, type ModelResult, type Observation, openShadowTap, parseAnswer, QUESTION_IDS, QUESTION_SET_VERSION, type QuestionId, RULE_SET_VERSION, type RuleContext, type RuleResult, type ShadowCall, type ShadowTap, SYSTEMONE_MODEL_LABEL, type SystemOneClientOptions, type SystemOneQuestion, type SystemOneRequest, scoreGuard, scoreRules, shadowCacheKey, toMechanical, } from "#src/command-safety/index";
12
12
  export { addTokenUsage, estimateCostUsd, inputClassTokens, type Pricing, type PricingRates, type PricingTier, priceCall, type TokenUsage, } from "#src/cost/core/index";
13
13
  export { type ModelSpec, parseModelSpec, } from "#src/cost/model-spec";
14
+ export type { ImageAttachment } from "#src/cost/standard-types";
14
15
  export type { AgentLogger, CredentialAuthConfig, CredentialsConfig } from "#src/infra/index";
15
16
  export { configureCredentials, getLogger, setAgentLogger } from "#src/infra/index";
16
17
  export { createStderrTail, type StderrTail } from "#src/infra/stderr-tail";
@@ -31,8 +32,9 @@ export { createSessionStorage, defaultSessionsDir, processAlive, LIST_PAGE_SIZE,
31
32
  export type { AdapterFailure } from "#src/session/adapter-failure";
32
33
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
33
34
  export { AgentSessionError, type AgentSessionErrorCode } from "#src/session/agent-session-errors";
34
- export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CommandRunResult, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
35
+ export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CommandRunResult, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionConfigureInput, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
35
36
  export type { AgentAwaitingHumanEvent, AgentCallEndedEvent, AgentCallStartedEvent, AgentMessageUpdateEvent, AgentProcessUpdateEvent, AgentStreamEvent, AgentStreamEventBase, AgentThinkingUpdateEvent, AgentToolCallUpdateEvent, AgentUsageUpdateEvent, } from "#src/session/agent-stream-events";
37
+ export { IMAGE_MIME_TYPES, IMAGE_TOKEN_ESTIMATE, MAX_IMAGE_BASE64_CHARS, MAX_IMAGES_PER_MESSAGE, MAX_REQUEST_IMAGE_BASE64_CHARS, MAX_REQUEST_IMAGES, type SendInput, validateImageAttachments, } from "#src/session/image-input";
36
38
  export { type AdapterInteraction, type AdapterInteractionResponse, type InteractionHandler, NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
37
39
  export { nativeBackend } from "#src/session/native-backend";
38
40
  export type { NativeBackendOptions } from "#src/session/native-backend-options";
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_S
25
25
  export { createSessionStorage, defaultSessionsDir, processAlive, LIST_PAGE_SIZE, LOCK_HEARTBEAT_MS, LOCK_STALE_MS, SESSION_ID_PATTERN, SESSION_META_MODES, } from "#src/session-store/index";
26
26
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
27
27
  export { AgentSessionError } from "#src/session/agent-session-errors";
28
+ export { IMAGE_MIME_TYPES, IMAGE_TOKEN_ESTIMATE, MAX_IMAGE_BASE64_CHARS, MAX_IMAGES_PER_MESSAGE, MAX_REQUEST_IMAGE_BASE64_CHARS, MAX_REQUEST_IMAGES, validateImageAttachments, } from "#src/session/image-input";
28
29
  export { NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
29
30
  export { nativeBackend } from "#src/session/native-backend";
30
31
  export { SessionTurnError, } from "#src/session/session-types";
@@ -1,6 +1,6 @@
1
1
  import { rename, rm } from "node:fs/promises";
2
2
  /** The one `writeFile` shape this module uses; narrower than the overloaded node signature so stubs type-check. */
3
- type WriteText = (path: string, content: string, options: {
3
+ type WriteText = (path: string, content: string | Uint8Array, options: {
4
4
  encoding: "utf8";
5
5
  mode?: number;
6
6
  }) => Promise<void>;
@@ -10,7 +10,7 @@ export declare const _atomicWriteDeps: {
10
10
  rename: typeof rename;
11
11
  rm: typeof rm;
12
12
  };
13
- export declare function writeFileAtomic(path: string, content: string, options?: {
13
+ export declare function writeFileAtomic(path: string, content: string | Uint8Array, options?: {
14
14
  mode?: number;
15
15
  }): Promise<void>;
16
16
  export {};
@@ -50,7 +50,7 @@ export declare function getGitRoot(workdir: string): Promise<string | null>;
50
50
  export declare function gitWithTimeout(args: string[], workdir: string, timeoutMs?: number, maxBytes?: number,
51
51
  /** Full argv INCLUDING argv[0]. Callers that must control argv[0] pass this;
52
52
  * everyone else gets ["git", ...args] as before. */
53
- argvOverride?: readonly string[]): Promise<{
53
+ argvOverride?: readonly string[], envOverlay?: Readonly<Record<string, string>>): Promise<{
54
54
  stdout: string;
55
55
  stderr: string;
56
56
  exitCode: number;
@@ -71,10 +71,10 @@ export async function getGitRoot(workdir) {
71
71
  export async function gitWithTimeout(args, workdir, timeoutMs = _gitDeps.gitTimeoutMs, maxBytes,
72
72
  /** Full argv INCLUDING argv[0]. Callers that must control argv[0] pass this;
73
73
  * everyone else gets ["git", ...args] as before. */
74
- argvOverride) {
74
+ argvOverride, envOverlay) {
75
75
  const proc = _gitDeps.spawn(hardenedGitArgv(argvOverride ?? ["git", ...args]), {
76
76
  cwd: workdir,
77
- env: hardenedGitEnv(process.env),
77
+ env: hardenedGitEnv(envOverlay === undefined ? process.env : { ...process.env, ...envOverlay }),
78
78
  stdout: "pipe",
79
79
  stderr: "pipe",
80
80
  });
@@ -105,3 +105,4 @@ export * from "#src/tools/run-command-exec";
105
105
  export * from "#src/tools/runtime";
106
106
  export * from "#src/tools/tool-audit";
107
107
  export * as truncateModule from "#src/tools/truncate";
108
+ export { _transcriptImagesDeps } from "./native/session/transcript-images.ts";
package/dist/internal.js CHANGED
@@ -106,3 +106,4 @@ export * from "#src/tools/run-command-exec";
106
106
  export * from "#src/tools/runtime";
107
107
  export * from "#src/tools/tool-audit";
108
108
  export * as truncateModule from "#src/tools/truncate";
109
+ export { _transcriptImagesDeps } from "./native/session/transcript-images.js";
@@ -118,6 +118,7 @@ export function toProviderOverrides(overrides) {
118
118
  // Omitted when the override declares none: nax-ai then falls back to the
119
119
  // template sibling's ceiling, which is the only honest value available.
120
120
  ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}),
121
+ ...(model.input !== undefined ? { input: model.input } : {}),
121
122
  ...(model.thinkingFormat !== undefined ? { thinkingFormat: model.thinkingFormat } : {}),
122
123
  supportsTools: model.supportsTools,
123
124
  thinkingLevels: model.thinkingLevels,
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * See docs/superpowers/specs/2026-09-04-native-context-compaction-design.md.
9
9
  */
10
+ import { IMAGE_TOKEN_ESTIMATE } from "#src/session/image-input";
10
11
  /** A reply needs room whatever the window is. */
11
12
  const MIN_HEADROOM_TOKENS = 4096;
12
13
  /** ...but never take a quarter of a small window for headroom. */
@@ -18,9 +19,16 @@ const CHARS_PER_TOKEN = 4;
18
19
  */
19
20
  export function estimateTokens(message) {
20
21
  let chars = 0;
22
+ let imageTokens = 0;
21
23
  switch (message.role) {
22
24
  case "user":
23
25
  chars = message.content.length;
26
+ for (const image of message.images ?? []) {
27
+ imageTokens +=
28
+ image.width !== undefined && image.height !== undefined
29
+ ? Math.min(IMAGE_TOKEN_ESTIMATE, Math.ceil((image.width * image.height) / 750))
30
+ : IMAGE_TOKEN_ESTIMATE;
31
+ }
24
32
  break;
25
33
  case "assistant":
26
34
  chars = message.content.length;
@@ -33,7 +41,7 @@ export function estimateTokens(message) {
33
41
  chars = message.content.length;
34
42
  break;
35
43
  }
36
- return Math.ceil(chars / CHARS_PER_TOKEN);
44
+ return Math.ceil(chars / CHARS_PER_TOKEN) + imageTokens;
37
45
  }
38
46
  /**
39
47
  * Anchor on truth, guess only the tail.
@@ -0,0 +1,5 @@
1
+ import type { TranscriptMessage } from "./compaction.ts";
2
+ /** Text-only summary input view; never rewrite the image-bearing transcript. */
3
+ export declare function imagesAsNotes(messages: readonly TranscriptMessage[]): readonly TranscriptMessage[];
4
+ /** Bound only the wire copy, preserving transcript data and dormant array identity. */
5
+ export declare function capRequestImages(messages: readonly TranscriptMessage[]): readonly TranscriptMessage[];
@@ -0,0 +1,46 @@
1
+ import { MAX_REQUEST_IMAGE_BASE64_CHARS, MAX_REQUEST_IMAGES } from "#src/session/image-input";
2
+ /** Text-only summary input view; never rewrite the image-bearing transcript. */
3
+ export function imagesAsNotes(messages) {
4
+ return messages.map((message) => {
5
+ if (message.role !== "user" || message.images === undefined)
6
+ return message;
7
+ const { images, ...textMessage } = message;
8
+ const notes = images.map((image) => {
9
+ const dimensions = image.width !== undefined && image.height !== undefined ? ` ${image.width}x${image.height}` : "";
10
+ return `[image: ${image.mimeType}${dimensions}]`;
11
+ });
12
+ const content = [...(message.content === "" ? [] : [message.content]), ...notes].join("\n");
13
+ return { ...textMessage, content };
14
+ });
15
+ }
16
+ /** Bound only the wire copy, preserving transcript data and dormant array identity. */
17
+ export function capRequestImages(messages) {
18
+ let count = 0;
19
+ let chars = 0;
20
+ let bounded;
21
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
22
+ const message = messages[index];
23
+ if (message?.role !== "user" || !message.images?.length)
24
+ continue;
25
+ const kept = new Set();
26
+ for (let i = message.images.length - 1; i >= 0; i -= 1) {
27
+ const image = message.images[i];
28
+ if (image && count < MAX_REQUEST_IMAGES && chars + image.data.length <= MAX_REQUEST_IMAGE_BASE64_CHARS) {
29
+ kept.add(i);
30
+ count += 1;
31
+ chars += image.data.length;
32
+ }
33
+ }
34
+ if (kept.size === message.images.length)
35
+ continue;
36
+ bounded ??= [...messages];
37
+ const images = message.images.filter((_, i) => kept.has(i));
38
+ const notes = message.images
39
+ .filter((_, i) => !kept.has(i))
40
+ .map((image) => `[image from an earlier turn dropped from context: ${image.mimeType}]`);
41
+ const { images: _images, ...textMessage } = message;
42
+ const content = [...(message.content === "" ? [] : [message.content]), ...notes].join("\n");
43
+ bounded[index] = { ...textMessage, content, ...(images.length ? { images } : {}) };
44
+ }
45
+ return bounded ?? messages;
46
+ }
@@ -19,20 +19,41 @@ import type { HandlerOf, LoopEvent, LoopHandlerContext, LoopHandlerEntry } from
19
19
  /**
20
20
  * How long a plugin handler may run before the wrapper answers for it. A
21
21
  * constant by ruling, not a `NaxConfig` key: it bounds the turn's liveness, it
22
- * is not a policy a project tunes per package.
22
+ * is not a policy a project tunes per package. An entry with its own
23
+ * `timeoutMs` replaces it for that handler alone (US-001 — see `deadlineFor`).
23
24
  */
24
25
  export declare const LOOP_HANDLER_TIMEOUT_MS = 10000;
25
26
  /**
26
- * Test seam for the deadline only. Read at DISPATCH time, never captured when
27
- * the handler is wrapped — a wrapped handler is installed once and dispatched
28
- * many times.
27
+ * The handle the deadline timer is armed with. Bun's `Timer` and a virtual
28
+ * clock's numeric id are the same thing at runtime, and the test seam below
29
+ * carries either.
29
30
  */
30
- export declare const _externalHandlerDeps: {
31
+ type TimerHandle = number | ReturnType<typeof setTimeout>;
32
+ interface ExternalHandlerDeps {
33
+ /**
34
+ * Test seam for the deadline only. Read at DISPATCH time, never captured
35
+ * when the handler is wrapped — a wrapped handler is installed once and
36
+ * dispatched many times.
37
+ */
31
38
  timeoutMs: number;
32
- };
39
+ /**
40
+ * The timer pair the deadline is armed on. Injected rather than called on the
41
+ * globals so a test can drive the deadline on `makeFakeClock()` instead of
42
+ * sleeping for it (`docs/guides/testing-rules.md` §3 forbids a fixed sleep,
43
+ * and "wait a real 100 ms and hope the 10 ms default lost the race" is the
44
+ * flake it forbids).
45
+ *
46
+ * The defaults READ the globals at call time instead of capturing them, so a
47
+ * `withTimerSpy` substitution still observes the timer the wrapper arms.
48
+ */
49
+ setTimeout(handler: () => void, delayMs: number): TimerHandle;
50
+ clearTimeout(handle: TimerHandle | undefined): void;
51
+ }
52
+ export declare const _externalHandlerDeps: ExternalHandlerDeps;
33
53
  /**
34
54
  * Wrap one staged plugin registration into a built-in-shaped `HandlerOf<E>`:
35
55
  * same payload, same patch, but with a bounded lifetime, the turn's context,
36
56
  * and a failure that is attributed and never rethrown.
37
57
  */
38
58
  export declare function wrapExternalHandler<E extends LoopEvent>(entry: LoopHandlerEntry, getCtx: () => LoopHandlerContext, signal: AbortSignal): HandlerOf<E>;
59
+ export {};
@@ -21,17 +21,33 @@ import { isCompleteBeforeToolOutcome } from "./registry.js";
21
21
  /**
22
22
  * How long a plugin handler may run before the wrapper answers for it. A
23
23
  * constant by ruling, not a `NaxConfig` key: it bounds the turn's liveness, it
24
- * is not a policy a project tunes per package.
24
+ * is not a policy a project tunes per package. An entry with its own
25
+ * `timeoutMs` replaces it for that handler alone (US-001 — see `deadlineFor`).
25
26
  */
26
27
  export const LOOP_HANDLER_TIMEOUT_MS = 10_000;
27
- /**
28
- * Test seam for the deadline only. Read at DISPATCH time, never captured when
29
- * the handler is wrapped — a wrapped handler is installed once and dispatched
30
- * many times.
31
- */
32
28
  export const _externalHandlerDeps = {
33
29
  timeoutMs: LOOP_HANDLER_TIMEOUT_MS,
30
+ setTimeout: (handler, delayMs) => globalThis.setTimeout(handler, delayMs),
31
+ clearTimeout: (handle) => {
32
+ if (handle !== undefined)
33
+ globalThis.clearTimeout(handle);
34
+ },
34
35
  };
36
+ /**
37
+ * The deadline THIS dispatch is bounded by (US-001): the entry's own when it is
38
+ * a finite positive number, the module default otherwise.
39
+ *
40
+ * `0` and `Infinity` are not deadlines — one would answer before the handler
41
+ * was ever called, the other would disable the bound — so they fall back to the
42
+ * default rather than being trusted as an override. `NaN` is not finite and
43
+ * falls back the same way.
44
+ */
45
+ function deadlineFor(entry) {
46
+ const override = entry.timeoutMs;
47
+ if (override === undefined || !Number.isFinite(override) || override <= 0)
48
+ return _externalHandlerDeps.timeoutMs;
49
+ return override;
50
+ }
35
51
  /**
36
52
  * The patch fields whose presence is worth tracing: the ones a later
37
53
  * `applyHistoryPatch` can reject, so a rejection is attributable to a plugin.
@@ -53,10 +69,14 @@ const TRACED_PATCH_FIELDS = ["seed", "history", "messages", "summary"];
53
69
  *
54
70
  * `setTimeout` and not `Bun.sleep()`: the handle has to be cancelled mid-flight
55
71
  * (`clearTimeout`), which is the sanctioned exception to the Bun-native delay
56
- * rule.
72
+ * rule. Both go through `_externalHandlerDeps`, whose defaults forward to the
73
+ * globals at call time (see its doc).
74
+ *
75
+ * `timeoutMs` is the entry's own deadline when it has one — `deadlineFor` — so
76
+ * a long-running hook widens only its own bound.
57
77
  */
58
78
  function raceSettlement(entry, payload, deps) {
59
- const timeoutMs = _externalHandlerDeps.timeoutMs;
79
+ const timeoutMs = deadlineFor(entry);
60
80
  return new Promise((resolve) => {
61
81
  let settled = false;
62
82
  let timer;
@@ -65,12 +85,12 @@ function raceSettlement(entry, payload, deps) {
65
85
  return;
66
86
  settled = true;
67
87
  if (timer !== undefined)
68
- clearTimeout(timer);
88
+ _externalHandlerDeps.clearTimeout(timer);
69
89
  deps.signal.removeEventListener("abort", onAbort);
70
90
  resolve(result);
71
91
  };
72
92
  const onAbort = () => finish({ ok: false, reason: "aborted before it settled" });
73
- timer = setTimeout(() => finish({ ok: false, reason: `did not settle within ${timeoutMs}ms` }), timeoutMs);
93
+ timer = _externalHandlerDeps.setTimeout(() => finish({ ok: false, reason: `did not settle within ${timeoutMs}ms` }), timeoutMs);
74
94
  deps.signal.addEventListener("abort", onAbort, { once: true });
75
95
  // The context is read here, at dispatch, so a handler always sees the facts
76
96
  // of the turn it is running in. Read in its OWN try: a `getCtx` throw is
@@ -58,7 +58,7 @@ const PATCHABLE_FIELDS = {
58
58
  // A decision, not a field patch — chained in dispatchBeforeTool below.
59
59
  before_tool: [],
60
60
  after_tool: ["content", "isError"],
61
- before_turn: ["seed", "history"],
61
+ before_turn: ["seed", "history", "block", "promptContext"],
62
62
  transform_context: ["messages"],
63
63
  before_request: ["options"],
64
64
  after_response: ["text", "toolCalls", "thinking"],
@@ -99,6 +99,30 @@ export interface BeforeTurnPatch {
99
99
  readonly seed?: readonly NativeTranscriptMessage[];
100
100
  /** Honoured ONLY when payload.boundary is true (spec 3.2, 6.1). */
101
101
  readonly history?: readonly NativeTranscriptMessage[];
102
+ /**
103
+ * Refuse the turn (US-001). `runNativeTurn` throws `NATIVE_TURN_BLOCKED`
104
+ * carrying `reason` as its message BEFORE it appends anything to the
105
+ * transcript or makes a provider request, so a hook can stop a prompt
106
+ * reaching the model without seeing it first. `block` wins over every other
107
+ * field in the same patch.
108
+ *
109
+ * `reason` is typed, but a plugin's `loop-handlers` are raw JavaScript: when
110
+ * `reason` is missing, not a string, or blank, the turn is STILL refused and
111
+ * the message falls back to a text naming the reason as absent, so the
112
+ * refusal is never a bare `NATIVE_TURN_BLOCKED:` line.
113
+ */
114
+ readonly block?: {
115
+ readonly reason: string;
116
+ };
117
+ /**
118
+ * Context to PREFIX to the first message the turn seeds (US-001).
119
+ *
120
+ * `seed` cannot carry it: that patch REPLACES the prompt message, and the
121
+ * prompt's images go with it. A prefix leaves the caller's message — text,
122
+ * images and all — exactly as it was, with the context above it. An empty
123
+ * string is no context, so the message is untouched.
124
+ */
125
+ readonly promptContext?: string;
102
126
  }
103
127
  /** `transform_context` — fires before every provider request attempt. */
104
128
  export interface TransformContextPayload {
@@ -248,6 +272,15 @@ export interface LoopHandlerEntry {
248
272
  readonly plugin: string;
249
273
  readonly event: LoopEvent;
250
274
  readonly handler: ExternalHandlerOf<LoopEvent>;
275
+ /**
276
+ * How long THIS handler may run before `wrapExternalHandler` answers for it,
277
+ * replacing `LOOP_HANDLER_TIMEOUT_MS` (US-001). A formatter or type check
278
+ * after an edit routinely needs longer than the 10 s default; a handler that
279
+ * does not set it keeps that default, so widening one hook never widens the
280
+ * rest. Only a FINITE POSITIVE number is a deadline — `0` and `Infinity` are
281
+ * not, and fall back to the default rather than disabling the bound.
282
+ */
283
+ readonly timeoutMs?: number;
251
284
  }
252
285
  /**
253
286
  * Every plugin-contributed handler for a run, in install order. The registry
@@ -0,0 +1,20 @@
1
+ import { type FileHandle, link, lstat, mkdir, open, realpath, rm } from "node:fs/promises";
2
+ import { writeFileAtomic } from "#src/internal/atomic-write";
3
+ import type { TranscriptDoc } from "./transcript-types.ts";
4
+ /** Filesystem operations separate from the atomic writer's write/rename seam. */
5
+ export declare const _transcriptImagesDeps: {
6
+ link: typeof link;
7
+ mkdir: typeof mkdir;
8
+ open: typeof open;
9
+ realpath: typeof realpath;
10
+ lstat: typeof lstat;
11
+ readFile: (path: string | FileHandle) => Promise<NonSharedBuffer>;
12
+ rm: typeof rm;
13
+ stat: (path: string) => Promise<import("node:fs").Stats>;
14
+ writeFileAtomic: typeof writeFileAtomic;
15
+ };
16
+ export declare function transcriptImagesPath(path: string): string;
17
+ /** Preflight the WHOLE document before even creating the image directory. */
18
+ export declare function splitTranscriptImages(path: string, doc: TranscriptDoc): Promise<unknown>;
19
+ /** Decode the private disk format; callers only ever see inline attachments. */
20
+ export declare function rehydrateTranscriptImages(path: string, value: object): Promise<TranscriptDoc>;