@arnilo/prism 0.9.0 → 0.10.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 +24 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.js +19 -5
- package/dist/agent-run-state.d.ts +26 -5
- package/dist/agent-run-state.js +97 -1
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/session/assemble.js +156 -9
- package/dist/agent-session/session/persist.js +11 -5
- package/dist/agent-session/session/provider-round.js +54 -13
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +58 -5
- package/dist/agent-session/session/types.d.ts +20 -2
- package/dist/agent-session/session.d.ts +65 -4
- package/dist/agent-session/session.js +156 -16
- package/dist/context-budget.d.ts +11 -0
- package/dist/context-budget.js +33 -2
- package/dist/contracts-core/agent.d.ts +26 -5
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +8 -3
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +6 -1
- package/dist/contracts-core/run-limits.d.ts +10 -1
- package/dist/contracts-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +37 -3
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/types.d.ts +10 -0
- package/dist/guardrail-packs/validation-respect.js +16 -0
- package/dist/guardrails.d.ts +42 -1
- package/dist/guardrails.js +124 -15
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -4
- package/dist/middleware.d.ts +1 -1
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.js +13 -0
- package/dist/testing/prefix-stability-conformance.d.ts +29 -0
- package/dist/testing/prefix-stability-conformance.js +91 -23
- package/dist/tools.js +10 -3
- package/docs/agent-events.md +12 -8
- package/docs/agent-session-runtime.md +9 -6
- package/docs/caveman.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/durable-runs.md +4 -3
- package/docs/embeddings.md +5 -1
- package/docs/execution-timeline.md +3 -2
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/index.md +18 -15
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migration.md +13 -0
- package/docs/options-index.md +3 -1
- package/docs/policy-and-audit.md +14 -1
- package/docs/prefix-stability-conformance.md +57 -7
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +1 -0
- package/docs/rag.md +93 -6
- package/docs/release-and-install.md +42 -39
- package/docs/runs-and-usage.md +17 -8
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/package.json +4 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
|
|
2
2
|
import { ActiveDurableRun, ActiveDurableRunExtras } from "../agent-approval.js";
|
|
3
|
-
import type { PendingToolCall, StoredAgentRunState } from "../agent-run-state.js";
|
|
3
|
+
import type { PendingToolCall, PersistedGuardrailPacks, StoredAgentRunState } from "../agent-run-state.js";
|
|
4
4
|
import { type AttentionFoldLedger, type AttentionStickyFrontier, type PersistedAttentionFoldLedger, type PersistedAttentionStickyFrontier } from "../attention-compiler.js";
|
|
5
|
-
import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ContextMeter, ErrorInfo, Guardrails, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolCallSummary, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
|
|
5
|
+
import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ContextMeter, ErrorInfo, GuardrailPackRef, Guardrails, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolCallSummary, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
|
|
6
6
|
import type { AgentIdentity } from "../identity.js";
|
|
7
7
|
import type { AgentInput } from "../input.js";
|
|
8
8
|
import type { RunLimitTracker } from "../run-limits.js";
|
|
@@ -14,6 +14,10 @@ export declare class RuntimeAgentSession implements AgentSession {
|
|
|
14
14
|
private readonly metadata?;
|
|
15
15
|
private readonly store;
|
|
16
16
|
private readonly subscribers;
|
|
17
|
+
/** Plan 106 R2: `session_start` is dispatched at the first run start, once per runtime session. */
|
|
18
|
+
private sessionOpened;
|
|
19
|
+
/** Plan 106 R2: `close()` dispatches `session_shutdown` and closes subscribers exactly once. */
|
|
20
|
+
private closed;
|
|
17
21
|
private currentLeafId?;
|
|
18
22
|
private history;
|
|
19
23
|
private activeRun?;
|
|
@@ -31,9 +35,15 @@ export declare class RuntimeAgentSession implements AgentSession {
|
|
|
31
35
|
private activeIdempotencyKey?;
|
|
32
36
|
private activeGuardrails?;
|
|
33
37
|
/** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
|
|
34
|
-
|
|
38
|
+
packGuardrails?: Guardrails;
|
|
39
|
+
/** Plan 104 Task 3: `ask` rules as the durable charge-time gate (a match records `interrupt`). */
|
|
40
|
+
packAskGate?: Guardrails;
|
|
41
|
+
/** Plan 104 Task 3: the same `ask` rules as plain blocks for a run that cannot suspend. */
|
|
42
|
+
packAskBlocks?: Guardrails;
|
|
35
43
|
/** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
|
|
36
|
-
private
|
|
44
|
+
private packRefs?;
|
|
45
|
+
/** Plan 104 Task 2: compiled refs + live pack state, replaced by `restoreGuardrailPacks` on resume. */
|
|
46
|
+
private compiledGuardrailPacks?;
|
|
37
47
|
activeMetadata?: Readonly<Record<string, unknown>>;
|
|
38
48
|
activePromptVersion?: PromptVersionRef;
|
|
39
49
|
activeLimits?: RunLimitTracker;
|
|
@@ -95,11 +105,28 @@ export declare class RuntimeAgentSession implements AgentSession {
|
|
|
95
105
|
clearActivatedTools(): void;
|
|
96
106
|
/** Plan 018 Task 6: restore persisted loaded-skill bodies (already validated fail-closed at load). */
|
|
97
107
|
restoreLoadedSkillBodies(bodies: readonly LoadedSkillBodiesEntry[]): void;
|
|
108
|
+
/** Plan 104 T2: the refs this session actually enforces (restored ones after a resume). */
|
|
109
|
+
get guardrailPackRefs(): readonly GuardrailPackRef[] | undefined;
|
|
110
|
+
/** Plan 104 T2: pack refs + live pack-owned state for a durable checkpoint (opt-in with `persistSessionState`). */
|
|
111
|
+
serializedGuardrailPackState(): PersistedGuardrailPacks | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Plan 104 T2: recompile checkpoint packs before the resumed run's first turn. `state` present
|
|
114
|
+
* (even empty) marks a restore, so unknown ids, version mismatches, and codec-less state fail
|
|
115
|
+
* closed as `AgentRunStateError` — never a session that silently enforces less than it did.
|
|
116
|
+
*/
|
|
117
|
+
restoreGuardrailPacks(refs: readonly GuardrailPackRef[], state?: Readonly<Record<string, unknown>>): void;
|
|
98
118
|
private ledgerChain;
|
|
99
119
|
private ledgerFailure;
|
|
100
120
|
private snapshotGeneration;
|
|
101
121
|
private snapshotCache?;
|
|
102
122
|
private readonly snapshotCacheTtlMs;
|
|
123
|
+
/**
|
|
124
|
+
* Plan 103 T4: identity-keyed meter cache. Holds only the last public meter value
|
|
125
|
+
* plus the identity of everything the cold read consumed (`snapshotGeneration`,
|
|
126
|
+
* leaf, active meter/limits, and the history array reference + length, which catches
|
|
127
|
+
* in-place `history.push` during a run) — never history content, never an estimator.
|
|
128
|
+
*/
|
|
129
|
+
private meterCache?;
|
|
103
130
|
constructor(config: AgentSessionConfig & {
|
|
104
131
|
readonly agent: Agent;
|
|
105
132
|
});
|
|
@@ -111,15 +138,44 @@ export declare class RuntimeAgentSession implements AgentSession {
|
|
|
111
138
|
* `provider_turn_finished.budgets` resolves them. Before any provider turn in
|
|
112
139
|
* this session it estimates stored history, so a non-reporting model still
|
|
113
140
|
* shows a working meter instead of zero. Never billing; estimates are labeled.
|
|
141
|
+
*
|
|
142
|
+
* Plan 103 T4: reads are cached until the history generation, leaf, history
|
|
143
|
+
* length, or active-run identity changes, so a per-frame poll pays one estimate
|
|
144
|
+
* per mutation instead of one per read. The cached value is frozen and is
|
|
145
|
+
* identical (`===`) to the previous read while nothing changed.
|
|
114
146
|
*/
|
|
115
147
|
contextMeter(): ContextMeter;
|
|
148
|
+
/** Cold path of `contextMeter()`: one estimate over stored history plus cap/budget resolution. */
|
|
149
|
+
private measureContextMeter;
|
|
150
|
+
/**
|
|
151
|
+
* Live events for this session. A run-scoped subscriber (the default) is closed when the run ends,
|
|
152
|
+
* suspends, or is denied; `SubscribeOptions.acrossRuns: true` keeps one subscriber open across runs
|
|
153
|
+
* of the same session until the host closes it, the session tears it down, or its bounded queue
|
|
154
|
+
* overflows under the default policy. Subscribe before `run()`; the consumer loop and `run()` must
|
|
155
|
+
* run concurrently, since events are only emitted during a live run.
|
|
156
|
+
*/
|
|
116
157
|
subscribe(options?: SubscribeOptions): AsyncIterable<AgentEvent>;
|
|
158
|
+
private createSubscriber;
|
|
117
159
|
run(input: AgentInput, options?: RunOptions): Promise<AgentRunResult>;
|
|
118
160
|
steer(input: AgentInput, options?: SteerOptions): void;
|
|
119
161
|
resumeDurable(state: StoredAgentRunState, runState: AgentRunStateOptions, ownership?: OwnershipScope, signal?: AbortSignal, decisions?: ReadonlyMap<string, RunDecision>, extras?: ActiveDurableRunExtras): Promise<AgentRunResult>;
|
|
120
162
|
recordDurableResumption(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
|
|
121
163
|
recordDurableDenial(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
|
|
122
164
|
private runInternal;
|
|
165
|
+
/**
|
|
166
|
+
* Plan 106 R2: dispatch `session_start` once per session, at its first run start (including the
|
|
167
|
+
* first run of a session rebuilt from a durable checkpoint). The run assembler awaits it right
|
|
168
|
+
* after `agent_started`/`agent_resumed`, so session-scoped provisioning is done before the first
|
|
169
|
+
* turn while the runtime's synchronous emit burst stays intact. Middleware error policy decides
|
|
170
|
+
* whether a failure surfaces or becomes an `extension_error` event.
|
|
171
|
+
*/
|
|
172
|
+
openSession(runId: string): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* Plan 106 R2: session teardown. Dispatches `session_shutdown` middleware once (idempotent) and
|
|
175
|
+
* then closes every subscriber, run-scoped and `acrossRuns` alike. Call it after the active run
|
|
176
|
+
* settles; `closeSubscribers()` remains the subscriber-only seam.
|
|
177
|
+
*/
|
|
178
|
+
close(): Promise<void>;
|
|
123
179
|
prompt(input: string, options?: RunOptions): Promise<AgentRunResult>;
|
|
124
180
|
stream(input: AgentInput, options?: RunOptions & SubscribeOptions): AsyncGenerator<AgentEvent>;
|
|
125
181
|
buildRunResult(input: {
|
|
@@ -149,6 +205,11 @@ export declare class RuntimeAgentSession implements AgentSession {
|
|
|
149
205
|
resolveRunProvider(options: RunOptions): void;
|
|
150
206
|
resolveRunSkills(options: RunOptions, tools: readonly ToolDefinition[]): readonly Skill[];
|
|
151
207
|
emit(event: AgentEvent): void;
|
|
208
|
+
/**
|
|
209
|
+
* Run end (finish, suspension, or denial): closes the run-scoped subscribers only. Subscribers that
|
|
210
|
+
* opted into `SubscribeOptions.acrossRuns` stay open for the next run of this session.
|
|
211
|
+
*/
|
|
212
|
+
closeRunSubscribers(): void;
|
|
152
213
|
closeSubscribers(): void;
|
|
153
214
|
drainLedger(): Promise<void>;
|
|
154
215
|
applyPendingSteers(runId: string, metadata: Readonly<Record<string, unknown>>, signal: AbortSignal): Promise<boolean>;
|
|
@@ -3,8 +3,8 @@ import { policyList } from "../agent-tool-dispatch.js";
|
|
|
3
3
|
import { createAttentionFoldLedger, createAttentionStickyFrontier, resolveInputCap, restoreAttentionStickyFrontier, serializeAttentionFoldLedger, serializeAttentionStickyFrontier, } from "../attention-compiler.js";
|
|
4
4
|
import { createDefaultCompactionStrategy, isCompactionEntryData } from "../compaction.js";
|
|
5
5
|
import { estimateAssemblyTokens, estimateMessageTokens, estimateTextTokens } from "../context-budget.js";
|
|
6
|
-
import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, resolveShouldCompact, } from "../contracts.js";
|
|
7
|
-
import {
|
|
6
|
+
import { AgentRunStateError, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, resolveShouldCompact, } from "../contracts.js";
|
|
7
|
+
import { compileGuardrailPacksWithState, GuardrailError, GuardrailPackError, runGuardrails } from "../guardrails.js";
|
|
8
8
|
import { applyDefaultProviderRequestOptions, createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult, } from "../provider-request-policy.js";
|
|
9
9
|
import { redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "../redaction.js";
|
|
10
10
|
import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext } from "../session-stores.js";
|
|
@@ -23,6 +23,10 @@ export class RuntimeAgentSession {
|
|
|
23
23
|
metadata;
|
|
24
24
|
store;
|
|
25
25
|
subscribers = new Set();
|
|
26
|
+
/** Plan 106 R2: `session_start` is dispatched at the first run start, once per runtime session. */
|
|
27
|
+
sessionOpened = false;
|
|
28
|
+
/** Plan 106 R2: `close()` dispatches `session_shutdown` and closes subscribers exactly once. */
|
|
29
|
+
closed = false;
|
|
26
30
|
currentLeafId;
|
|
27
31
|
history = [];
|
|
28
32
|
activeRun;
|
|
@@ -41,8 +45,14 @@ export class RuntimeAgentSession {
|
|
|
41
45
|
activeGuardrails;
|
|
42
46
|
/** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
|
|
43
47
|
packGuardrails;
|
|
48
|
+
/** Plan 104 Task 3: `ask` rules as the durable charge-time gate (a match records `interrupt`). */
|
|
49
|
+
packAskGate;
|
|
50
|
+
/** Plan 104 Task 3: the same `ask` rules as plain blocks for a run that cannot suspend. */
|
|
51
|
+
packAskBlocks;
|
|
44
52
|
/** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
|
|
45
|
-
|
|
53
|
+
packRefs;
|
|
54
|
+
/** Plan 104 Task 2: compiled refs + live pack state, replaced by `restoreGuardrailPacks` on resume. */
|
|
55
|
+
compiledGuardrailPacks;
|
|
46
56
|
activeMetadata;
|
|
47
57
|
activePromptVersion;
|
|
48
58
|
activeLimits;
|
|
@@ -127,11 +137,51 @@ export class RuntimeAgentSession {
|
|
|
127
137
|
for (const entry of bodies)
|
|
128
138
|
this.loadedSkills.add(entry.name);
|
|
129
139
|
}
|
|
140
|
+
/** Plan 104 T2: the refs this session actually enforces (restored ones after a resume). */
|
|
141
|
+
get guardrailPackRefs() {
|
|
142
|
+
return this.packRefs;
|
|
143
|
+
}
|
|
144
|
+
/** Plan 104 T2: pack refs + live pack-owned state for a durable checkpoint (opt-in with `persistSessionState`). */
|
|
145
|
+
serializedGuardrailPackState() {
|
|
146
|
+
const compiled = this.compiledGuardrailPacks;
|
|
147
|
+
if (!compiled || compiled.packs.length === 0)
|
|
148
|
+
return undefined;
|
|
149
|
+
const state = compiled.snapshotState();
|
|
150
|
+
return { packs: compiled.packs, ...(state ? { state } : {}) };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Plan 104 T2: recompile checkpoint packs before the resumed run's first turn. `state` present
|
|
154
|
+
* (even empty) marks a restore, so unknown ids, version mismatches, and codec-less state fail
|
|
155
|
+
* closed as `AgentRunStateError` — never a session that silently enforces less than it did.
|
|
156
|
+
*/
|
|
157
|
+
restoreGuardrailPacks(refs, state) {
|
|
158
|
+
let compiled;
|
|
159
|
+
try {
|
|
160
|
+
compiled = compileGuardrailPacksWithState(refs, undefined, state ?? {});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof GuardrailPackError)
|
|
164
|
+
throw new AgentRunStateError(`Cannot restore guardrail packs: ${error.message}`);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
this.compiledGuardrailPacks = compiled;
|
|
168
|
+
this.packGuardrails = compiled.guardrails;
|
|
169
|
+
this.packAskGate = compiled.askGate;
|
|
170
|
+
this.packAskBlocks = compiled.askBlocks;
|
|
171
|
+
this.packRefs = refs;
|
|
172
|
+
}
|
|
130
173
|
ledgerChain = Promise.resolve();
|
|
131
174
|
ledgerFailure;
|
|
132
175
|
snapshotGeneration = 0;
|
|
133
176
|
snapshotCache;
|
|
134
177
|
snapshotCacheTtlMs;
|
|
178
|
+
/**
|
|
179
|
+
* Plan 103 T4: identity-keyed meter cache. Holds only the last public meter value
|
|
180
|
+
* plus the identity of everything the cold read consumed (`snapshotGeneration`,
|
|
181
|
+
* leaf, active meter/limits, and the history array reference + length, which catches
|
|
182
|
+
* in-place `history.push` during a run) — never history content, never an estimator.
|
|
183
|
+
*/
|
|
184
|
+
meterCache;
|
|
135
185
|
constructor(config) {
|
|
136
186
|
this.id = config.id ?? randomId("session");
|
|
137
187
|
this.agent = config.agent;
|
|
@@ -139,11 +189,14 @@ export class RuntimeAgentSession {
|
|
|
139
189
|
this.store = config.store ?? config.agent.config.store ?? createMemorySessionStore();
|
|
140
190
|
this.currentLeafId = config.leafId;
|
|
141
191
|
this.snapshotCacheTtlMs = resolveSnapshotCacheTtlMs(config.snapshotCacheTtlMs);
|
|
142
|
-
this.
|
|
143
|
-
this.
|
|
192
|
+
this.compiledGuardrailPacks = compileGuardrailPacksWithState(config.guardrailPacks);
|
|
193
|
+
this.packGuardrails = this.compiledGuardrailPacks.guardrails;
|
|
194
|
+
this.packAskGate = this.compiledGuardrailPacks.askGate;
|
|
195
|
+
this.packAskBlocks = this.compiledGuardrailPacks.askBlocks;
|
|
196
|
+
this.packRefs = config.guardrailPacks;
|
|
144
197
|
const usageEstimation = config.agent.config.usageEstimation;
|
|
145
|
-
if (usageEstimation !== undefined && usageEstimation !== "fallback" && usageEstimation !== "off") {
|
|
146
|
-
throw new TypeError('usageEstimation must be "fallback" or "
|
|
198
|
+
if (usageEstimation !== undefined && usageEstimation !== "fallback" && usageEstimation !== "off" && usageEstimation !== "strict") {
|
|
199
|
+
throw new TypeError('usageEstimation must be "fallback", "off", or "strict"');
|
|
147
200
|
}
|
|
148
201
|
}
|
|
149
202
|
get leafId() {
|
|
@@ -156,8 +209,37 @@ export class RuntimeAgentSession {
|
|
|
156
209
|
* `provider_turn_finished.budgets` resolves them. Before any provider turn in
|
|
157
210
|
* this session it estimates stored history, so a non-reporting model still
|
|
158
211
|
* shows a working meter instead of zero. Never billing; estimates are labeled.
|
|
212
|
+
*
|
|
213
|
+
* Plan 103 T4: reads are cached until the history generation, leaf, history
|
|
214
|
+
* length, or active-run identity changes, so a per-frame poll pays one estimate
|
|
215
|
+
* per mutation instead of one per read. The cached value is frozen and is
|
|
216
|
+
* identical (`===`) to the previous read while nothing changed.
|
|
159
217
|
*/
|
|
160
218
|
contextMeter() {
|
|
219
|
+
const cached = this.meterCache;
|
|
220
|
+
if (cached &&
|
|
221
|
+
cached.leafId === this.currentLeafId &&
|
|
222
|
+
cached.generation === this.snapshotGeneration &&
|
|
223
|
+
cached.meter === this.activeInputMeter &&
|
|
224
|
+
cached.limits === this.activeLimits &&
|
|
225
|
+
cached.history === this.history &&
|
|
226
|
+
cached.historyLength === this.history.length) {
|
|
227
|
+
return cached.value;
|
|
228
|
+
}
|
|
229
|
+
const value = Object.freeze(this.measureContextMeter());
|
|
230
|
+
this.meterCache = {
|
|
231
|
+
leafId: this.currentLeafId,
|
|
232
|
+
generation: this.snapshotGeneration,
|
|
233
|
+
meter: this.activeInputMeter,
|
|
234
|
+
limits: this.activeLimits,
|
|
235
|
+
history: this.history,
|
|
236
|
+
historyLength: this.history.length,
|
|
237
|
+
value,
|
|
238
|
+
};
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
/** Cold path of `contextMeter()`: one estimate over stored history plus cap/budget resolution. */
|
|
242
|
+
measureContextMeter() {
|
|
161
243
|
const model = this.agent.config.model;
|
|
162
244
|
const inputTokens = this.activeInputMeter?.tokens ?? estimateMessageTokens(this.history, model.model).tokens;
|
|
163
245
|
const source = this.activeInputMeter?.source ?? "estimated";
|
|
@@ -179,7 +261,17 @@ export class RuntimeAgentSession {
|
|
|
179
261
|
...(inputCap === undefined ? {} : { usedRatio: inputTokens / inputCap }),
|
|
180
262
|
};
|
|
181
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Live events for this session. A run-scoped subscriber (the default) is closed when the run ends,
|
|
266
|
+
* suspends, or is denied; `SubscribeOptions.acrossRuns: true` keeps one subscriber open across runs
|
|
267
|
+
* of the same session until the host closes it, the session tears it down, or its bounded queue
|
|
268
|
+
* overflows under the default policy. Subscribe before `run()`; the consumer loop and `run()` must
|
|
269
|
+
* run concurrently, since events are only emitted during a live run.
|
|
270
|
+
*/
|
|
182
271
|
subscribe(options = {}) {
|
|
272
|
+
return this.createSubscriber(options);
|
|
273
|
+
}
|
|
274
|
+
createSubscriber(options) {
|
|
183
275
|
const subscriber = new EventSubscriber(this.id, options, () => this.subscribers.delete(subscriber));
|
|
184
276
|
this.subscribers.add(subscriber);
|
|
185
277
|
return subscriber;
|
|
@@ -233,7 +325,7 @@ export class RuntimeAgentSession {
|
|
|
233
325
|
this.activeLedger = undefined;
|
|
234
326
|
this.activeOwnership = undefined;
|
|
235
327
|
this.activeRedactor = undefined;
|
|
236
|
-
this.
|
|
328
|
+
this.closeRunSubscribers();
|
|
237
329
|
}
|
|
238
330
|
}
|
|
239
331
|
async recordDurableDenial(runId, interruption, version, ownership) {
|
|
@@ -248,25 +340,58 @@ export class RuntimeAgentSession {
|
|
|
248
340
|
this.activeLedger = undefined;
|
|
249
341
|
this.activeOwnership = undefined;
|
|
250
342
|
this.activeRedactor = undefined;
|
|
251
|
-
this.
|
|
343
|
+
this.closeRunSubscribers();
|
|
252
344
|
}
|
|
253
345
|
}
|
|
254
346
|
async runInternal(input, options, runId, resumed) {
|
|
255
347
|
return executeRun(asSessionHost(this), input, options, runId, resumed);
|
|
256
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Plan 106 R2: dispatch `session_start` once per session, at its first run start (including the
|
|
351
|
+
* first run of a session rebuilt from a durable checkpoint). The run assembler awaits it right
|
|
352
|
+
* after `agent_started`/`agent_resumed`, so session-scoped provisioning is done before the first
|
|
353
|
+
* turn while the runtime's synchronous emit burst stays intact. Middleware error policy decides
|
|
354
|
+
* whether a failure surfaces or becomes an `extension_error` event.
|
|
355
|
+
*/
|
|
356
|
+
async openSession(runId) {
|
|
357
|
+
if (this.sessionOpened)
|
|
358
|
+
return;
|
|
359
|
+
this.sessionOpened = true;
|
|
360
|
+
await this.agent.config.middleware?.run("session_start", { sessionId: this.id, runId });
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Plan 106 R2: session teardown. Dispatches `session_shutdown` middleware once (idempotent) and
|
|
364
|
+
* then closes every subscriber, run-scoped and `acrossRuns` alike. Call it after the active run
|
|
365
|
+
* settles; `closeSubscribers()` remains the subscriber-only seam.
|
|
366
|
+
*/
|
|
367
|
+
async close() {
|
|
368
|
+
if (this.closed)
|
|
369
|
+
return;
|
|
370
|
+
this.closed = true;
|
|
371
|
+
try {
|
|
372
|
+
await this.agent.config.middleware?.run("session_shutdown", { sessionId: this.id });
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
this.closeSubscribers();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
257
378
|
prompt(input, options) {
|
|
258
379
|
return this.run(input, options);
|
|
259
380
|
}
|
|
260
381
|
async *stream(input, options = {}) {
|
|
261
382
|
const { maxQueuedEvents, overflow, ...runOptions } = options;
|
|
262
|
-
const
|
|
383
|
+
const subscriber = this.createSubscriber({ maxQueuedEvents, overflow });
|
|
263
384
|
let runOwnedId;
|
|
264
385
|
let settled = false;
|
|
386
|
+
// This subscription is stream()'s own, so it does not depend on the run-end close: settling the
|
|
387
|
+
// run closes it too, which also unblocks the consumer loop when the run fails before it ever
|
|
388
|
+
// emits (a pre-flight validation rejection returns before run-end cleanup).
|
|
265
389
|
const runPromise = this.run(input, runOptions).finally(() => {
|
|
266
390
|
settled = true;
|
|
391
|
+
subscriber.close();
|
|
267
392
|
});
|
|
268
393
|
try {
|
|
269
|
-
for await (const event of
|
|
394
|
+
for await (const event of subscriber) {
|
|
270
395
|
if ("runId" in event && typeof event.runId === "string") {
|
|
271
396
|
if (runOwnedId === undefined && event.type === "agent_started")
|
|
272
397
|
runOwnedId = event.runId;
|
|
@@ -278,6 +403,7 @@ export class RuntimeAgentSession {
|
|
|
278
403
|
await runPromise;
|
|
279
404
|
}
|
|
280
405
|
finally {
|
|
406
|
+
subscriber.close();
|
|
281
407
|
if (!settled) {
|
|
282
408
|
this.abort(new Error("stream consumer closed"));
|
|
283
409
|
await runPromise.catch(() => undefined);
|
|
@@ -333,7 +459,7 @@ export class RuntimeAgentSession {
|
|
|
333
459
|
store: this.store,
|
|
334
460
|
leafId: options.leafId ?? this.currentLeafId,
|
|
335
461
|
metadata: this.metadata,
|
|
336
|
-
...(this.
|
|
462
|
+
...(this.packRefs ? { guardrailPacks: this.packRefs } : {}),
|
|
337
463
|
});
|
|
338
464
|
}
|
|
339
465
|
async clone(options = {}) {
|
|
@@ -356,7 +482,7 @@ export class RuntimeAgentSession {
|
|
|
356
482
|
store: this.store,
|
|
357
483
|
leafId: branch.length ? remap.get(branch[branch.length - 1].id) : undefined,
|
|
358
484
|
metadata: this.metadata,
|
|
359
|
-
...(this.
|
|
485
|
+
...(this.packRefs ? { guardrailPacks: this.packRefs } : {}),
|
|
360
486
|
});
|
|
361
487
|
}
|
|
362
488
|
branchReader() {
|
|
@@ -418,6 +544,15 @@ export class RuntimeAgentSession {
|
|
|
418
544
|
});
|
|
419
545
|
}
|
|
420
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Run end (finish, suspension, or denial): closes the run-scoped subscribers only. Subscribers that
|
|
549
|
+
* opted into `SubscribeOptions.acrossRuns` stay open for the next run of this session.
|
|
550
|
+
*/
|
|
551
|
+
closeRunSubscribers() {
|
|
552
|
+
for (const subscriber of this.subscribers)
|
|
553
|
+
if (!subscriber.acrossRuns)
|
|
554
|
+
subscriber.close();
|
|
555
|
+
}
|
|
421
556
|
closeSubscribers() {
|
|
422
557
|
for (const subscriber of this.subscribers)
|
|
423
558
|
subscriber.close();
|
|
@@ -528,10 +663,15 @@ export class RuntimeAgentSession {
|
|
|
528
663
|
signal,
|
|
529
664
|
};
|
|
530
665
|
this.emit({ type: "compaction_started", sessionId: this.id, runId });
|
|
531
|
-
|
|
666
|
+
// Plan 106 R3: pre-compaction seam — the strategy compacts exactly the context this returns.
|
|
667
|
+
const requested = (await this.agent.config.middleware?.run("compaction_request", context)) ?? context;
|
|
668
|
+
let result = await strategy.compact(requested);
|
|
532
669
|
result = { ...result, summary: redactSecrets(result.summary, secrets) };
|
|
533
|
-
const payload = (await this.agent.config.middleware?.run("compaction", {
|
|
534
|
-
context,
|
|
670
|
+
const payload = (await this.agent.config.middleware?.run("compaction", {
|
|
671
|
+
context: requested,
|
|
672
|
+
result,
|
|
673
|
+
})) ?? {
|
|
674
|
+
context: requested,
|
|
535
675
|
result,
|
|
536
676
|
};
|
|
537
677
|
result = { ...payload.result, summary: redactSecrets(payload.result.summary, secrets) };
|
package/dist/context-budget.d.ts
CHANGED
|
@@ -96,3 +96,14 @@ export declare function measureInputCost(options: MeasureInputCostOptions): {
|
|
|
96
96
|
tokens: number;
|
|
97
97
|
bytes: number;
|
|
98
98
|
};
|
|
99
|
+
/** Plan 103 T6: the host's `contextBudget.tokenEstimator`, validated exactly like the budget pass
|
|
100
|
+
* validates it (a non-function, or a non-finite/negative count, fails closed with `TypeError`).
|
|
101
|
+
* `undefined` when no host estimator is configured, so callers can fall through to the built-in
|
|
102
|
+
* heuristic. Exported for the usage seam (`provider-round.ts`) — deliberately not re-exported by
|
|
103
|
+
* `src/index.ts`, so the public surface is unchanged. */
|
|
104
|
+
export declare function resolveHostTokenEstimator(budget: ContextBudget | undefined): TokenEstimator | undefined;
|
|
105
|
+
/** Plan 103 T6: tool declarations and context blocks projected with the assembler's own
|
|
106
|
+
* `measureAll` text shapes, so the usage-fallback estimate and the budget pass cannot drift
|
|
107
|
+
* (never `JSON.stringify` of the raw schemas). Exported for the usage seam — deliberately not
|
|
108
|
+
* re-exported by `src/index.ts`. */
|
|
109
|
+
export declare function estimateRequestExtrasTokens(tools: readonly ToolDefinition[] | undefined, context: readonly ContextBlock[] | undefined, estimateTokens: TokenEstimator): number;
|
package/dist/context-budget.js
CHANGED
|
@@ -246,7 +246,7 @@ function measureAll(groups, context, skills, tools, skillContext, demotedBodies,
|
|
|
246
246
|
for (const message of groups.toolResults)
|
|
247
247
|
addMessage(message);
|
|
248
248
|
for (const block of context) {
|
|
249
|
-
const text =
|
|
249
|
+
const text = contextBlockMeasureText(block);
|
|
250
250
|
tokens += estimateTokens(text);
|
|
251
251
|
bytes += estimateTextBytes(text);
|
|
252
252
|
}
|
|
@@ -256,12 +256,35 @@ function measureAll(groups, context, skills, tools, skillContext, demotedBodies,
|
|
|
256
256
|
bytes += estimateTextBytes(text);
|
|
257
257
|
}
|
|
258
258
|
if (tools?.length) {
|
|
259
|
-
const text =
|
|
259
|
+
const text = toolsMeasureText(tools);
|
|
260
260
|
tokens += estimateTokens(text);
|
|
261
261
|
bytes += estimateTextBytes(text);
|
|
262
262
|
}
|
|
263
263
|
return { tokens, bytes };
|
|
264
264
|
}
|
|
265
|
+
/** Plan 103 T6: the host's `contextBudget.tokenEstimator`, validated exactly like the budget pass
|
|
266
|
+
* validates it (a non-function, or a non-finite/negative count, fails closed with `TypeError`).
|
|
267
|
+
* `undefined` when no host estimator is configured, so callers can fall through to the built-in
|
|
268
|
+
* heuristic. Exported for the usage seam (`provider-round.ts`) — deliberately not re-exported by
|
|
269
|
+
* `src/index.ts`, so the public surface is unchanged. */
|
|
270
|
+
export function resolveHostTokenEstimator(budget) {
|
|
271
|
+
if (budget?.tokenEstimator === undefined)
|
|
272
|
+
return undefined;
|
|
273
|
+
return resolveTokenEstimator(budget);
|
|
274
|
+
}
|
|
275
|
+
/** Plan 103 T6: tool declarations and context blocks projected with the assembler's own
|
|
276
|
+
* `measureAll` text shapes, so the usage-fallback estimate and the budget pass cannot drift
|
|
277
|
+
* (never `JSON.stringify` of the raw schemas). Exported for the usage seam — deliberately not
|
|
278
|
+
* re-exported by `src/index.ts`. */
|
|
279
|
+
export function estimateRequestExtrasTokens(tools, context, estimateTokens) {
|
|
280
|
+
let tokens = 0;
|
|
281
|
+
if (context?.length)
|
|
282
|
+
for (const block of context)
|
|
283
|
+
tokens += estimateTokens(contextBlockMeasureText(block));
|
|
284
|
+
if (tools?.length)
|
|
285
|
+
tokens += estimateTokens(toolsMeasureText(tools));
|
|
286
|
+
return tokens;
|
|
287
|
+
}
|
|
265
288
|
function overBudget(cost, budget) {
|
|
266
289
|
if (budget.maxInputTokens !== undefined && cost.tokens > budget.maxInputTokens)
|
|
267
290
|
return true;
|
|
@@ -342,4 +365,12 @@ function contextBlockText(block) {
|
|
|
342
365
|
})
|
|
343
366
|
.join("\n");
|
|
344
367
|
}
|
|
368
|
+
/** The context block exactly as `measureAll` measures it (plan 103 T6 shares this shape with the usage seam). */
|
|
369
|
+
function contextBlockMeasureText(block) {
|
|
370
|
+
return `${block.title ? `${block.title}:\n` : "Context:\n"}${contextBlockText(block)}`;
|
|
371
|
+
}
|
|
372
|
+
/** The tool list exactly as `measureAll` measures it (plan 103 T6 shares this shape with the usage seam). */
|
|
373
|
+
function toolsMeasureText(tools) {
|
|
374
|
+
return `Available tools:\n${tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ""}`).join("\n")}`;
|
|
375
|
+
}
|
|
345
376
|
//# sourceMappingURL=context-budget.js.map
|
|
@@ -11,7 +11,7 @@ import type { CompactionOptions, RetryOptions } from "./compaction.js";
|
|
|
11
11
|
import type { ContentBlock, ErrorInfo, JsonObject, Message, ModelConfig } from "./content.js";
|
|
12
12
|
import type { ProviderRequestPolicy, SystemPromptConfig } from "./extensions.js";
|
|
13
13
|
import type { GuardrailPackRef } from "./guardrail-packs.js";
|
|
14
|
-
import type { AgentLoopOptions, AgentLoopStrategy } from "./loop.js";
|
|
14
|
+
import type { AgentLoopOptions, AgentLoopStrategy, StopHook } from "./loop.js";
|
|
15
15
|
import type { OwnershipScope } from "./persistence.js";
|
|
16
16
|
import type { AIProvider, ProviderRequestOptions, ProviderResolver } from "./provider.js";
|
|
17
17
|
import type { ResourceLoader } from "./resources.js";
|
|
@@ -86,11 +86,24 @@ export interface AgentConfig {
|
|
|
86
86
|
* Omitted keeps today's request bytes; per-run options may only relax this setting. */
|
|
87
87
|
readonly attentionCompiler?: import("./attention.js").AttentionCompilerSetting;
|
|
88
88
|
/**
|
|
89
|
-
* Missing-usage
|
|
90
|
-
* estimate when a provider turn reports no usage; `"off"` leaves usage absent —
|
|
91
|
-
*
|
|
89
|
+
* Missing-usage handling (plan 091 T2, plan 103 T5): `"fallback"` (default) records a labeled
|
|
90
|
+
* estimate when a provider turn reports no usage; `"off"` leaves usage absent — never zero;
|
|
91
|
+
* `"strict"` refuses a usage-less turn instead, failing the run with `code: "usage_missing"`
|
|
92
|
+
* (`name: "UsageMissingError"`) so a host whose cost gates cannot tolerate approximations never
|
|
93
|
+
* runs on an estimate. A refusal is a harness decision, not a provider failure, so it carries no
|
|
94
|
+
* `failureClass` and is never retried. Estimates are marked `Usage.estimated` and never priced.
|
|
92
95
|
*/
|
|
93
|
-
readonly usageEstimation?: "fallback" | "off";
|
|
96
|
+
readonly usageEstimation?: "fallback" | "off" | "strict";
|
|
97
|
+
/**
|
|
98
|
+
* Session-turn context budget (plan 103 T6): forwarded to every `assembleProviderInput`
|
|
99
|
+
* call this agent's sessions make, so a session gets the same eviction, `tokenEstimator`,
|
|
100
|
+
* and `reportOmissions` semantics as a direct assembler caller. Mutually exclusive with
|
|
101
|
+
* `attentionCompiler` (rejected at assembly). With `usageEstimation: "fallback"`, the
|
|
102
|
+
* missing-usage estimate prefers this budget's own measurement: the request's
|
|
103
|
+
* `ContextBudgetReport.keptTokens` when `reportOmissions` is on, else the `tokenEstimator`
|
|
104
|
+
* projection — see [Runs and usage](../../docs/runs-and-usage.md).
|
|
105
|
+
*/
|
|
106
|
+
readonly contextBudget?: import("../context-budget.js").ContextBudget;
|
|
94
107
|
readonly inputBuilder?: InputBuilder;
|
|
95
108
|
readonly promptBuilder?: PromptBuilder;
|
|
96
109
|
readonly middleware?: MiddlewareRegistry;
|
|
@@ -124,6 +137,8 @@ export interface AgentConfig {
|
|
|
124
137
|
readonly inputLayout?: InputAssemblyLayout;
|
|
125
138
|
readonly loop?: AgentLoopStrategy | AgentLoopOptions;
|
|
126
139
|
readonly guardrails?: Guardrails;
|
|
140
|
+
/** Run-end stop hooks (plan 106 R1); `RunOptions.stopHooks` appends to this list. */
|
|
141
|
+
readonly stopHooks?: readonly StopHook[];
|
|
127
142
|
/** Opt-in durable interruption/checkpointing default for this agent. */
|
|
128
143
|
readonly runState?: AgentRunStateOptions;
|
|
129
144
|
/** Internal marker set by createSecureAgent(); makes security defaults immutable per run. */
|
|
@@ -183,6 +198,12 @@ export interface AgentSessionCloneOptions {
|
|
|
183
198
|
}
|
|
184
199
|
export type SubscriberOverflowPolicy = "close" | "drop_oldest" | "drop_newest";
|
|
185
200
|
export interface SubscribeOptions {
|
|
201
|
+
/**
|
|
202
|
+
* Plan 104 T5: `true` keeps this subscriber open across runs of the same session; it is then
|
|
203
|
+
* closed only by the host (`subscription.close()` / `session.closeSubscribers()`) or by an
|
|
204
|
+
* overflow under the default `close` policy. Default `false` (closed at run end).
|
|
205
|
+
*/
|
|
206
|
+
readonly acrossRuns?: boolean;
|
|
186
207
|
/** Maximum queued events for a subscriber that is not actively awaiting `next()`. Defaults to 1024. */
|
|
187
208
|
readonly maxQueuedEvents?: number;
|
|
188
209
|
/** What to do when `maxQueuedEvents` is reached. Defaults to `close`. */
|
|
@@ -7,6 +7,7 @@ import type { Middleware, MiddlewareHookName, MiddlewareRegistry } from "../midd
|
|
|
7
7
|
import type { AgentDefinition, CommandDefinition, ContextProvider, InputBuilder, InstructionInjector, PromptBuilder, Skill } from "./agent.js";
|
|
8
8
|
import type { CompactionStrategy, RetryPolicy } from "./compaction.js";
|
|
9
9
|
import type { ErrorInfo, ModelConfig } from "./content.js";
|
|
10
|
+
import type { StopHook } from "./loop.js";
|
|
10
11
|
import type { StoreFactory } from "./persistence.js";
|
|
11
12
|
import type { AIProvider, ProviderRequest } from "./provider.js";
|
|
12
13
|
import type { Credential, CredentialResolver, ResourceLoader, SettingsProvider } from "./resources.js";
|
|
@@ -160,4 +161,6 @@ export interface ExtensionAPI {
|
|
|
160
161
|
registerProviderRequestPolicy(policy: ProviderRequestPolicy): void;
|
|
161
162
|
registerSystemPromptContribution(contribution: SystemPromptContribution): void;
|
|
162
163
|
registerInstructionInjector(injector: InstructionInjector): void;
|
|
164
|
+
/** Contributes an inert run-end stop hook; activate it via `activateKernel()` → `AgentConfig.stopHooks` (plan 106 R1). */
|
|
165
|
+
registerStopHook(hook: StopHook): void;
|
|
163
166
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guardrail packs (plan 092 Task 2): config-declared, restrictive-only rule sets compiled once per
|
|
3
3
|
* session onto the existing tool interception seams (`tool_input` / `tool_output`). Packs can only
|
|
4
|
-
* deny or
|
|
4
|
+
* deny, tripwire, or ask for approval — they never grant permissions, widen arguments, or add a stage.
|
|
5
5
|
*/
|
|
6
6
|
import type { JsonObject } from "./content.js";
|
|
7
|
-
export type GuardrailRuleAction = "deny" | "tripwire";
|
|
7
|
+
export type GuardrailRuleAction = "deny" | "tripwire" | "ask";
|
|
8
8
|
/** Read-only identity view handed to a pack rule predicate (never carries a raw argument echo). */
|
|
9
9
|
export interface GuardrailRuleContext {
|
|
10
10
|
readonly toolName: string;
|
|
@@ -25,7 +25,12 @@ export interface GuardrailRule {
|
|
|
25
25
|
readonly argPath?: string | readonly string[];
|
|
26
26
|
/** Typed predicate escape hatch (host-trusted like all host code); deny when it returns true. Exactly one of `pattern` / `deny`. */
|
|
27
27
|
readonly deny?: (args: JsonObject, context: GuardrailRuleContext) => boolean;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Defaults to `deny`. `tripwire` also rejects the enclosing run. `ask` suspends a durable run
|
|
30
|
+
* before the call dispatches (the pending decision names this rule) and blocks the call in a run
|
|
31
|
+
* that cannot suspend; it requires `pattern` — an opaque predicate cannot raise an approval
|
|
32
|
+
* (plan 104 Task 3).
|
|
33
|
+
*/
|
|
29
34
|
readonly action?: GuardrailRuleAction;
|
|
30
35
|
/** Bounded, redacted record reason; defaults to the pack/rule id. */
|
|
31
36
|
readonly reason?: string;
|
|
@@ -46,6 +46,36 @@ export interface TurnPolicyOptions {
|
|
|
46
46
|
*/
|
|
47
47
|
readonly stop?: (context: TurnBoundaryContext) => TurnStopDecision;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Run-end stop-hook contract (plan 106 R1). Stop hooks run at a natural loop end — never after a
|
|
51
|
+
* loop ceiling, a host turn-policy stop, or an artifact failure — and decide whether the run is
|
|
52
|
+
* done. The first `continue` queues `reason` (plus optional `steer`) through the same steer path a
|
|
53
|
+
* host would use and re-enters the loop; `stop` (or no hook continuing) ends the run normally.
|
|
54
|
+
*/
|
|
55
|
+
export interface StopHookContext {
|
|
56
|
+
readonly sessionId: string;
|
|
57
|
+
readonly runId: string;
|
|
58
|
+
/** Provider turns already assembled in this run (resumption continues the run's counter). */
|
|
59
|
+
readonly turn: number;
|
|
60
|
+
/** Live transcript at loop end; hooks read it, never mutate it. */
|
|
61
|
+
readonly history: readonly Message[];
|
|
62
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
63
|
+
readonly signal: AbortSignal;
|
|
64
|
+
/** True on every invocation after the first continuation in this run (Claude Code `stop_hook_active`). */
|
|
65
|
+
readonly stopHookActive: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** `continue` re-enters the loop with `reason` queued as a steer (optional extra `steer` message follows it). */
|
|
68
|
+
export type StopHookDecision = {
|
|
69
|
+
readonly action: "stop";
|
|
70
|
+
} | {
|
|
71
|
+
readonly action: "continue";
|
|
72
|
+
readonly reason: string;
|
|
73
|
+
readonly steer?: string | Message;
|
|
74
|
+
};
|
|
75
|
+
export interface StopHook {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
decide(context: StopHookContext): StopHookDecision | Promise<StopHookDecision>;
|
|
78
|
+
}
|
|
49
79
|
export interface LoopContext {
|
|
50
80
|
readonly sessionId: string;
|
|
51
81
|
readonly runId: string;
|
|
@@ -54,6 +84,12 @@ export interface LoopContext {
|
|
|
54
84
|
readonly history: Message[];
|
|
55
85
|
readonly input: AgentInput;
|
|
56
86
|
readonly inputMessages: readonly Message[];
|
|
87
|
+
/**
|
|
88
|
+
* True when this `run()` call is a stop-hook continuation re-entry (plan 106 R1): `input` and
|
|
89
|
+
* `inputMessages` are empty because the continuation message is already in `history`. Custom
|
|
90
|
+
* strategies must not replay run-start input when this is set.
|
|
91
|
+
*/
|
|
92
|
+
readonly continuation?: boolean;
|
|
57
93
|
readonly maxToolRounds: number;
|
|
58
94
|
/**
|
|
59
95
|
* Why the loop stopped, when a limit/ceiling ends the run cleanly (F4). Strategies set
|