@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
|
@@ -4,6 +4,7 @@ import { AgentSessionError } from "./agent-session-errors.js";
|
|
|
4
4
|
import { resolveAgentSessionOptions } from "./agent-session-options.js";
|
|
5
5
|
import { checkBackendKind, interruptedTurnOf, loadResumable, resumeInput } from "./agent-session-resume.js";
|
|
6
6
|
import { claimTurn } from "./agent-session-turn.js";
|
|
7
|
+
import { validateImageAttachments } from "./image-input.js";
|
|
7
8
|
import { createPendingAskTable } from "./pending-asks.js";
|
|
8
9
|
import { createSessionAskPort } from "./session-ask-port.js";
|
|
9
10
|
/** The handler's turn signal between turns: never aborts. */
|
|
@@ -24,6 +25,8 @@ class FacadeAgentSession {
|
|
|
24
25
|
active;
|
|
25
26
|
last;
|
|
26
27
|
closing;
|
|
28
|
+
/** True while the backend's configure is pending: send, runCommand and configure refuse BUSY. */
|
|
29
|
+
configuring = false;
|
|
27
30
|
constructor(parts, last) {
|
|
28
31
|
this.parts = parts;
|
|
29
32
|
this.last = last;
|
|
@@ -44,7 +47,13 @@ class FacadeAgentSession {
|
|
|
44
47
|
get lastTurn() {
|
|
45
48
|
return this.last;
|
|
46
49
|
}
|
|
47
|
-
|
|
50
|
+
async acceptsImages() {
|
|
51
|
+
if (this.closing !== undefined) {
|
|
52
|
+
throw new AgentSessionError(`Session "${this.id}" is closed`, "AGENT_SESSION_CLOSED", { sessionId: this.id });
|
|
53
|
+
}
|
|
54
|
+
return this.parts.opened.acceptsImages?.() ?? false;
|
|
55
|
+
}
|
|
56
|
+
send(message, input) {
|
|
48
57
|
if (this.closing !== undefined) {
|
|
49
58
|
throw new AgentSessionError(`Session "${this.id}" is closed`, "AGENT_SESSION_CLOSED", { sessionId: this.id });
|
|
50
59
|
}
|
|
@@ -53,7 +62,24 @@ class FacadeAgentSession {
|
|
|
53
62
|
sessionId: this.id,
|
|
54
63
|
});
|
|
55
64
|
}
|
|
65
|
+
if (this.configuring) {
|
|
66
|
+
throw new AgentSessionError(`Session "${this.id}" is being configured`, "AGENT_SESSION_BUSY", {
|
|
67
|
+
sessionId: this.id,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const images = input?.images;
|
|
71
|
+
const reason = images === undefined ? undefined : validateImageAttachments(images);
|
|
72
|
+
if (reason !== undefined) {
|
|
73
|
+
throw new AgentSessionError(reason, "AGENT_SESSION_INVALID_INPUT", { sessionId: this.id });
|
|
74
|
+
}
|
|
75
|
+
if (images?.length && this.parts.opened.acceptsImages === undefined) {
|
|
76
|
+
throw new AgentSessionError(`Session "${this.id}" cannot receive images`, "AGENT_SESSION_CAPABILITY_UNSUPPORTED", {
|
|
77
|
+
sessionId: this.id,
|
|
78
|
+
capability: "images",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
56
81
|
const turn = claimTurn(this.parts.ctx, message, {
|
|
82
|
+
images,
|
|
57
83
|
onStart: (live) => {
|
|
58
84
|
this.parts.slot.turn = live;
|
|
59
85
|
},
|
|
@@ -67,10 +93,40 @@ class FacadeAgentSession {
|
|
|
67
93
|
this.active = turn;
|
|
68
94
|
return turn.iterable;
|
|
69
95
|
}
|
|
96
|
+
async configure(input) {
|
|
97
|
+
const refusal = this.configureRefusal(input);
|
|
98
|
+
if (refusal !== undefined)
|
|
99
|
+
throw refusal;
|
|
100
|
+
const configure = this.parts.opened.configure;
|
|
101
|
+
if (configure === undefined)
|
|
102
|
+
throw this.refusal("AGENT_SESSION_CAPABILITY_UNSUPPORTED", { capability: "configure" });
|
|
103
|
+
this.configuring = true;
|
|
104
|
+
try {
|
|
105
|
+
await configure.call(this.parts.opened, input);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
this.configuring = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** The refusals that precede the backend call, in the documented order; the backend check is last and separate. */
|
|
112
|
+
configureRefusal(input) {
|
|
113
|
+
if (this.closing !== undefined)
|
|
114
|
+
return this.refusal("AGENT_SESSION_CLOSED");
|
|
115
|
+
if (this.active !== undefined || this.configuring)
|
|
116
|
+
return this.refusal("AGENT_SESSION_BUSY");
|
|
117
|
+
const { model, effort } = input;
|
|
118
|
+
if ((model === undefined && effort === undefined) || model === "" || effort === "") {
|
|
119
|
+
return this.refusal("AGENT_SESSION_INVALID_OPTIONS", { path: "configure" });
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
refusal(code, context = {}) {
|
|
124
|
+
return new AgentSessionError(`Session "${this.id}" cannot be configured`, code, { sessionId: this.id, ...context });
|
|
125
|
+
}
|
|
70
126
|
async runCommand(command, onEvent) {
|
|
71
127
|
const code = this.closing !== undefined
|
|
72
128
|
? "AGENT_SESSION_CLOSED"
|
|
73
|
-
: this.active !== undefined
|
|
129
|
+
: this.active !== undefined || this.configuring
|
|
74
130
|
? "AGENT_SESSION_BUSY"
|
|
75
131
|
: this.parts.opened.runCommand === undefined
|
|
76
132
|
? "AGENT_SESSION_CAPABILITY_UNSUPPORTED"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ImageAttachment } from "#src/cost/standard-types";
|
|
2
|
+
/** Supported encodings; dimensions are supplied by callers, not decoded here. */
|
|
3
|
+
export declare const IMAGE_MIME_TYPES: readonly string[];
|
|
4
|
+
export declare const MAX_IMAGES_PER_MESSAGE = 10;
|
|
5
|
+
/** Base64 characters per image (5 MiB encoded, 3.75 MiB raw). */
|
|
6
|
+
export declare const MAX_IMAGE_BASE64_CHARS = 5242880;
|
|
7
|
+
export declare const MAX_REQUEST_IMAGES = 20;
|
|
8
|
+
/** Base64 characters per model request and per send. */
|
|
9
|
+
export declare const MAX_REQUEST_IMAGE_BASE64_CHARS = 20971520;
|
|
10
|
+
export declare const IMAGE_TOKEN_ESTIMATE = 1600;
|
|
11
|
+
export type SendInput = {
|
|
12
|
+
readonly images?: readonly ImageAttachment[];
|
|
13
|
+
};
|
|
14
|
+
/** Return the first failure, in count, image order, then total-size order. */
|
|
15
|
+
export declare function validateImageAttachments(images: readonly ImageAttachment[]): string | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Supported encodings; dimensions are supplied by callers, not decoded here. */
|
|
2
|
+
export const IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
3
|
+
export const MAX_IMAGES_PER_MESSAGE = 10;
|
|
4
|
+
/** Base64 characters per image (5 MiB encoded, 3.75 MiB raw). */
|
|
5
|
+
export const MAX_IMAGE_BASE64_CHARS = 5_242_880;
|
|
6
|
+
export const MAX_REQUEST_IMAGES = 20;
|
|
7
|
+
/** Base64 characters per model request and per send. */
|
|
8
|
+
export const MAX_REQUEST_IMAGE_BASE64_CHARS = 20_971_520;
|
|
9
|
+
export const IMAGE_TOKEN_ESTIMATE = 1_600;
|
|
10
|
+
function validateImage(image) {
|
|
11
|
+
if (!IMAGE_MIME_TYPES.includes(image.mimeType)) {
|
|
12
|
+
return `Unsupported image MIME type: ${JSON.stringify(image.mimeType)}`;
|
|
13
|
+
}
|
|
14
|
+
if (image.data.length === 0)
|
|
15
|
+
return "Image data is empty";
|
|
16
|
+
if (image.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(image.data)) {
|
|
17
|
+
return "Image data is not base64";
|
|
18
|
+
}
|
|
19
|
+
if (image.data.length > MAX_IMAGE_BASE64_CHARS) {
|
|
20
|
+
return `Image data exceeds the per-image limit of ${MAX_IMAGE_BASE64_CHARS} base64 characters`;
|
|
21
|
+
}
|
|
22
|
+
for (const dimension of ["width", "height"]) {
|
|
23
|
+
const value = image[dimension];
|
|
24
|
+
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
|
|
25
|
+
return `Image ${dimension} must be a positive integer`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Return the first failure, in count, image order, then total-size order. */
|
|
31
|
+
export function validateImageAttachments(images) {
|
|
32
|
+
if (images.length > MAX_IMAGES_PER_MESSAGE) {
|
|
33
|
+
return `Image count exceeds the limit of ${MAX_IMAGES_PER_MESSAGE} images per message`;
|
|
34
|
+
}
|
|
35
|
+
let total = 0;
|
|
36
|
+
for (const image of images) {
|
|
37
|
+
const reason = validateImage(image);
|
|
38
|
+
if (reason !== undefined)
|
|
39
|
+
return reason;
|
|
40
|
+
total += image.data.length;
|
|
41
|
+
}
|
|
42
|
+
if (total > MAX_REQUEST_IMAGE_BASE64_CHARS) {
|
|
43
|
+
return `Image data exceeds the total limit of ${MAX_REQUEST_IMAGE_BASE64_CHARS} base64 characters`;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
@@ -132,6 +132,7 @@ async function openNative(resolved, ctx) {
|
|
|
132
132
|
return {
|
|
133
133
|
adapter,
|
|
134
134
|
handle,
|
|
135
|
+
acceptsImages: () => adapter.imageInput(handle),
|
|
135
136
|
runCommand: async ({ command, callId, signal }) => {
|
|
136
137
|
try {
|
|
137
138
|
const response = await interactionHandler.onInteraction({
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { TranscriptDoc, TranscriptStore } from "#src/native/session/transcript-types";
|
|
9
9
|
import type { SandboxState } from "#src/sandbox/types";
|
|
10
|
-
import type { AgentSessionProfile, ApprovalDecidedBy, CommandRunResult, EmbedderTool } from "./agent-session-types.ts";
|
|
10
|
+
import type { AgentSessionProfile, ApprovalDecidedBy, CommandRunResult, EmbedderTool, SessionConfigureInput } from "./agent-session-types.ts";
|
|
11
11
|
import type { AgentSessionAdapter, SendTurnOpts, SessionHandle } from "./session-types.ts";
|
|
12
12
|
export interface BackendInfo {
|
|
13
13
|
/** "native" or "acp:<agent>"; recorded in the transcript document. */
|
|
@@ -89,6 +89,10 @@ export interface BackendOpenContext {
|
|
|
89
89
|
readonly providerInstructions?: string;
|
|
90
90
|
}
|
|
91
91
|
export interface OpenedBackend {
|
|
92
|
+
/** Absent on a backend that cannot receive image input. Re-read the current model/agent on each query. */
|
|
93
|
+
acceptsImages?(): Promise<boolean>;
|
|
94
|
+
/** Absent on a backend that cannot change model or effort on a live session. The facade validates `input` first. */
|
|
95
|
+
configure?(input: SessionConfigureInput): Promise<void>;
|
|
92
96
|
/** Absent on a backend that cannot run a host-side command (ACP). */
|
|
93
97
|
runCommand?(request: {
|
|
94
98
|
command: string;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* working; a consumer of anything outside that subset (`AuthStamp`,
|
|
11
11
|
* `InvalidToolCallDetail`) imports it from `@nathapp/nax-agent` directly.
|
|
12
12
|
*/
|
|
13
|
-
import type { Pricing, PricingRates, TokenUsage } from "#src/cost/standard-types";
|
|
13
|
+
import type { ImageAttachment, Pricing, PricingRates, TokenUsage } from "#src/cost/standard-types";
|
|
14
14
|
import type { ResolvedPermissions } from "#src/permissions/index";
|
|
15
15
|
import type { ProtocolIds } from "#src/session/protocol-types";
|
|
16
16
|
import type { AdapterFailure } from "./adapter-failure.ts";
|
|
@@ -234,6 +234,8 @@ export interface OpenSessionOpts extends TrackedSpawnDeadlineOptions {
|
|
|
234
234
|
}
|
|
235
235
|
/** Options for sendTurn(). */
|
|
236
236
|
export interface SendTurnOpts {
|
|
237
|
+
/** Images accompanying this turn's prompt; omitted for text-only turns. */
|
|
238
|
+
images?: readonly ImageAttachment[];
|
|
237
239
|
/** Unified callback for context-tool calls and agent questions. */
|
|
238
240
|
interactionHandler: import("./interaction-handler.ts").InteractionHandler;
|
|
239
241
|
/** Native-only in-process loop-event registrations for this turn. ACP ignores this. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax-agent",
|
|
3
|
-
"version": "0.91.7
|
|
3
|
+
"version": "0.91.7",
|
|
4
4
|
"description": "nax's native coding agent: session contract, native loop, tools, permissions, sandbox, command-safety.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "William Khoo",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
50
|
-
"@nathapp/nax-ai": "0.91.7
|
|
50
|
+
"@nathapp/nax-ai": "0.91.7",
|
|
51
51
|
"zod": "^4.3.6"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public",
|
|
55
55
|
"registry": "https://registry.npmjs.org/",
|
|
56
56
|
"provenance": true,
|
|
57
|
-
"tag": "
|
|
57
|
+
"tag": "latest"
|
|
58
58
|
}
|
|
59
59
|
}
|