@nathapp/nax-agent 0.1.0 → 0.2.0
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 +22 -0
- package/README.md +47 -2
- package/dist/coding-tools/coding-tool-sandbox.d.ts +3 -0
- package/dist/coding-tools/coding-tool-sandbox.js +14 -3
- package/dist/coding-tools/coding-tool-support.d.ts +3 -0
- package/dist/coding-tools/coding-tool-support.js +2 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +4 -1
- package/dist/internal/redact.d.ts +23 -0
- package/dist/internal/redact.js +60 -0
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +8 -0
- package/dist/native/adapter-deps.d.ts +3 -3
- package/dist/native/adapter-deps.js +3 -3
- package/dist/native/client.d.ts +7 -1
- package/dist/native/client.js +1 -1
- package/dist/native/credentials/change-guard.d.ts +8 -1
- package/dist/native/credentials/change-guard.js +1 -1
- package/dist/native/credentials/fingerprint.d.ts +4 -1
- package/dist/native/credentials/fingerprint.js +6 -3
- package/dist/native/credentials/session-source.d.ts +12 -0
- package/dist/native/credentials/session-source.js +33 -0
- package/dist/native/index.d.ts +3 -1
- package/dist/native/index.js +2 -1
- package/dist/native/session/loop-events/types.d.ts +1 -1
- package/dist/native/session/loop-handlers.d.ts +3 -0
- package/dist/native/session/loop-handlers.js +1 -1
- package/dist/native/session/memory-transcript-store.d.ts +12 -0
- package/dist/native/session/memory-transcript-store.js +36 -0
- package/dist/native/session/session.d.ts +138 -96
- package/dist/native/session/session.js +107 -127
- package/dist/native/session/transcript-identity.d.ts +39 -0
- package/dist/native/session/transcript-identity.js +81 -0
- package/dist/native/session/transcript-store.d.ts +22 -21
- package/dist/native/session/transcript-store.js +59 -68
- package/dist/native/session/transcript-types.d.ts +38 -0
- package/dist/native/session/transcript-types.js +7 -0
- package/dist/native/session/truncation-handler.d.ts +4 -3
- package/dist/native/session/truncation-handler.js +7 -10
- package/dist/native/session/turn-complete-step.d.ts +4 -1
- package/dist/native/session/turn-complete-step.js +11 -4
- package/dist/native/session/turn-event-emitter.d.ts +48 -0
- package/dist/native/session/turn-event-emitter.js +134 -0
- package/dist/native/session/turn-loop-round-trip.d.ts +5 -2
- package/dist/native/session/turn-loop-round-trip.js +8 -4
- package/dist/native/session/turn-loop.js +27 -13
- package/dist/native/session/turn-result.d.ts +1 -1
- package/dist/native/session/turn-result.js +1 -1
- package/dist/native/session/turn-tool-batch.d.ts +3 -0
- package/dist/native/session/turn-tool-batch.js +14 -6
- package/dist/native/session/turn-types.d.ts +17 -1
- package/dist/native/session-adapter.d.ts +22 -4
- package/dist/native/session-adapter.js +79 -35
- package/dist/native/stream-complete.d.ts +17 -0
- package/dist/native/stream-complete.js +40 -0
- package/dist/permissions/approvals-link.js +5 -4
- package/dist/sandbox/policy-builder.d.ts +6 -1
- package/dist/sandbox/policy-builder.js +18 -14
- package/dist/session/agent-session-deps.d.ts +12 -0
- package/dist/session/agent-session-deps.js +28 -0
- package/dist/session/agent-session-errors.d.ts +11 -0
- package/dist/session/agent-session-errors.js +12 -0
- package/dist/session/agent-session-options.d.ts +16 -0
- package/dist/session/agent-session-options.js +137 -0
- package/dist/session/agent-session-resume.d.ts +12 -0
- package/dist/session/agent-session-resume.js +47 -0
- package/dist/session/agent-session-turn.d.ts +45 -0
- package/dist/session/agent-session-turn.js +179 -0
- package/dist/session/agent-session-types.d.ts +182 -0
- package/dist/session/agent-session-types.js +1 -0
- package/dist/session/agent-session.d.ts +8 -0
- package/dist/session/agent-session.js +223 -0
- package/dist/session/pending-asks.d.ts +24 -0
- package/dist/session/pending-asks.js +77 -0
- package/dist/session/session-ask-link.d.ts +26 -0
- package/dist/session/session-ask-link.js +52 -0
- package/dist/session/session-event-channel.d.ts +17 -0
- package/dist/session/session-event-channel.js +132 -0
- package/dist/session/session-interaction.d.ts +20 -0
- package/dist/session/session-interaction.js +164 -0
- package/dist/session/session-tool-support.d.ts +48 -0
- package/dist/session/session-tool-support.js +101 -0
- package/dist/session/session-types.d.ts +31 -2
- package/dist/session/turn-event.d.ts +61 -0
- package/dist/session/turn-event.js +1 -0
- package/dist/tools/bash-cwd.d.ts +3 -3
- package/dist/tools/credential-read-deny.d.ts +22 -0
- package/dist/tools/credential-read-deny.js +56 -0
- package/dist/tools/glob.js +9 -2
- package/dist/tools/grep.js +55 -26
- package/dist/tools/owned-paths.d.ts +43 -0
- package/dist/tools/owned-paths.js +17 -0
- package/dist/tools/package-managers.d.ts +3 -0
- package/dist/tools/package-managers.js +7 -3
- package/dist/tools/policy-bash-raw.d.ts +3 -0
- package/dist/tools/policy-bash-raw.js +49 -62
- package/dist/tools/policy-command-branch.d.ts +3 -0
- package/dist/tools/policy-command-branch.js +2 -1
- package/dist/tools/policy-paths-branch.d.ts +14 -8
- package/dist/tools/policy-paths-branch.js +20 -19
- package/dist/tools/policy.d.ts +10 -7
- package/dist/tools/policy.js +9 -7
- package/dist/tools/protected-paths.d.ts +6 -6
- package/dist/tools/read.js +38 -20
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/run-command-exec.js +1 -0
- package/dist/tools/runtime.d.ts +3 -0
- package/dist/tools/runtime.js +1 -0
- package/dist/tools/scratchpad.js +2 -1
- package/dist/tools/types.d.ts +2 -2
- package/package.json +2 -2
- package/dist/tools/nax-owned-writes.d.ts +0 -135
- package/dist/tools/nax-owned-writes.js +0 -259
|
@@ -22,7 +22,7 @@ import { withNudge } from "./nudge.js";
|
|
|
22
22
|
import { buildToolResult } from "./tool-result.js";
|
|
23
23
|
import { handleAskHumanCall } from "./turn-ask-human.js";
|
|
24
24
|
export async function runToolBatch(args) {
|
|
25
|
-
const { toolCalls, tools, codingToolNames, roundTrips, opts, deps, loopEvents, invalidCallBudget, spinBreaker, maxInteractions, spinWarned, interactionsSoFar, } = args;
|
|
25
|
+
const { toolCalls, tools, codingToolNames, roundTrips, opts, deps, loopEvents, invalidCallBudget, spinBreaker, maxInteractions, spinWarned, interactionsSoFar, turnEvents, } = args;
|
|
26
26
|
let messages = [...args.messages];
|
|
27
27
|
const interactions = [];
|
|
28
28
|
const codingToolsCalled = [];
|
|
@@ -124,7 +124,10 @@ export async function runToolBatch(args) {
|
|
|
124
124
|
// invalid-call repair's redacted call), written before the answer.
|
|
125
125
|
if (outcome.input !== undefined)
|
|
126
126
|
messages = rewriteToolCallInput(messages, call.id, outcome.input);
|
|
127
|
-
|
|
127
|
+
turnEvents.toolCall(call, outcome.input);
|
|
128
|
+
const blocked = buildToolResult({ toolCallId: call.id, content: outcome.content, isError: outcome.isError });
|
|
129
|
+
messages.push(blocked);
|
|
130
|
+
turnEvents.toolResult(blocked);
|
|
128
131
|
continue;
|
|
129
132
|
}
|
|
130
133
|
// `allow` and `nudge` may rewrite the call's input; the rewritten value
|
|
@@ -138,6 +141,7 @@ export async function runToolBatch(args) {
|
|
|
138
141
|
const kind = codingToolNames.has(call.name) ? "coding-tool" : "context-tool";
|
|
139
142
|
if (kind === "coding-tool")
|
|
140
143
|
codingToolsCalled.push(call.name);
|
|
144
|
+
turnEvents.toolCall(call, rewritten);
|
|
141
145
|
const answer = await opts.interactionHandler.onInteraction(kind === "coding-tool"
|
|
142
146
|
? {
|
|
143
147
|
kind,
|
|
@@ -212,12 +216,14 @@ export async function runToolBatch(args) {
|
|
|
212
216
|
});
|
|
213
217
|
const finalContent = withNudge(nudgeText, patch.content ?? answerText);
|
|
214
218
|
answer?.finalizeAudit?.(finalContent);
|
|
215
|
-
|
|
219
|
+
const result = buildToolResult({
|
|
216
220
|
toolCallId: call.id,
|
|
217
221
|
content: finalContent,
|
|
218
222
|
isError: patch.isError,
|
|
219
223
|
denied: answer?.denied,
|
|
220
|
-
})
|
|
224
|
+
});
|
|
225
|
+
messages.push(result);
|
|
226
|
+
turnEvents.toolResult(result);
|
|
221
227
|
// The breaker observed the call through the seam, i.e. with whatever
|
|
222
228
|
// input a handler rewrote in place — so the result has to be noted
|
|
223
229
|
// against that same input, or the key misses and result-based
|
|
@@ -236,11 +242,13 @@ export async function runToolBatch(args) {
|
|
|
236
242
|
toolName: call.name,
|
|
237
243
|
callId: call.id,
|
|
238
244
|
});
|
|
239
|
-
|
|
245
|
+
const failed = buildToolResult({
|
|
240
246
|
toolCallId: call.id,
|
|
241
247
|
content: patch.content ?? errorText,
|
|
242
248
|
isError: patch.isError ?? true,
|
|
243
|
-
})
|
|
249
|
+
});
|
|
250
|
+
messages.push(failed);
|
|
251
|
+
turnEvents.toolResult(failed);
|
|
244
252
|
}
|
|
245
253
|
}
|
|
246
254
|
return {
|
|
@@ -19,8 +19,11 @@ import type { SpinBreaker } from "#src/infra/spin-breaker/index";
|
|
|
19
19
|
import type { TurnDeadline } from "#src/session/turn-deadline";
|
|
20
20
|
import type { TranscriptMessage as NativeTranscriptMessage, ResolvedCompaction } from "./compaction.ts";
|
|
21
21
|
import type { CompleteCallOptions, LoopEventRegistry, LoopHandlerContext, LoopHandlerSet } from "./loop-events/index.ts";
|
|
22
|
+
import type { NativeSessionState } from "./session.ts";
|
|
22
23
|
import type { toToolDefinitions } from "./tool-mapping.ts";
|
|
24
|
+
import type { StreamDeltaSink } from "./turn-event-emitter.ts";
|
|
23
25
|
import type { TurnRetryConfig } from "./turn-retry.ts";
|
|
26
|
+
export type { StreamDelta, StreamDeltaSink } from "./turn-event-emitter.ts";
|
|
24
27
|
export interface NativeTurnResponse {
|
|
25
28
|
readonly text: string;
|
|
26
29
|
readonly toolCalls?: readonly ToolCall[];
|
|
@@ -47,7 +50,15 @@ export interface NativeSummaryResponse {
|
|
|
47
50
|
readonly rates?: PricingRates;
|
|
48
51
|
}
|
|
49
52
|
export interface TurnDeps {
|
|
50
|
-
|
|
53
|
+
/** The owning adapter's per-session state (S3 spec 5.2). Required: there is no process-global fallback. */
|
|
54
|
+
readonly sessionState: NativeSessionState;
|
|
55
|
+
/**
|
|
56
|
+
* One round-trip model call. `onDelta` (S3-3) receives the call's text and
|
|
57
|
+
* thinking deltas as they stream. The loop builds it per attempt from the
|
|
58
|
+
* turn's emitter, already stamped with the round, and leaves it absent when
|
|
59
|
+
* the turn has no `onTurnEvent` sink. Fakes may ignore it.
|
|
60
|
+
*/
|
|
61
|
+
complete(messages: readonly ConversationMessage[], tools: ReturnType<typeof toToolDefinitions>, options?: CompleteCallOptions, onDelta?: StreamDeltaSink): Promise<NativeTurnResponse>;
|
|
51
62
|
/**
|
|
52
63
|
* One model call, no tools, used only to summarize a dropped span. Separate
|
|
53
64
|
* from complete() because it must not advertise tools, must not count as a
|
|
@@ -132,6 +143,11 @@ export interface TurnDeps {
|
|
|
132
143
|
* `SendTurnOpts`. Read at dispatch, like the rest of the per-turn state.
|
|
133
144
|
*/
|
|
134
145
|
loopHandlerContext?: LoopHandlerContext;
|
|
146
|
+
/**
|
|
147
|
+
* S3-3: the turn's event sink, forwarded from `SendTurnOpts`. `runNativeTurn`
|
|
148
|
+
* wraps it once in a `TurnEventEmitter`; nothing else calls it.
|
|
149
|
+
*/
|
|
150
|
+
onTurnEvent?: import("#src/session/turn-event").TurnEventSink;
|
|
135
151
|
}
|
|
136
152
|
/**
|
|
137
153
|
* nax#1840: round trips completed before a turn fails still spent real money.
|
|
@@ -2,14 +2,32 @@
|
|
|
2
2
|
* The native session adapter: openSession / sendTurn / closeSession over
|
|
3
3
|
* nax-ai, no subprocess. openSession/closeSession are transcript-file
|
|
4
4
|
* bookkeeping, and sendTurn maps the native turn loop (session/turn-loop.ts)
|
|
5
|
-
* over
|
|
5
|
+
* over streamed model calls (`stream-complete.ts`; the compaction summary uses
|
|
6
|
+
* complete()) — nax owns the conversation because nax-ai's client is
|
|
6
7
|
* stateless (ADR-027 section 10, ADR-028).
|
|
7
8
|
*/
|
|
8
9
|
import { type AgentSessionAdapter, type OpenSessionOpts, type SendTurnOpts, type SessionHandle, type TurnResult } from "#src/session/session-types";
|
|
9
10
|
import { type NativeCatalogOverrides } from "./client.ts";
|
|
11
|
+
import { type CredentialSource } from "./credentials/session-source.ts";
|
|
12
|
+
import { type NativeSessionState } from "./session/session.ts";
|
|
13
|
+
/** The adapter's per-session state. `/internal` only: tests and the S3 facade read it; nax never does. */
|
|
14
|
+
export declare function nativeSessionStateOf(adapter: NativeSessionAdapter): NativeSessionState;
|
|
15
|
+
/** Options for an adapter that owns its client and/or its credentials (S3 5.2). */
|
|
16
|
+
export interface NativeSessionAdapterOptions {
|
|
17
|
+
/** A credential source owned by this adapter. Implies an owned client. */
|
|
18
|
+
readonly credentials?: CredentialSource;
|
|
19
|
+
/** Build and own a client even without `credentials` (for per-session catalog overrides). */
|
|
20
|
+
readonly ownClient?: boolean;
|
|
21
|
+
}
|
|
10
22
|
export declare class NativeSessionAdapter implements AgentSessionAdapter {
|
|
11
23
|
private readonly catalogOverrides;
|
|
12
|
-
|
|
24
|
+
private readonly ownStore;
|
|
25
|
+
private ownClient;
|
|
26
|
+
private readonly owns;
|
|
27
|
+
constructor(catalogOverrides?: NativeCatalogOverrides, options?: NativeSessionAdapterOptions);
|
|
28
|
+
/** The module memo for nax (one client per process, as before); an owned client for an embedder session. */
|
|
29
|
+
private client;
|
|
30
|
+
private get state();
|
|
13
31
|
/**
|
|
14
32
|
* Can this agent authenticate to at least one provider?
|
|
15
33
|
*
|
|
@@ -46,8 +64,8 @@ export declare class NativeSessionAdapter implements AgentSessionAdapter {
|
|
|
46
64
|
* stayed behind for the process lifetime.
|
|
47
65
|
*
|
|
48
66
|
* The ACP contract takes a handle string and closeNativeSession takes a
|
|
49
|
-
* SessionHandle, but
|
|
50
|
-
* carry, so clear by name rather than synthesising a handle.
|
|
67
|
+
* SessionHandle, but this adapter's state is keyed by the session-name string
|
|
68
|
+
* both carry, so clear by name rather than synthesising a handle.
|
|
51
69
|
*/
|
|
52
70
|
closePhysicalSession(handle: string, _workdir?: string, _options?: {
|
|
53
71
|
force?: boolean;
|
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
* The native session adapter: openSession / sendTurn / closeSession over
|
|
3
3
|
* nax-ai, no subprocess. openSession/closeSession are transcript-file
|
|
4
4
|
* bookkeeping, and sendTurn maps the native turn loop (session/turn-loop.ts)
|
|
5
|
-
* over
|
|
5
|
+
* over streamed model calls (`stream-complete.ts`; the compaction summary uses
|
|
6
|
+
* complete()) — nax owns the conversation because nax-ai's client is
|
|
6
7
|
* stateless (ADR-027 section 10, ADR-028).
|
|
7
8
|
*/
|
|
8
9
|
import { randomUUID } from "node:crypto";
|
|
9
10
|
import { priceCall } from "#src/cost/core/index";
|
|
10
|
-
import { getSafeLogger } from "#src/infra/index";
|
|
11
|
+
import { getSafeLogger, NaxError } from "#src/infra/index";
|
|
11
12
|
import { SessionTurnError, } from "#src/session/session-types";
|
|
12
13
|
import { createTurnDeadline } from "#src/session/turn-deadline";
|
|
13
14
|
import { _adapterDeps, authFields, isProtocolStreamError } from "./adapter-deps.js";
|
|
14
|
-
import { getNativeClient } from "./client.js";
|
|
15
|
+
import { _clientDeps, getNativeClient } from "./client.js";
|
|
16
|
+
import { createSessionCredentialStore } from "./credentials/session-source.js";
|
|
15
17
|
import { toAdapterFailure } from "./errors.js";
|
|
16
18
|
import { buildRateCard, NATIVE_AGENT, parseNativeModel, resolveContextWindow, toThinkingLevel } from "./models.js";
|
|
17
|
-
import { closeNativeSession,
|
|
19
|
+
import { closeNativeSession, createNativeSessionState, markNativeTurnOutcome, openNativeSession, systemFieldFor, } from "./session/session.js";
|
|
18
20
|
import { buildNativeStreamEvent } from "./session/turn-events.js";
|
|
19
21
|
import { runNativeTurn } from "./session/turn-loop.js";
|
|
20
22
|
import { readNativeTurnFailureUsage } from "./session/turn-types.js";
|
|
21
23
|
import { nativeSessionId } from "./session-affinity.js";
|
|
24
|
+
import { streamComplete } from "./stream-complete.js";
|
|
22
25
|
/**
|
|
23
26
|
* Fallback whole-turn budget when a session's timeout entry is missing.
|
|
24
27
|
*
|
|
@@ -39,23 +42,57 @@ function summaryPrompt(previousSummary) {
|
|
|
39
42
|
`rather than repeating or discarding it:\n\n${previousSummary}`);
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
|
-
* US-003: the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* US-003: the per-turn inputs the adapter forwards verbatim into `TurnDeps`:
|
|
46
|
+
* the plugin-contributed loop handlers and the facts their handlers read, plus
|
|
47
|
+
* (S3-3) the turn's `onTurnEvent` sink. Built here rather than inline in
|
|
48
|
+
* `sendTurn` because those conditional spreads are what pushed that method past
|
|
49
|
+
* its recorded cognitive complexity; each field is omitted entirely when the
|
|
50
|
+
* caller supplied none, so a session that loaded no `loop-handlers` plugin and
|
|
51
|
+
* sets no sink carries none of these keys.
|
|
48
52
|
*/
|
|
49
|
-
function
|
|
53
|
+
function perTurnDeps(opts) {
|
|
50
54
|
return {
|
|
51
55
|
...(opts.loopHandlers !== undefined ? { loopHandlers: opts.loopHandlers } : {}),
|
|
52
56
|
...(opts.loopHandlerContext !== undefined ? { loopHandlerContext: opts.loopHandlerContext } : {}),
|
|
57
|
+
...(opts.onTurnEvent !== undefined ? { onTurnEvent: opts.onTurnEvent } : {}),
|
|
53
58
|
};
|
|
54
59
|
}
|
|
60
|
+
const adapterStates = new WeakMap();
|
|
61
|
+
/** The adapter's per-session state. `/internal` only: tests and the S3 facade read it; nax never does. */
|
|
62
|
+
export function nativeSessionStateOf(adapter) {
|
|
63
|
+
const state = adapterStates.get(adapter);
|
|
64
|
+
if (state === undefined) {
|
|
65
|
+
throw new NaxError("NativeSessionAdapter has no session state", "NATIVE_SESSION_STATE_MISSING", {
|
|
66
|
+
stage: "native-session",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
55
71
|
export class NativeSessionAdapter {
|
|
56
72
|
catalogOverrides;
|
|
57
|
-
|
|
73
|
+
ownStore;
|
|
74
|
+
ownClient;
|
|
75
|
+
owns;
|
|
76
|
+
constructor(catalogOverrides = [], options = {}) {
|
|
58
77
|
this.catalogOverrides = catalogOverrides;
|
|
78
|
+
adapterStates.set(this, createNativeSessionState());
|
|
79
|
+
this.ownStore = options.credentials !== undefined ? createSessionCredentialStore(options.credentials) : undefined;
|
|
80
|
+
this.owns = this.ownStore !== undefined || options.ownClient === true;
|
|
81
|
+
}
|
|
82
|
+
/** The module memo for nax (one client per process, as before); an owned client for an embedder session. */
|
|
83
|
+
client() {
|
|
84
|
+
if (!this.owns)
|
|
85
|
+
return getNativeClient(this.catalogOverrides);
|
|
86
|
+
this.ownClient ??= _clientDeps
|
|
87
|
+
.build(this.catalogOverrides, this.ownStore !== undefined ? { credentials: this.ownStore } : {})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
this.ownClient = undefined;
|
|
90
|
+
throw err;
|
|
91
|
+
});
|
|
92
|
+
return this.ownClient;
|
|
93
|
+
}
|
|
94
|
+
get state() {
|
|
95
|
+
return nativeSessionStateOf(this);
|
|
59
96
|
}
|
|
60
97
|
/**
|
|
61
98
|
* Can this agent authenticate to at least one provider?
|
|
@@ -83,6 +120,8 @@ export class NativeSessionAdapter {
|
|
|
83
120
|
*/
|
|
84
121
|
async hasCredentials() {
|
|
85
122
|
try {
|
|
123
|
+
if (this.ownStore !== undefined)
|
|
124
|
+
return true;
|
|
86
125
|
if (await _adapterDeps.authSourceIsExec())
|
|
87
126
|
return true;
|
|
88
127
|
if ((await _adapterDeps.listStoredProviders()).length > 0)
|
|
@@ -94,16 +133,16 @@ export class NativeSessionAdapter {
|
|
|
94
133
|
}
|
|
95
134
|
}
|
|
96
135
|
openSession(name, opts) {
|
|
97
|
-
return openNativeSession(name, opts);
|
|
136
|
+
return openNativeSession(this.state, name, opts);
|
|
98
137
|
}
|
|
99
138
|
async sendTurn(handle, prompt, opts) {
|
|
100
139
|
const { provider, model, effort } = parseNativeModel(handle.modelDef?.model ?? "");
|
|
101
140
|
const thinking = toThinkingLevel(effort);
|
|
102
|
-
const client = await
|
|
141
|
+
const client = await this.client();
|
|
103
142
|
const resolved = await client.model(provider, model);
|
|
104
143
|
const catalog = client.pricing(resolved);
|
|
105
144
|
const { rates, source: pricingSource } = buildRateCard(catalog, handle.modelDef?.pricing);
|
|
106
|
-
const storedTimeoutSeconds =
|
|
145
|
+
const storedTimeoutSeconds = this.state.timeouts.get(handle.id);
|
|
107
146
|
if (storedTimeoutSeconds === undefined) {
|
|
108
147
|
getSafeLogger()?.warn("native-adapter", "session has no recorded timeout; falling back to the default budget", {
|
|
109
148
|
sessionName: handle.id,
|
|
@@ -114,20 +153,23 @@ export class NativeSessionAdapter {
|
|
|
114
153
|
// Keyed on the session, so every turn of one conversation carries the same
|
|
115
154
|
// id and the provider can keep its cache warm across them.
|
|
116
155
|
const sessionId = nativeSessionId(handle.id);
|
|
156
|
+
// S3-4: the session's system prompt (facade `instructions`), or `{}`.
|
|
157
|
+
// Resolved here so the `complete` closure spreads it without a branch.
|
|
158
|
+
const systemField = systemFieldFor(this.state, handle.id);
|
|
117
159
|
// One budget for the whole turn, not one per round-trip. Created here
|
|
118
160
|
// because this is where `timeoutSeconds` is known; consulted by the loop.
|
|
119
161
|
const deadline = createTurnDeadline(timeoutSeconds);
|
|
120
|
-
const hooks =
|
|
162
|
+
const hooks = this.state.streamHooks.get(handle.id);
|
|
121
163
|
// One callId per turn, mirroring SpawnAcpSession.prompt(). `runId` is
|
|
122
164
|
// backfilled by the runtime's forwarding closure, which is the only place
|
|
123
165
|
// that knows it — see runtime/index.ts.
|
|
124
166
|
const callId = randomUUID();
|
|
125
|
-
// The transcript/ledger join key, not a stream id. `
|
|
167
|
+
// The transcript/ledger join key, not a stream id. `this.state.transcriptOwners`
|
|
126
168
|
// is keyed on this same `handle.id` and already holds the session's
|
|
127
169
|
// `transcriptOwner` (`scopeId ?? callId` from session-run-hop.ts). Omitted
|
|
128
170
|
// entirely when the session declared no owner, so an absent key reads as
|
|
129
171
|
// "unknown" rather than as a wrong one.
|
|
130
|
-
const owner =
|
|
172
|
+
const owner = this.state.transcriptOwners.get(handle.id);
|
|
131
173
|
const eventBase = {
|
|
132
174
|
callId,
|
|
133
175
|
runId: "",
|
|
@@ -170,23 +212,24 @@ export class NativeSessionAdapter {
|
|
|
170
212
|
// so a settled turn must drop its timer to avoid keeping it armed past
|
|
171
213
|
// the turn boundary.
|
|
172
214
|
result = await runNativeTurn(handle, prompt, opts, {
|
|
215
|
+
sessionState: this.state,
|
|
173
216
|
deadline,
|
|
174
217
|
contextWindow: resolveContextWindow(handle.modelDef?.contextWindow, resolved.contextWindow),
|
|
175
|
-
...(
|
|
176
|
-
? { compaction:
|
|
218
|
+
...(this.state.compaction.get(handle.id) !== undefined
|
|
219
|
+
? { compaction: this.state.compaction.get(handle.id) }
|
|
177
220
|
: {}),
|
|
178
|
-
...(
|
|
179
|
-
? { transportRetry:
|
|
221
|
+
...(this.state.transportRetry.get(handle.id) !== undefined
|
|
222
|
+
? { transportRetry: this.state.transportRetry.get(handle.id) }
|
|
180
223
|
: {}),
|
|
181
|
-
...(
|
|
182
|
-
? { spinBreaker:
|
|
224
|
+
...(this.state.spinBreakers.get(handle.id) !== undefined
|
|
225
|
+
? { spinBreaker: this.state.spinBreakers.get(handle.id) }
|
|
183
226
|
: {}),
|
|
184
227
|
...(opts.loopEvents !== undefined ? { loopEvents: opts.loopEvents } : {}),
|
|
185
228
|
// US-003: the run's plugin-contributed loop handlers and the facts
|
|
186
229
|
// their handlers read travel the same way as `loopEvents` — both are
|
|
187
230
|
// per-turn inputs the loop installs onto its registry rather than
|
|
188
231
|
// state the adapter owns.
|
|
189
|
-
...
|
|
232
|
+
...perTurnDeps(opts),
|
|
190
233
|
// US-002: the one per-turn signal threaded into the batch and the
|
|
191
234
|
// in-flight coding-tool runtime. When `opts.signal` and the watchdog
|
|
192
235
|
// and the deadline are all absent, `turnSignal` is a non-aborted
|
|
@@ -231,7 +274,7 @@ export class NativeSessionAdapter {
|
|
|
231
274
|
_adapterDeps.clearTimeout(timer);
|
|
232
275
|
}
|
|
233
276
|
},
|
|
234
|
-
complete: async (messages, tools, requestOptions) => {
|
|
277
|
+
complete: async (messages, tools, requestOptions, onDelta) => {
|
|
235
278
|
// The controller is armed with what is LEFT of the turn, so N
|
|
236
279
|
// round-trips can no longer add up to N x timeoutSeconds. Still
|
|
237
280
|
// combined with any caller-supplied opts.signal via AbortSignal.any so
|
|
@@ -250,10 +293,11 @@ export class NativeSessionAdapter {
|
|
|
250
293
|
// thinking LEVEL rather than a boolean, so normalize at this boundary.
|
|
251
294
|
const requestThinking = requestOptions?.thinking === false ? undefined : thinking;
|
|
252
295
|
try {
|
|
253
|
-
const res = await client
|
|
296
|
+
const res = await streamComplete(client, resolved, {
|
|
254
297
|
messages,
|
|
255
298
|
...(tools.length > 0 ? { tools } : {}),
|
|
256
299
|
sessionId,
|
|
300
|
+
...systemField,
|
|
257
301
|
signal,
|
|
258
302
|
...(requestThinking !== undefined ? { thinking: requestThinking } : {}),
|
|
259
303
|
...(requestOptions?.temperature !== undefined ? { temperature: requestOptions.temperature } : {}),
|
|
@@ -268,7 +312,7 @@ export class NativeSessionAdapter {
|
|
|
268
312
|
// to reuse the entry, so a cache write there costs more than it
|
|
269
313
|
// saves.
|
|
270
314
|
cacheRetention: "short",
|
|
271
|
-
});
|
|
315
|
+
}, onDelta);
|
|
272
316
|
const usage = res.usage;
|
|
273
317
|
// Single `priceCall` invocation: `costUsd` and `resolvedRates`
|
|
274
318
|
// come from the same call so they cannot diverge — the
|
|
@@ -304,7 +348,7 @@ export class NativeSessionAdapter {
|
|
|
304
348
|
status: turnController.signal.aborted ? "cancelled" : "error",
|
|
305
349
|
timestamp: Date.now(),
|
|
306
350
|
});
|
|
307
|
-
markNativeTurnOutcome(handle.id, true);
|
|
351
|
+
markNativeTurnOutcome(this.state, handle.id, true);
|
|
308
352
|
// The same treatment complete() gives a protocol fault, on the path that
|
|
309
353
|
// was missing it (nax#1838). Rethrowing untouched left build-hop-callback
|
|
310
354
|
// to synthesise a generic fail-adapter-error, which cost a rate limit its
|
|
@@ -340,17 +384,17 @@ export class NativeSessionAdapter {
|
|
|
340
384
|
status: result.timedOut === true ? "timeout" : turnController.signal.aborted ? "cancelled" : "success",
|
|
341
385
|
timestamp: Date.now(),
|
|
342
386
|
});
|
|
343
|
-
markNativeTurnOutcome(handle.id, false);
|
|
387
|
+
markNativeTurnOutcome(this.state, handle.id, false);
|
|
344
388
|
// US-006: the store only observes `provider` while a request is in flight,
|
|
345
389
|
// so the stamp is read here, after the loop.
|
|
346
|
-
return { ...result, ...authFields(provider) };
|
|
390
|
+
return { ...result, ...authFields(provider, this.ownStore) };
|
|
347
391
|
}
|
|
348
392
|
closeSession(handle) {
|
|
349
393
|
// The adapter interface has no failure signal, so the verdict comes from the
|
|
350
394
|
// session's last turn (markNativeTurnOutcome) rather than from this call.
|
|
351
395
|
// Passing a literal false here is what deleted the transcript of a failed
|
|
352
396
|
// session -- the one the retry reloads and a human reads (nax#1838).
|
|
353
|
-
return closeNativeSession(handle);
|
|
397
|
+
return closeNativeSession(this.state, handle);
|
|
354
398
|
}
|
|
355
399
|
/**
|
|
356
400
|
* Run teardown addresses a session the process no longer has a handle for,
|
|
@@ -360,10 +404,10 @@ export class NativeSessionAdapter {
|
|
|
360
404
|
* stayed behind for the process lifetime.
|
|
361
405
|
*
|
|
362
406
|
* The ACP contract takes a handle string and closeNativeSession takes a
|
|
363
|
-
* SessionHandle, but
|
|
364
|
-
* carry, so clear by name rather than synthesising a handle.
|
|
407
|
+
* SessionHandle, but this adapter's state is keyed by the session-name string
|
|
408
|
+
* both carry, so clear by name rather than synthesising a handle.
|
|
365
409
|
*/
|
|
366
410
|
async closePhysicalSession(handle, _workdir, _options) {
|
|
367
|
-
return closeNativeSession({ id: handle, agentName: NATIVE_AGENT });
|
|
411
|
+
return closeNativeSession(this.state, { id: handle, agentName: NATIVE_AGENT });
|
|
368
412
|
}
|
|
369
413
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One streamed model call (S3 spec 5.3, R7).
|
|
3
|
+
*
|
|
4
|
+
* nax-ai's `Client.complete()` is `collectStream(streamFrom(...))`, so the wire
|
|
5
|
+
* was always a stream; this calls the same `stream()` and folds it with the
|
|
6
|
+
* same `collectStream`, tapping text and thinking deltas on the way. The fold
|
|
7
|
+
* is deliberately not reimplemented: last-usage-wins, the required `done` and
|
|
8
|
+
* `ProtocolStreamError` on an `error` event are what the turn loop's retry and
|
|
9
|
+
* overflow classifiers key on.
|
|
10
|
+
*
|
|
11
|
+
* `async` on purpose: `client.stream()` throws synchronously on an invalid
|
|
12
|
+
* header or session id, and an async function turns that into a rejection,
|
|
13
|
+
* exactly as `complete()` does. Without `onDelta` there is no tap at all.
|
|
14
|
+
*/
|
|
15
|
+
import { type Client, type ClientRequest, type CompleteResult, type ResolvedModel } from "@nathapp/nax-ai";
|
|
16
|
+
import type { StreamDeltaSink } from "./session/turn-types.ts";
|
|
17
|
+
export declare function streamComplete(client: Client, model: ResolvedModel, req: ClientRequest, onDelta?: StreamDeltaSink): Promise<CompleteResult>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One streamed model call (S3 spec 5.3, R7).
|
|
3
|
+
*
|
|
4
|
+
* nax-ai's `Client.complete()` is `collectStream(streamFrom(...))`, so the wire
|
|
5
|
+
* was always a stream; this calls the same `stream()` and folds it with the
|
|
6
|
+
* same `collectStream`, tapping text and thinking deltas on the way. The fold
|
|
7
|
+
* is deliberately not reimplemented: last-usage-wins, the required `done` and
|
|
8
|
+
* `ProtocolStreamError` on an `error` event are what the turn loop's retry and
|
|
9
|
+
* overflow classifiers key on.
|
|
10
|
+
*
|
|
11
|
+
* `async` on purpose: `client.stream()` throws synchronously on an invalid
|
|
12
|
+
* header or session id, and an async function turns that into a rejection,
|
|
13
|
+
* exactly as `complete()` does. Without `onDelta` there is no tap at all.
|
|
14
|
+
*/
|
|
15
|
+
import { collectStream, } from "@nathapp/nax-ai";
|
|
16
|
+
function deltaOf(event) {
|
|
17
|
+
if (event.type === "text-delta")
|
|
18
|
+
return { type: "text_delta", text: event.text };
|
|
19
|
+
if (event.type === "thinking-delta")
|
|
20
|
+
return { type: "thinking_delta", text: event.text };
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
async function* tap(events, onDelta) {
|
|
24
|
+
for await (const event of events) {
|
|
25
|
+
const delta = deltaOf(event);
|
|
26
|
+
if (delta !== undefined) {
|
|
27
|
+
try {
|
|
28
|
+
onDelta(delta);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// A display sink must not break the call; the emitter already logs its own failures.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
yield event;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function streamComplete(client, model, req, onDelta) {
|
|
38
|
+
const events = client.stream(model, req);
|
|
39
|
+
return collectStream(onDelta === undefined ? events : tap(events, onDelta));
|
|
40
|
+
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* The approvals cache as the FIRST link of the ask chain (P2 design 4.3, 6.6).
|
|
3
3
|
*
|
|
4
4
|
* TRUST BOUNDARY. Living outside repoRoot protects this file from the TYPED
|
|
5
|
-
* path-bearing tools, but NOT from Bash:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* path-bearing tools, but NOT from Bash: the `OwnedPathsPolicy` port's
|
|
6
|
+
* `writeRefusal` (`tools/owned-paths.ts`) excludes
|
|
7
|
+
* Bash by design, and under `raw` mode screenRawBashCommand's owned-path
|
|
8
|
+
* candidates skip every path outside the root (policy-bash-raw.ts), so the
|
|
9
|
+
* file is never screened. A raw shell can forge entries.
|
|
9
10
|
*
|
|
10
11
|
* That is not a new vulnerability -- a raw shell needs no forged permission to
|
|
11
12
|
* run a command -- but it IS a cross-stage escalation in a MIXED-mode run,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SandboxConfig } from "../config/schemas-sandbox.ts";
|
|
2
|
+
import type { OwnedPathsPolicy } from "../tools/owned-paths.ts";
|
|
2
3
|
import type { GitLayout } from "./policy-inputs.ts";
|
|
3
4
|
import type { SandboxPolicy } from "./types.ts";
|
|
4
5
|
export interface SandboxPolicyInput {
|
|
@@ -6,8 +7,12 @@ export interface SandboxPolicyInput {
|
|
|
6
7
|
readonly git: GitLayout;
|
|
7
8
|
/** Redirecting git files present now, every worktree included (listGitGuardFiles, #2198). */
|
|
8
9
|
readonly gitGuardFiles: readonly string[];
|
|
9
|
-
/** Top-level entry names under `<root
|
|
10
|
+
/** Top-level entry names under `<root>/<projectStateDir>` right now (listNaxEntries). */
|
|
10
11
|
readonly naxEntries: readonly string[];
|
|
12
|
+
/** S3-2: host-owned path rules; supplies the always-denied project-state entries and root files. */
|
|
13
|
+
readonly ownedPaths: OwnedPathsPolicy;
|
|
14
|
+
/** Project-relative state directory (ProtectedPathsPolicy.projectStateDir); absent: no project-state denies. */
|
|
15
|
+
readonly projectStateDir?: string;
|
|
11
16
|
readonly credentialFiles: readonly string[];
|
|
12
17
|
readonly approvalsFile?: string;
|
|
13
18
|
/** US-006 — the trust store (`trustStorePath()`), denied so a command cannot rewrite its own trust. */
|
|
@@ -10,7 +10,6 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
10
10
|
import { NaxError } from "#src/infra/index";
|
|
11
11
|
import { realOrRaw } from "#src/internal/realpath";
|
|
12
12
|
import { SANDBOX_GLOB_CHARS } from "../config/schemas-sandbox.js";
|
|
13
|
-
import { NAX_ALWAYS_DENIED_ENTRIES, NAX_SCRATCHPAD_ENTRY, naxWriteOptIns, QUEUE_CONTROL_FILES, } from "../tools/nax-owned-writes.js";
|
|
14
13
|
import { BUILTIN_CACHE_WRITE_ROOTS, BUILTIN_CREDENTIAL_READ_DENIES, MACOS_CACHE_WRITE_ROOT, SRT_MACOS_TMPDIR, SRT_MACOS_TMPDIR_DENIES, } from "./defaults.js";
|
|
15
14
|
import { WORKTREE_COMMON_WRITE_DIRS, WORKTREE_CONFIG_FILE } from "./git-guards.js";
|
|
16
15
|
function expandHome(p, home) {
|
|
@@ -62,13 +61,15 @@ function worktreeGitWriteRoots(git) {
|
|
|
62
61
|
return [git.gitDir, ...WORKTREE_COMMON_WRITE_DIRS.map((dir) => join(git.commonDir, dir))];
|
|
63
62
|
}
|
|
64
63
|
/**
|
|
65
|
-
* nax#2260: every top-level
|
|
66
|
-
* when absent -- except the scratchpad
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
64
|
+
* nax#2260: every top-level project-state entry, whole, plus the ones the host
|
|
65
|
+
* loads even when absent (`ownedPaths.deniedEntries`) -- except the scratchpad
|
|
66
|
+
* and entries opted in via allowWrite. The ENTRIES come from the owned-paths
|
|
67
|
+
* port; the DIRECTORY comes from `projectStateDir`, and its absence denies
|
|
68
|
+
* nothing. srt applies deny over allow and on Linux turns every literal deny
|
|
69
|
+
* into a bind mount, so whole entries (one `features` deny, not one per PRD)
|
|
70
|
+
* keep both the protection and the per-command cost bounded.
|
|
70
71
|
*
|
|
71
|
-
* Only `<root
|
|
72
|
+
* Only `<root>/<projectStateDir>` is covered. A monorepo package's own `.nax/`
|
|
72
73
|
* (`packages/<pkg>/.nax/`) is deliberately excluded: what lives there --
|
|
73
74
|
* cache/, scratchpad/, status.json, the features' acceptance tests and run
|
|
74
75
|
* artifacts -- is gitignored and rebuilt or regenerated, so deleting it is
|
|
@@ -77,11 +78,14 @@ function worktreeGitWriteRoots(git) {
|
|
|
77
78
|
* into prompts; covering it needs package discovery, because a `.nax/` at any
|
|
78
79
|
* depth also matches test-fixture projects (`test/fixtures/<p>/.nax/`).
|
|
79
80
|
*/
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
function projectStateDenies(input) {
|
|
82
|
+
const { root, projectStateDir, ownedPaths } = input;
|
|
83
|
+
if (projectStateDir === undefined)
|
|
84
|
+
return [];
|
|
85
|
+
const optIns = ownedPaths.writeOptIns(root, input.config.filesystem.allowWrite);
|
|
86
|
+
return [...new Set([...input.naxEntries, ...ownedPaths.deniedEntries])]
|
|
87
|
+
.filter((name) => name !== ownedPaths.scratchpadEntry && !optIns.has(name))
|
|
88
|
+
.map((name) => join(root, projectStateDir, name));
|
|
85
89
|
}
|
|
86
90
|
export function buildSandboxPolicy(input) {
|
|
87
91
|
const { root, home, config } = input;
|
|
@@ -131,8 +135,8 @@ export function buildSandboxPolicy(input) {
|
|
|
131
135
|
}),
|
|
132
136
|
]);
|
|
133
137
|
const denyWrite = literal([
|
|
134
|
-
...
|
|
135
|
-
...
|
|
138
|
+
...projectStateDenies(input),
|
|
139
|
+
...input.ownedPaths.rootWriteDenies.map((name) => join(root, name)),
|
|
136
140
|
...gitDenies(root, input.git),
|
|
137
141
|
...input.gitGuardFiles,
|
|
138
142
|
...(input.approvalsFile !== undefined ? [input.approvalsFile] : []),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Undelivered control events a turn may queue before it is cancelled as stalled (spec 4.4). */
|
|
2
|
+
export declare const MAX_UNDELIVERED_CONTROL_EVENTS = 1000;
|
|
3
|
+
export declare const _agentSessionDeps: {
|
|
4
|
+
now: () => number;
|
|
5
|
+
setTimeout: (fn: () => void, ms: number) => unknown;
|
|
6
|
+
clearTimeout: (handle: unknown) => void;
|
|
7
|
+
randomUUID: () => string;
|
|
8
|
+
isDirectory: (path: string) => Promise<boolean>;
|
|
9
|
+
makeScratchRoot: () => Promise<string>;
|
|
10
|
+
removeScratchRoot: (dir: string) => Promise<void>;
|
|
11
|
+
controlEventCap: number;
|
|
12
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injectable clock, timers, ids and filesystem for the agent session facade.
|
|
3
|
+
* Tests replace members (paired with withDepsRestore) to fire deadlines
|
|
4
|
+
* without sleeping and to lower the control-event cap. Exported on ./internal.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { mkdtemp, rm, stat } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
/** Undelivered control events a turn may queue before it is cancelled as stalled (spec 4.4). */
|
|
11
|
+
export const MAX_UNDELIVERED_CONTROL_EVENTS = 1000;
|
|
12
|
+
export const _agentSessionDeps = {
|
|
13
|
+
now: () => Date.now(),
|
|
14
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
15
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
16
|
+
randomUUID: () => randomUUID(),
|
|
17
|
+
isDirectory: async (path) => {
|
|
18
|
+
try {
|
|
19
|
+
return (await stat(path)).isDirectory();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
makeScratchRoot: () => mkdtemp(join(tmpdir(), "nax-agent-session-")),
|
|
26
|
+
removeScratchRoot: (dir) => rm(dir, { recursive: true, force: true }),
|
|
27
|
+
controlEventCap: MAX_UNDELIVERED_CONTROL_EVENTS,
|
|
28
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent session facade's public error (S3 spec section 7). Every code is
|
|
3
|
+
* namespaced AGENT_SESSION_* so it cannot collide with nax's SESSION_* codes
|
|
4
|
+
* or registerCodingTool's TOOL_NAME_RESERVED.
|
|
5
|
+
*/
|
|
6
|
+
import { NaxError } from "#src/infra/nax-error";
|
|
7
|
+
export type AgentSessionErrorCode = "AGENT_SESSION_INVALID_OPTIONS" | "AGENT_SESSION_EXISTS" | "AGENT_SESSION_BUSY" | "AGENT_SESSION_CLOSED" | "AGENT_SESSION_INVALID_ANSWER" | "AGENT_SESSION_NOT_FOUND" | "AGENT_SESSION_SCHEMA_UNSUPPORTED" | "AGENT_SESSION_MODEL_MISMATCH" | "AGENT_SESSION_SANDBOX_UNAVAILABLE" | "AGENT_SESSION_TOOL_NAME_RESERVED";
|
|
8
|
+
export declare class AgentSessionError extends NaxError {
|
|
9
|
+
readonly code: AgentSessionErrorCode;
|
|
10
|
+
constructor(message: string, code: AgentSessionErrorCode, context?: Record<string, unknown>);
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent session facade's public error (S3 spec section 7). Every code is
|
|
3
|
+
* namespaced AGENT_SESSION_* so it cannot collide with nax's SESSION_* codes
|
|
4
|
+
* or registerCodingTool's TOOL_NAME_RESERVED.
|
|
5
|
+
*/
|
|
6
|
+
import { NaxError } from "#src/infra/nax-error";
|
|
7
|
+
export class AgentSessionError extends NaxError {
|
|
8
|
+
constructor(message, code, context = {}) {
|
|
9
|
+
super(message, code, { stage: "agent-session", ...context });
|
|
10
|
+
this.name = "AgentSessionError";
|
|
11
|
+
}
|
|
12
|
+
}
|