@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.
- package/CHANGELOG.md +20 -0
- package/dist/config/catalog-overrides.d.ts +2 -0
- package/dist/cost/standard-types.d.ts +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/internal/atomic-write.d.ts +2 -2
- package/dist/internal/git-exec.d.ts +1 -1
- package/dist/internal/git-exec.js +2 -2
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/native/models.js +1 -0
- package/dist/native/session/compaction.js +9 -1
- package/dist/native/session/image-context.d.ts +5 -0
- package/dist/native/session/image-context.js +46 -0
- package/dist/native/session/loop-events/external-handler.d.ts +27 -6
- package/dist/native/session/loop-events/external-handler.js +30 -10
- package/dist/native/session/loop-events/registry.js +1 -1
- package/dist/native/session/loop-events/types.d.ts +33 -0
- package/dist/native/session/transcript-images.d.ts +20 -0
- package/dist/native/session/transcript-images.js +247 -0
- package/dist/native/session/transcript-store.d.ts +6 -0
- package/dist/native/session/transcript-store.js +39 -6
- package/dist/native/session/turn-compaction-step.js +2 -1
- package/dist/native/session/turn-complete-step.js +4 -1
- package/dist/native/session/turn-loop.js +83 -4
- package/dist/native/session-adapter.d.ts +2 -0
- package/dist/native/session-adapter.js +14 -0
- package/dist/session/agent-session-errors.d.ts +1 -1
- package/dist/session/agent-session-turn.d.ts +2 -1
- package/dist/session/agent-session-turn.js +6 -5
- package/dist/session/agent-session-types.d.ts +16 -1
- package/dist/session/agent-session.js +58 -2
- package/dist/session/image-input.d.ts +15 -0
- package/dist/session/image-input.js +46 -0
- package/dist/session/native-backend.js +1 -0
- package/dist/session/session-backend.d.ts +5 -1
- package/dist/session/session-types.d.ts +3 -1
- 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
|
});
|
package/dist/internal.d.ts
CHANGED
|
@@ -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";
|
package/dist/native/models.js
CHANGED
|
@@ -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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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 =
|
|
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>;
|