@nathapp/nax-agent 0.91.7 → 0.91.8-canary.2
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 +8 -0
- package/dist/config/catalog-overrides.d.ts +13 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/internal/atomic-write.d.ts +22 -7
- package/dist/internal/atomic-write.js +107 -12
- package/dist/internal/pinned-directory-worker.d.ts +52 -0
- package/dist/internal/pinned-directory-worker.js +105 -0
- package/dist/internal/pinned-directory.d.ts +48 -0
- package/dist/internal/pinned-directory.js +168 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +2 -0
- package/dist/memory/delivery.d.ts +38 -0
- package/dist/memory/delivery.js +28 -0
- package/dist/memory/index-render.d.ts +55 -0
- package/dist/memory/index-render.js +225 -0
- package/dist/memory/index.d.ts +12 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/local-provider.d.ts +3 -0
- package/dist/memory/local-provider.js +127 -0
- package/dist/memory/mirror-lock.d.ts +109 -0
- package/dist/memory/mirror-lock.js +334 -0
- package/dist/memory/note.d.ts +15 -0
- package/dist/memory/note.js +131 -0
- package/dist/memory/operations.d.ts +123 -0
- package/dist/memory/operations.js +471 -0
- package/dist/memory/outbox.d.ts +35 -0
- package/dist/memory/outbox.js +289 -0
- package/dist/memory/provider-types.d.ts +50 -0
- package/dist/memory/provider-types.js +1 -0
- package/dist/memory/records.d.ts +41 -0
- package/dist/memory/records.js +93 -0
- package/dist/memory/refusal.d.ts +36 -0
- package/dist/memory/refusal.js +25 -0
- package/dist/memory/search.d.ts +17 -0
- package/dist/memory/search.js +112 -0
- package/dist/memory/store.d.ts +106 -0
- package/dist/memory/store.js +459 -0
- package/dist/memory/support.d.ts +63 -0
- package/dist/memory/support.js +98 -0
- package/dist/memory/sync-flight.d.ts +25 -0
- package/dist/memory/sync-flight.js +65 -0
- package/dist/memory/sync.d.ts +82 -0
- package/dist/memory/sync.js +344 -0
- package/dist/memory/types.d.ts +108 -0
- package/dist/memory/types.js +51 -0
- package/dist/memory/validate.d.ts +31 -0
- package/dist/memory/validate.js +104 -0
- package/dist/native/adapter-deps.d.ts +9 -0
- package/dist/native/adapter-deps.js +9 -0
- package/dist/native/models.js +3 -0
- package/dist/native/session/compaction.d.ts +15 -1
- package/dist/native/session/compaction.js +90 -23
- package/dist/native/session/manual-compaction.d.ts +57 -0
- package/dist/native/session/manual-compaction.js +72 -0
- package/dist/native/session/session-summarizer.d.ts +75 -0
- package/dist/native/session/session-summarizer.js +73 -0
- package/dist/native/session/transcript-identity.d.ts +12 -0
- package/dist/native/session/transcript-identity.js +16 -0
- package/dist/native/session/turn-compaction-step.d.ts +30 -2
- package/dist/native/session/turn-compaction-step.js +7 -1
- package/dist/native/session/turn-loop-round-trip.js +71 -17
- package/dist/native/session/turn-loop.js +2 -6
- package/dist/native/session-adapter.d.ts +19 -0
- package/dist/native/session-adapter.js +42 -64
- package/dist/sandbox/srt-backend.js +9 -16
- package/dist/session/agent-session-options.js +1 -0
- package/dist/session/agent-session-turn.d.ts +6 -0
- package/dist/session/agent-session-turn.js +5 -4
- package/dist/session/agent-session-types.d.ts +38 -0
- package/dist/session/agent-session.d.ts +8 -0
- package/dist/session/agent-session.js +163 -20
- package/dist/session/native-backend.js +8 -0
- package/dist/session/session-backend.d.ts +19 -1
- package/dist/session/session-types.d.ts +6 -0
- package/dist/session/steer-channel.d.ts +36 -0
- package/dist/session/steer-channel.js +39 -0
- package/dist/session-store/index.d.ts +1 -1
- package/dist/session-store/index.js +1 -1
- package/dist/session-store/session-lock.d.ts +2 -2
- package/dist/session-store/session-lock.js +4 -2
- package/dist/tools/ask-request.d.ts +8 -0
- package/dist/tools/ask-request.js +34 -1
- package/dist/tools/provider-adapt.js +5 -1
- package/dist/tools/provider-types.d.ts +7 -0
- package/dist/tools/types.d.ts +16 -0
- package/package.json +4 -4
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one summarizer every native compaction path shares (US-002).
|
|
3
|
+
*
|
|
4
|
+
* `sendTurn` built this closure inline until manual compaction needed the same
|
|
5
|
+
* call: same model, same clock, no tools, the same prompt asking for what a
|
|
6
|
+
* coding agent needs back — what was tried, what was rejected and why, the files
|
|
7
|
+
* touched — plus the same per-call budget and the same pricing. Two copies would
|
|
8
|
+
* have drifted on exactly the details that are invisible when they do (the
|
|
9
|
+
* thinking level, the rate card branch, the merge instruction), so the closure
|
|
10
|
+
* and the per-session resolution it depends on live here once.
|
|
11
|
+
*
|
|
12
|
+
* `resolveSessionModel` is the other half of that sharing: `sendTurn` and
|
|
13
|
+
* `compactHistory` must agree on the model, the rate card, the context window
|
|
14
|
+
* and the compaction settings, and they now do so by construction. The window
|
|
15
|
+
* and the settings are not used by the closure itself — they are what the caller
|
|
16
|
+
* hands the turn loop or the keep-budget maths, and resolving them alongside the
|
|
17
|
+
* client is what keeps one session's five facts from being assembled two ways.
|
|
18
|
+
*
|
|
19
|
+
* This module deliberately does NOT import a second site of `@nathapp/nax-ai`
|
|
20
|
+
* beyond the client types it is handed: the client arrives already built.
|
|
21
|
+
*/
|
|
22
|
+
import type { Client, ResolvedModel, ThinkingLevel } from "@nathapp/nax-ai";
|
|
23
|
+
import type { Pricing } from "#src/cost/standard-types";
|
|
24
|
+
import type { SessionHandle } from "#src/session/session-types";
|
|
25
|
+
import type { TurnDeadline } from "#src/session/turn-deadline";
|
|
26
|
+
import type { ResolvedCompaction } from "./compaction.ts";
|
|
27
|
+
import type { NativeSessionState } from "./session.ts";
|
|
28
|
+
import type { TurnDeps } from "./turn-types.ts";
|
|
29
|
+
/**
|
|
30
|
+
* Everything one session's model call needs, resolved once per operation.
|
|
31
|
+
*
|
|
32
|
+
* `compaction` falls back to `DEFAULT_COMPACTION` rather than staying absent:
|
|
33
|
+
* an adapter whose open did not carry settings (an embedder that never set them)
|
|
34
|
+
* still answers "what is the keep budget" the way every other caller does. The
|
|
35
|
+
* automatic path's `TurnDeps.compaction` stays optional-and-absent for such a
|
|
36
|
+
* session — see the adapter's own note — because there "no settings" and
|
|
37
|
+
* "settings that disable compaction" must remain distinguishable.
|
|
38
|
+
*/
|
|
39
|
+
export interface SessionModelResolution {
|
|
40
|
+
readonly client: Client;
|
|
41
|
+
readonly model: ResolvedModel;
|
|
42
|
+
/** The provider half of the model id, as parsed (`authFields` needs it). */
|
|
43
|
+
readonly provider: string;
|
|
44
|
+
/** The request-side reasoning level the session's effort suffix translates to. */
|
|
45
|
+
readonly thinking: ThinkingLevel | undefined;
|
|
46
|
+
/** The rate card `priceCall` prices from: the config override, or the catalog. */
|
|
47
|
+
readonly rates: Pricing;
|
|
48
|
+
readonly pricingSource: "catalog-rates" | "config-override";
|
|
49
|
+
/** The window compaction counts against (`resolveContextWindow`). */
|
|
50
|
+
readonly contextWindow: number;
|
|
51
|
+
readonly compaction: ResolvedCompaction;
|
|
52
|
+
/** The provider-facing session id every call of this session carries. */
|
|
53
|
+
readonly sessionId: string;
|
|
54
|
+
}
|
|
55
|
+
/** Resolve the session's model facts. Throws on a malformed model id, as `sendTurn` always has. */
|
|
56
|
+
export declare function resolveSessionModel(client: Client, state: NativeSessionState, handle: SessionHandle): Promise<SessionModelResolution>;
|
|
57
|
+
/**
|
|
58
|
+
* What the summary call is asked for. Moved here unchanged from the adapter:
|
|
59
|
+
* the merge instruction is part of the summarizer's contract, not of the turn
|
|
60
|
+
* that happens to trigger it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function summaryPrompt(previousSummary?: string): string;
|
|
63
|
+
export interface SessionSummarizerInput {
|
|
64
|
+
readonly resolution: SessionModelResolution;
|
|
65
|
+
/** What is LEFT of the operation's budget, consulted per call. */
|
|
66
|
+
readonly deadline: TurnDeadline;
|
|
67
|
+
/**
|
|
68
|
+
* The operation's cancellation, already composed: for a turn that is the
|
|
69
|
+
* per-turn signal (caller + watchdog + deadline), for a manual compaction the
|
|
70
|
+
* facade's deadline and close signal. The per-call budget is fanned in here.
|
|
71
|
+
*/
|
|
72
|
+
readonly signal: AbortSignal;
|
|
73
|
+
}
|
|
74
|
+
/** The `summarize` closure both `sendTurn` and `compactHistory` hand the compaction core. */
|
|
75
|
+
export declare function createSessionSummarizer(input: SessionSummarizerInput): NonNullable<TurnDeps["summarize"]>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { priceCall } from "#src/cost/core/index";
|
|
2
|
+
import { _adapterDeps } from "../adapter-deps.js";
|
|
3
|
+
import { buildRateCard, parseNativeModel, resolveContextWindow, toThinkingLevel } from "../models.js";
|
|
4
|
+
import { nativeSessionId } from "../session-affinity.js";
|
|
5
|
+
import { DEFAULT_COMPACTION } from "./compaction-settings.js";
|
|
6
|
+
/** Resolve the session's model facts. Throws on a malformed model id, as `sendTurn` always has. */
|
|
7
|
+
export async function resolveSessionModel(client, state, handle) {
|
|
8
|
+
const { provider, model, effort } = parseNativeModel(handle.modelDef?.model ?? "");
|
|
9
|
+
const resolved = await client.model(provider, model);
|
|
10
|
+
const { rates, source } = buildRateCard(client.pricing(resolved), handle.modelDef?.pricing);
|
|
11
|
+
return {
|
|
12
|
+
client,
|
|
13
|
+
model: resolved,
|
|
14
|
+
provider,
|
|
15
|
+
thinking: toThinkingLevel(effort),
|
|
16
|
+
rates,
|
|
17
|
+
pricingSource: source,
|
|
18
|
+
contextWindow: resolveContextWindow(handle.modelDef?.contextWindow, resolved.contextWindow),
|
|
19
|
+
compaction: state.compaction.get(handle.id) ?? DEFAULT_COMPACTION,
|
|
20
|
+
sessionId: nativeSessionId(handle.id),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What the summary call is asked for. Moved here unchanged from the adapter:
|
|
25
|
+
* the merge instruction is part of the summarizer's contract, not of the turn
|
|
26
|
+
* that happens to trigger it.
|
|
27
|
+
*/
|
|
28
|
+
export function summaryPrompt(previousSummary) {
|
|
29
|
+
const base = "Summarize the conversation above so it can be dropped from context. " +
|
|
30
|
+
"Record what was attempted, what was rejected and why, any decisions that still bind, " +
|
|
31
|
+
"and list the files read and the files modified. Be specific: this summary is the only " +
|
|
32
|
+
"memory of this work that survives.";
|
|
33
|
+
if (previousSummary === undefined)
|
|
34
|
+
return base;
|
|
35
|
+
return (`${base}\n\nAn earlier summary of still-older history follows. Merge it into your summary ` +
|
|
36
|
+
`rather than repeating or discarding it:\n\n${previousSummary}`);
|
|
37
|
+
}
|
|
38
|
+
/** The `summarize` closure both `sendTurn` and `compactHistory` hand the compaction core. */
|
|
39
|
+
export function createSessionSummarizer(input) {
|
|
40
|
+
const { resolution, deadline, signal } = input;
|
|
41
|
+
return async (span, previousSummary) => {
|
|
42
|
+
const remainingMs = deadline.remainingMs();
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = remainingMs !== undefined ? _adapterDeps.setTimeout(() => controller.abort(), remainingMs) : undefined;
|
|
45
|
+
// The per-call signal combines the operation's cancellation with this
|
|
46
|
+
// call's remaining budget, so a cancel ends the summary even mid-call.
|
|
47
|
+
const callSignal = AbortSignal.any([signal, controller.signal]);
|
|
48
|
+
try {
|
|
49
|
+
const res = await resolution.client.complete(resolution.model, {
|
|
50
|
+
messages: [...span, { role: "user", content: summaryPrompt(previousSummary) }],
|
|
51
|
+
sessionId: resolution.sessionId,
|
|
52
|
+
signal: callSignal,
|
|
53
|
+
// A summary restates the transcript; it does not need reasoning.
|
|
54
|
+
// "off", not an absent level: absent means the backend default,
|
|
55
|
+
// which on a model that cannot disable thinking is adaptive at
|
|
56
|
+
// effort high (#2686). The client clamps "off" to the least
|
|
57
|
+
// thinking such a model allows.
|
|
58
|
+
thinking: "off",
|
|
59
|
+
});
|
|
60
|
+
const { costUsd, resolvedRates } = priceCall(res.usage, resolution.rates);
|
|
61
|
+
return {
|
|
62
|
+
text: res.text,
|
|
63
|
+
usage: res.usage,
|
|
64
|
+
costUsd,
|
|
65
|
+
rates: resolvedRates,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
if (timer !== undefined)
|
|
70
|
+
_adapterDeps.clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* store rather than only the file store.
|
|
5
5
|
*/
|
|
6
6
|
import type { ConversationMessage } from "@nathapp/nax-ai";
|
|
7
|
+
import type { SessionHandle } from "#src/session/session-types";
|
|
8
|
+
import type { NativeSessionState } from "./session.ts";
|
|
7
9
|
import type { TranscriptDoc } from "./transcript-types.ts";
|
|
8
10
|
/**
|
|
9
11
|
* Who may resume a transcript. `owner` is the op invocation (nax#1877); `model`
|
|
@@ -39,3 +41,13 @@ export declare function historyFromTranscript(doc: TranscriptDoc | null, identit
|
|
|
39
41
|
* the file store writes it as-is, and nax's transcript bytes depend on that order.
|
|
40
42
|
*/
|
|
41
43
|
export declare function transcriptDocFor(messages: readonly ConversationMessage[], identity: TranscriptIdentity): TranscriptDoc;
|
|
44
|
+
/**
|
|
45
|
+
* The identity a session's transcript is loaded and saved under (US-002).
|
|
46
|
+
*
|
|
47
|
+
* One builder, because a turn's save and a manual compaction's save must claim
|
|
48
|
+
* exactly the same identity: the owner the invocation recorded (nax#1877) and
|
|
49
|
+
* the model that produced the history (nax#2150). A compaction that saved under
|
|
50
|
+
* a different identity would write a transcript the very next turn refuses to
|
|
51
|
+
* resume.
|
|
52
|
+
*/
|
|
53
|
+
export declare function transcriptIdentityFor(state: NativeSessionState, handle: SessionHandle): TranscriptIdentity;
|
|
@@ -79,3 +79,19 @@ export function transcriptDocFor(messages, identity) {
|
|
|
79
79
|
messages: [...messages],
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The identity a session's transcript is loaded and saved under (US-002).
|
|
84
|
+
*
|
|
85
|
+
* One builder, because a turn's save and a manual compaction's save must claim
|
|
86
|
+
* exactly the same identity: the owner the invocation recorded (nax#1877) and
|
|
87
|
+
* the model that produced the history (nax#2150). A compaction that saved under
|
|
88
|
+
* a different identity would write a transcript the very next turn refuses to
|
|
89
|
+
* resume.
|
|
90
|
+
*/
|
|
91
|
+
export function transcriptIdentityFor(state, handle) {
|
|
92
|
+
return {
|
|
93
|
+
owner: state.transcriptOwners.get(handle.id),
|
|
94
|
+
model: transcriptModelIdentity(handle.modelDef?.model),
|
|
95
|
+
...(state.carryHistoryAcrossModels.has(handle.id) ? { carryAcrossModels: true } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* They share only the summarize-and-apply core below. The failure semantics
|
|
21
21
|
* stay with the callers on purpose — see each function's catch.
|
|
22
22
|
*/
|
|
23
|
-
import { type TranscriptMessage as NativeTranscriptMessage } from "./compaction.ts";
|
|
23
|
+
import { type CompactionPlan, type TranscriptMessage as NativeTranscriptMessage } from "./compaction.ts";
|
|
24
24
|
import type { LoopEventRegistry } from "./loop-events/index.ts";
|
|
25
25
|
import { type TurnAccumulator } from "./turn-accumulator.ts";
|
|
26
|
-
import type { TurnDeps } from "./turn-types.ts";
|
|
26
|
+
import type { NativeSummaryResponse, TurnDeps } from "./turn-types.ts";
|
|
27
27
|
/**
|
|
28
28
|
* The compaction dependencies, with the three the step cannot run without
|
|
29
29
|
* required. `TurnDeps` has them optional (absent disables compaction); the call
|
|
@@ -70,5 +70,33 @@ export interface OverflowCompactionArgs extends CompactionStepArgs {
|
|
|
70
70
|
*/
|
|
71
71
|
readonly error: unknown;
|
|
72
72
|
}
|
|
73
|
+
interface AppliedSummary {
|
|
74
|
+
readonly messages: readonly NativeTranscriptMessage[];
|
|
75
|
+
readonly summary: NativeSummaryResponse;
|
|
76
|
+
}
|
|
77
|
+
/** What `summarizeAndApply` should use as the summary text. */
|
|
78
|
+
interface SummarySource {
|
|
79
|
+
/**
|
|
80
|
+
* A `before_compaction` handler's replacement summary. When defined the
|
|
81
|
+
* summarizer call is skipped entirely, and the response below carries the
|
|
82
|
+
* handler's text with all-zero usage: no model call billed anything, so zero
|
|
83
|
+
* is honest (the same ruling as the transport-retry beat), and the beat the
|
|
84
|
+
* callers emit still resets the watchdog between apply and round trip.
|
|
85
|
+
*/
|
|
86
|
+
readonly replacementSummary?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The half both branches share: summarize the dropped span, then apply the
|
|
90
|
+
* summary to the transcript. A throw here is left to propagate — catching it is
|
|
91
|
+
* the proactive caller's decision, not this function's.
|
|
92
|
+
*
|
|
93
|
+
* Exported (US-002) because manual compaction is that same half: it plans with
|
|
94
|
+
* the same `prepareCompaction`, summarizes the same span with the same closure,
|
|
95
|
+
* and must not grow a second copy of the images-as-notes step. Its one
|
|
96
|
+
* difference — where the summary lands — is applied to the returned array by
|
|
97
|
+
* `applyManualCompaction`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function summarizeAndApply(messages: readonly NativeTranscriptMessage[], plan: CompactionPlan, deps: CompactionStepDeps, source?: SummarySource): Promise<AppliedSummary>;
|
|
73
100
|
export declare function runProactiveCompaction(args: CompactionStepArgs): Promise<CompactionStepResult>;
|
|
74
101
|
export declare function runOverflowCompaction(args: OverflowCompactionArgs): Promise<CompactionStepResult>;
|
|
102
|
+
export {};
|
|
@@ -28,8 +28,14 @@ import { usageBeat } from "./turn-accumulator.js";
|
|
|
28
28
|
* The half both branches share: summarize the dropped span, then apply the
|
|
29
29
|
* summary to the transcript. A throw here is left to propagate — catching it is
|
|
30
30
|
* the proactive caller's decision, not this function's.
|
|
31
|
+
*
|
|
32
|
+
* Exported (US-002) because manual compaction is that same half: it plans with
|
|
33
|
+
* the same `prepareCompaction`, summarizes the same span with the same closure,
|
|
34
|
+
* and must not grow a second copy of the images-as-notes step. Its one
|
|
35
|
+
* difference — where the summary lands — is applied to the returned array by
|
|
36
|
+
* `applyManualCompaction`.
|
|
31
37
|
*/
|
|
32
|
-
async function summarizeAndApply(messages, plan, deps, source = {}) {
|
|
38
|
+
export async function summarizeAndApply(messages, plan, deps, source = {}) {
|
|
33
39
|
const summary = source.replacementSummary !== undefined
|
|
34
40
|
? { text: source.replacementSummary, usage: { inputTokens: 0, outputTokens: 0 }, costUsd: 0 }
|
|
35
41
|
: await deps.summarize(imagesAsNotes(plan.toSummarize), plan.previousSummary);
|
|
@@ -263,6 +263,34 @@ async function dispatchToolBatch(state, params, toolCalls) {
|
|
|
263
263
|
}
|
|
264
264
|
return { shouldBreak: false };
|
|
265
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* US-001: the pending steers, as the transcript messages they become. A steer
|
|
268
|
+
* is an ordinary user message — it persists and replays — so the loop appends
|
|
269
|
+
* it exactly as it appends a `before_turn_end` followUp.
|
|
270
|
+
*/
|
|
271
|
+
function steerMessages(steers) {
|
|
272
|
+
const texts = steers?.take() ?? [];
|
|
273
|
+
return texts.map((text) => ({ role: "user", content: text }));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* US-001, round boundary: a steer accepted while a round ran joins the
|
|
277
|
+
* transcript before the next request, after that round's tool results. Two
|
|
278
|
+
* consecutive user messages (the tool results, then the steer) is intentional:
|
|
279
|
+
* the compaction path already produces that order and nax-ai passes it to the
|
|
280
|
+
* provider unchanged.
|
|
281
|
+
*
|
|
282
|
+
* Only a turn that has already run a round trip is a boundary — the first
|
|
283
|
+
* iteration IS the prompt — and never an aborted one: a cancelled turn's
|
|
284
|
+
* transcript records what happened, not what was typed as it went.
|
|
285
|
+
*/
|
|
286
|
+
function injectSteers(state, params) {
|
|
287
|
+
if (state.roundTrips === 0 || params.deps.signal?.aborted === true)
|
|
288
|
+
return;
|
|
289
|
+
const injected = steerMessages(params.opts.steers);
|
|
290
|
+
if (injected.length === 0)
|
|
291
|
+
return;
|
|
292
|
+
state.messages = [...state.messages, ...injected];
|
|
293
|
+
}
|
|
266
294
|
/**
|
|
267
295
|
* Deliberately unbounded by COUNT of varied calls. A coding agent working a
|
|
268
296
|
* story is bounded by wall clock (deps.deadline), by the idle watchdog, and
|
|
@@ -279,6 +307,10 @@ export async function runRoundTripLoop(state, params) {
|
|
|
279
307
|
state.timedOut = true;
|
|
280
308
|
break;
|
|
281
309
|
}
|
|
310
|
+
// US-001: after the deadline check, so a steer typed into a turn with no
|
|
311
|
+
// round left is never appended, and before compaction, so the steer is part
|
|
312
|
+
// of what the compaction estimate sees.
|
|
313
|
+
injectSteers(state, params);
|
|
282
314
|
const { summarizeFailed } = await maybeCompact(state, params);
|
|
283
315
|
const toolCalls = await runModelRoundTrip(state, params, summarizeFailed);
|
|
284
316
|
if (toolCalls === undefined)
|
|
@@ -301,30 +333,52 @@ export async function runRoundTripLoop(state, params) {
|
|
|
301
333
|
* at MAX_FOLLOW_UPS_PER_TURN.
|
|
302
334
|
*/
|
|
303
335
|
export async function runTurnEndPhase(state, params) {
|
|
304
|
-
const
|
|
336
|
+
const stoppedAtDispatch = params.spinFlags.stopped || params.invalidCallBudget.exceeded || state.timedOut;
|
|
305
337
|
const turnEnd = await params.loopEvents.dispatch("before_turn_end", {
|
|
306
338
|
messages: state.messages,
|
|
307
339
|
roundTrips: state.roundTrips,
|
|
308
340
|
ended: "completed",
|
|
309
|
-
|
|
341
|
+
// What the handler sees is the state it fired on, not what the dispatch
|
|
342
|
+
// itself went on to spend.
|
|
343
|
+
stopped: stoppedAtDispatch,
|
|
310
344
|
followUpsSoFar: state.followUpsSoFar,
|
|
311
345
|
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
346
|
+
// The dispatch awaits, so the budget can run out while a handler works. A
|
|
347
|
+
// continuation planned from the pre-dispatch state would then be a round trip
|
|
348
|
+
// the very next deadline check refuses: the injection would sit in the
|
|
349
|
+
// transcript having never been sent (US-001 AC-18), and a steer in it would
|
|
350
|
+
// resolve `true` for a turn that never used it. So the phase re-reads the
|
|
351
|
+
// deadline — and marks the turn timed out exactly as the round-boundary check
|
|
352
|
+
// does — before it decides anything.
|
|
353
|
+
if (!stoppedAtDispatch && params.deps.deadline?.expired() === true)
|
|
354
|
+
state.timedOut = true;
|
|
355
|
+
const stopped = stoppedAtDispatch || state.timedOut;
|
|
356
|
+
// US-001: a steer accepted while the last round ran has no round boundary
|
|
357
|
+
// left to wait for, so it continues the turn from here — unless the turn was
|
|
358
|
+
// STOPPED (a breaker, or the budget, ended it on purpose; resurrecting it
|
|
359
|
+
// would reopen the loop the breaker exists to close) or its signal fired. The
|
|
360
|
+
// signal is re-read here too: it is checked AFTER the dispatch for the same
|
|
361
|
+
// reason as the deadline.
|
|
362
|
+
const steered = stopped || params.deps.signal?.aborted === true ? [] : steerMessages(params.opts.steers);
|
|
363
|
+
const capped = state.followUpsSoFar >= MAX_FOLLOW_UPS_PER_TURN;
|
|
364
|
+
const followUp = stopped || capped ? undefined : turnEnd.followUp;
|
|
365
|
+
if (followUp === undefined && turnEnd.followUp !== undefined) {
|
|
366
|
+
getSafeLogger()?.warn("native-loop-events", "before_turn_end followUp ignored", {
|
|
367
|
+
sessionName: params.handle.id,
|
|
368
|
+
...(stopped ? { reason: "stopped" } : { reason: "cap", cap: MAX_FOLLOW_UPS_PER_TURN }),
|
|
369
|
+
});
|
|
320
370
|
}
|
|
321
|
-
|
|
371
|
+
// The cap gates the followUp ONLY: a steer is the person's own input, not a
|
|
372
|
+
// handler's injection, so it does not spend the followUp budget.
|
|
373
|
+
if (steered.length === 0 && followUp === undefined)
|
|
322
374
|
return { action: "break" };
|
|
323
|
-
// Honoured: the user
|
|
324
|
-
//
|
|
325
|
-
// round
|
|
326
|
-
// built here.
|
|
327
|
-
const messages = [...state.messages,
|
|
375
|
+
// Honoured: the user messages enter the array in the order they were typed —
|
|
376
|
+
// steers first, then the handler's followUp — and the outer loop re-enters
|
|
377
|
+
// the round-trip loop; each injection counts as its own round trip through
|
|
378
|
+
// the normal loop, and the turn's result is NOT built here.
|
|
379
|
+
const messages = [...state.messages, ...steered];
|
|
380
|
+
if (followUp !== undefined)
|
|
381
|
+
messages.push({ role: "user", content: followUp });
|
|
328
382
|
// The continuation must earn its own clean exit, and a reprieved stop's
|
|
329
383
|
// terminal-round-trip snapshot does not carry into it — the breaker's
|
|
330
384
|
// own cumulative, session-lifetime counters (nax#2047) still enforce.
|
|
@@ -334,7 +388,7 @@ export async function runTurnEndPhase(state, params) {
|
|
|
334
388
|
state: {
|
|
335
389
|
...state,
|
|
336
390
|
messages,
|
|
337
|
-
followUpsSoFar: state.followUpsSoFar + 1,
|
|
391
|
+
followUpsSoFar: state.followUpsSoFar + (followUp !== undefined ? 1 : 0),
|
|
338
392
|
completedNormally: false,
|
|
339
393
|
},
|
|
340
394
|
};
|
|
@@ -26,7 +26,7 @@ import { createLoopEventRegistry } from "./loop-events/index.js";
|
|
|
26
26
|
import { registerBuiltinLoopHandlers } from "./loop-handlers.js";
|
|
27
27
|
import { sessionAnchorFor, sessionTranscriptFor } from "./session.js";
|
|
28
28
|
import { codingToolsToDefinitions, toToolDefinitions } from "./tool-mapping.js";
|
|
29
|
-
import { historyFromTranscript, transcriptDocFor,
|
|
29
|
+
import { historyFromTranscript, transcriptDocFor, transcriptIdentityFor, } from "./transcript-identity.js";
|
|
30
30
|
import { createTurnAccumulator } from "./turn-accumulator.js";
|
|
31
31
|
import { dispatchTurnEndOnError } from "./turn-end-event.js";
|
|
32
32
|
import { createTurnEventEmitter } from "./turn-event-emitter.js";
|
|
@@ -142,11 +142,7 @@ export async function runNativeTurn(handle, prompt, opts, deps) {
|
|
|
142
142
|
// nax#2150 (P3 spec 8.3): so does a recorded different model — the loop
|
|
143
143
|
// applies that guarantee to every store's document (historyFromTranscript),
|
|
144
144
|
// whatever the session layer above decided.
|
|
145
|
-
const transcriptIdentity =
|
|
146
|
-
owner: deps.sessionState.transcriptOwners.get(handle.id),
|
|
147
|
-
model: transcriptModelIdentity(handle.modelDef?.model),
|
|
148
|
-
...(deps.sessionState.carryHistoryAcrossModels.has(handle.id) ? { carryAcrossModels: true } : {}),
|
|
149
|
-
};
|
|
145
|
+
const transcriptIdentity = transcriptIdentityFor(deps.sessionState, handle);
|
|
150
146
|
let messages = [
|
|
151
147
|
...historyFromTranscript(await store.load(handle.id), transcriptIdentity, handle.id),
|
|
152
148
|
];
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* complete()) — nax owns the conversation because nax-ai's client is
|
|
7
7
|
* stateless (ADR-027 section 10, ADR-028).
|
|
8
8
|
*/
|
|
9
|
+
import type { CompactResult } from "#src/session/agent-session-types";
|
|
9
10
|
import { type AgentSessionAdapter, type OpenSessionOpts, type SendTurnOpts, type SessionHandle, type TurnResult } from "#src/session/session-types";
|
|
10
11
|
import { type NativeCatalogOverrides } from "./client.ts";
|
|
11
12
|
import { type CredentialSource } from "./credentials/session-source.ts";
|
|
@@ -57,6 +58,24 @@ export declare class NativeSessionAdapter implements AgentSessionAdapter {
|
|
|
57
58
|
/** Resolve the current handle's model; absent input metadata is text-only. */
|
|
58
59
|
imageInput(handle: SessionHandle): Promise<boolean>;
|
|
59
60
|
sendTurn(handle: SessionHandle, prompt: string, opts: SendTurnOpts): Promise<TurnResult>;
|
|
61
|
+
/**
|
|
62
|
+
* US-002: compact the stored history on demand, for a person who is done with
|
|
63
|
+
* an exploration and starting the next one.
|
|
64
|
+
*
|
|
65
|
+
* The whole budget this call gets is the facade's: its signal aborts on the
|
|
66
|
+
* session's turn deadline or on close, and this method only adds the same
|
|
67
|
+
* per-call ceiling a turn's model calls carry. The transcript is loaded,
|
|
68
|
+
* planned against and saved by `compactHistory` (session/manual-compaction.ts);
|
|
69
|
+
* this method supplies what it cannot reach for itself — the session's client,
|
|
70
|
+
* its state, and a deadline.
|
|
71
|
+
*
|
|
72
|
+
* A missing timeout entry falls back to the turn default WITHOUT a warning,
|
|
73
|
+
* unlike `sendTurn`: the facade always arms its own deadline first, so a
|
|
74
|
+
* missing entry is not the unbounded degradation the turn's warning names.
|
|
75
|
+
*/
|
|
76
|
+
compactHistory(handle: SessionHandle, opts: {
|
|
77
|
+
signal: AbortSignal;
|
|
78
|
+
}): Promise<CompactResult>;
|
|
60
79
|
closeSession(handle: SessionHandle): Promise<void>;
|
|
61
80
|
/**
|
|
62
81
|
* Run teardown addresses a session the process no longer has a handle for,
|
|
@@ -16,13 +16,14 @@ import { _clientDeps, getNativeClient } from "./client.js";
|
|
|
16
16
|
import { createSessionCredentialStore } from "./credentials/session-source.js";
|
|
17
17
|
import { toAdapterFailure } from "./errors.js";
|
|
18
18
|
import { billedOnFailure } from "./failed-round-trip-spend.js";
|
|
19
|
-
import {
|
|
19
|
+
import { NATIVE_AGENT, parseNativeModel } from "./models.js";
|
|
20
20
|
import { createInstructionInteraction } from "./session/instruction-interaction.js";
|
|
21
|
+
import { compactHistory as compactStoredHistory } from "./session/manual-compaction.js";
|
|
21
22
|
import { closeNativeSession, createNativeSessionState, markNativeTurnOutcome, openNativeSession, systemFieldFor, } from "./session/session.js";
|
|
23
|
+
import { createSessionSummarizer, resolveSessionModel } from "./session/session-summarizer.js";
|
|
22
24
|
import { buildNativeStreamEvent } from "./session/turn-events.js";
|
|
23
25
|
import { runNativeTurn } from "./session/turn-loop.js";
|
|
24
26
|
import { readNativeTurnFailureUsage } from "./session/turn-types.js";
|
|
25
|
-
import { nativeSessionId } from "./session-affinity.js";
|
|
26
27
|
import { streamComplete } from "./stream-complete.js";
|
|
27
28
|
/**
|
|
28
29
|
* Fallback whole-turn budget when a session's timeout entry is missing.
|
|
@@ -33,16 +34,6 @@ import { streamComplete } from "./stream-complete.js";
|
|
|
33
34
|
* apply. Bounded-and-logged beats unbounded-and-quiet.
|
|
34
35
|
*/
|
|
35
36
|
const FALLBACK_TURN_TIMEOUT_SECONDS = 3600;
|
|
36
|
-
function summaryPrompt(previousSummary) {
|
|
37
|
-
const base = "Summarize the conversation above so it can be dropped from context. " +
|
|
38
|
-
"Record what was attempted, what was rejected and why, any decisions that still bind, " +
|
|
39
|
-
"and list the files read and the files modified. Be specific: this summary is the only " +
|
|
40
|
-
"memory of this work that survives.";
|
|
41
|
-
if (previousSummary === undefined)
|
|
42
|
-
return base;
|
|
43
|
-
return (`${base}\n\nAn earlier summary of still-older history follows. Merge it into your summary ` +
|
|
44
|
-
`rather than repeating or discarding it:\n\n${previousSummary}`);
|
|
45
|
-
}
|
|
46
37
|
/**
|
|
47
38
|
* US-003: the per-turn inputs the adapter forwards verbatim into `TurnDeps`:
|
|
48
39
|
* the plugin-contributed loop handlers and the facts their handlers read, plus
|
|
@@ -153,18 +144,23 @@ export class NativeSessionAdapter {
|
|
|
153
144
|
return resolved.input?.includes("image") === true;
|
|
154
145
|
}
|
|
155
146
|
async sendTurn(handle, prompt, opts) {
|
|
156
|
-
const { provider, model, effort } = parseNativeModel(handle.modelDef?.model ?? "");
|
|
157
|
-
const thinking = toThinkingLevel(effort);
|
|
158
147
|
const client = await this.client();
|
|
159
|
-
|
|
148
|
+
// US-002: the model, rate card, window, settings and session id come from
|
|
149
|
+
// the same resolution manual compaction uses, so the two cannot disagree
|
|
150
|
+
// about what this session is.
|
|
151
|
+
const sessionModel = await resolveSessionModel(client, this.state, handle);
|
|
152
|
+
const { rates, pricingSource, contextWindow, thinking, provider, sessionId } = sessionModel;
|
|
153
|
+
const resolved = sessionModel.model;
|
|
160
154
|
// Refuse before runNativeTurn: its error path deliberately saves history.
|
|
161
155
|
assertImageInput(handle, resolved, opts.images);
|
|
162
|
-
const contextWindow = resolveContextWindow(handle.modelDef?.contextWindow, resolved.contextWindow);
|
|
163
156
|
this.state.repositoryInstructions
|
|
164
157
|
.get(handle.id)
|
|
165
158
|
?.setContextBudget(contextWindow, this.state.systemPrompts.get(handle.id));
|
|
166
|
-
|
|
167
|
-
|
|
159
|
+
// Read BEFORE the turn: an absent setting must stay absent in TurnDeps, so
|
|
160
|
+
// "this session recorded none" and "recorded settings that disable
|
|
161
|
+
// compaction" stay distinguishable for the loop. The fallback the
|
|
162
|
+
// resolution applies is the manual path's keep budget, nothing more.
|
|
163
|
+
const recordedCompaction = this.state.compaction.get(handle.id);
|
|
168
164
|
const storedTimeoutSeconds = this.state.timeouts.get(handle.id);
|
|
169
165
|
if (storedTimeoutSeconds === undefined) {
|
|
170
166
|
getSafeLogger()?.warn("native-adapter", "session has no recorded timeout; falling back to the default budget", {
|
|
@@ -173,15 +169,12 @@ export class NativeSessionAdapter {
|
|
|
173
169
|
});
|
|
174
170
|
}
|
|
175
171
|
const timeoutSeconds = storedTimeoutSeconds ?? FALLBACK_TURN_TIMEOUT_SECONDS;
|
|
176
|
-
// Keyed on the session, so every turn of one conversation carries the same
|
|
177
|
-
// id and the provider can keep its cache warm across them.
|
|
178
|
-
const sessionId = nativeSessionId(handle.id);
|
|
179
172
|
// S3-4: the session's system prompt (facade `instructions`), or `{}`.
|
|
180
173
|
// Resolved here so the `complete` closure spreads it without a branch.
|
|
181
174
|
const instructionInteraction = createInstructionInteraction(this.state.repositoryInstructions.get(handle.id), opts.interactionHandler);
|
|
182
175
|
// One budget for the whole turn, not one per round-trip. Created here
|
|
183
176
|
// because this is where `timeoutSeconds` is known; consulted by the loop.
|
|
184
|
-
const deadline = createTurnDeadline(timeoutSeconds);
|
|
177
|
+
const deadline = createTurnDeadline(timeoutSeconds, () => _adapterDeps.now());
|
|
185
178
|
const hooks = this.state.streamHooks.get(handle.id);
|
|
186
179
|
// One callId per turn, mirroring SpawnAcpSession.prompt(). `runId` is
|
|
187
180
|
// backfilled by the runtime's forwarding closure, which is the only place
|
|
@@ -239,9 +232,7 @@ export class NativeSessionAdapter {
|
|
|
239
232
|
sessionState: this.state,
|
|
240
233
|
deadline,
|
|
241
234
|
contextWindow,
|
|
242
|
-
...(
|
|
243
|
-
? { compaction: this.state.compaction.get(handle.id) }
|
|
244
|
-
: {}),
|
|
235
|
+
...(recordedCompaction !== undefined ? { compaction: sessionModel.compaction } : {}),
|
|
245
236
|
...(this.state.transportRetry.get(handle.id) !== undefined
|
|
246
237
|
? { transportRetry: this.state.transportRetry.get(handle.id) }
|
|
247
238
|
: {}),
|
|
@@ -270,45 +261,7 @@ export class NativeSessionAdapter {
|
|
|
270
261
|
onActivity: (activity) => {
|
|
271
262
|
hooks?.onStreamActivity?.(buildNativeStreamEvent(eventBase, activity, Date.now()));
|
|
272
263
|
},
|
|
273
|
-
summarize:
|
|
274
|
-
// Same model, same clock, no tools. The prompt asks for what a coding
|
|
275
|
-
// agent needs back: what was tried, what was rejected and why, and the
|
|
276
|
-
// files touched -- without them the agent re-reads what it already read.
|
|
277
|
-
const remainingMs = deadline.remainingMs();
|
|
278
|
-
const controller = new AbortController();
|
|
279
|
-
const timer = remainingMs !== undefined ? _adapterDeps.setTimeout(() => controller.abort(), remainingMs) : undefined;
|
|
280
|
-
// US-002: the per-call signal still combines with the per-turn signal
|
|
281
|
-
// (watchdog + deadline + caller), so a turn cancel ends the summary
|
|
282
|
-
// even mid-call. The per-call timer is the additional budget on top.
|
|
283
|
-
const signal = AbortSignal.any(opts.signal !== undefined
|
|
284
|
-
? [opts.signal, controller.signal, turnController.signal, deadlineController.signal]
|
|
285
|
-
: [controller.signal, turnController.signal, deadlineController.signal]);
|
|
286
|
-
try {
|
|
287
|
-
const res = await client.complete(resolved, {
|
|
288
|
-
messages: [...span, { role: "user", content: summaryPrompt(previousSummary) }],
|
|
289
|
-
sessionId,
|
|
290
|
-
signal,
|
|
291
|
-
// A summary restates the transcript; it does not need reasoning.
|
|
292
|
-
// "off", not an absent level: absent means the backend default,
|
|
293
|
-
// which on a model that cannot disable thinking is adaptive at
|
|
294
|
-
// effort high (#2686). The client clamps "off" to the least
|
|
295
|
-
// thinking such a model allows.
|
|
296
|
-
thinking: "off",
|
|
297
|
-
});
|
|
298
|
-
const summaryUsage = res.usage;
|
|
299
|
-
const { costUsd, resolvedRates } = priceCall(summaryUsage, rates);
|
|
300
|
-
return {
|
|
301
|
-
text: res.text,
|
|
302
|
-
usage: summaryUsage,
|
|
303
|
-
costUsd,
|
|
304
|
-
rates: resolvedRates,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
finally {
|
|
308
|
-
if (timer !== undefined)
|
|
309
|
-
_adapterDeps.clearTimeout(timer);
|
|
310
|
-
}
|
|
311
|
-
},
|
|
264
|
+
summarize: createSessionSummarizer({ resolution: sessionModel, deadline, signal: turnSignal }),
|
|
312
265
|
complete: async (messages, tools, requestOptions, onDelta) => {
|
|
313
266
|
// The controller is armed with what is LEFT of the turn, so N
|
|
314
267
|
// round-trips can no longer add up to N x timeoutSeconds. Still
|
|
@@ -430,6 +383,31 @@ export class NativeSessionAdapter {
|
|
|
430
383
|
// so the stamp is read here, after the loop.
|
|
431
384
|
return { ...result, ...authFields(provider, this.ownStore) };
|
|
432
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* US-002: compact the stored history on demand, for a person who is done with
|
|
388
|
+
* an exploration and starting the next one.
|
|
389
|
+
*
|
|
390
|
+
* The whole budget this call gets is the facade's: its signal aborts on the
|
|
391
|
+
* session's turn deadline or on close, and this method only adds the same
|
|
392
|
+
* per-call ceiling a turn's model calls carry. The transcript is loaded,
|
|
393
|
+
* planned against and saved by `compactHistory` (session/manual-compaction.ts);
|
|
394
|
+
* this method supplies what it cannot reach for itself — the session's client,
|
|
395
|
+
* its state, and a deadline.
|
|
396
|
+
*
|
|
397
|
+
* A missing timeout entry falls back to the turn default WITHOUT a warning,
|
|
398
|
+
* unlike `sendTurn`: the facade always arms its own deadline first, so a
|
|
399
|
+
* missing entry is not the unbounded degradation the turn's warning names.
|
|
400
|
+
*/
|
|
401
|
+
async compactHistory(handle, opts) {
|
|
402
|
+
const timeoutSeconds = this.state.timeouts.get(handle.id) ?? FALLBACK_TURN_TIMEOUT_SECONDS;
|
|
403
|
+
return compactStoredHistory({
|
|
404
|
+
state: this.state,
|
|
405
|
+
handle,
|
|
406
|
+
client: await this.client(),
|
|
407
|
+
deadline: createTurnDeadline(timeoutSeconds, () => _adapterDeps.now()),
|
|
408
|
+
signal: opts.signal,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
433
411
|
closeSession(handle) {
|
|
434
412
|
// The adapter interface has no failure signal, so the verdict comes from the
|
|
435
413
|
// session's last turn (markNativeTurnOutcome) rather than from this call.
|