@salesforce/sfdx-agent-sdk 0.24.0 → 0.26.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 CHANGED
@@ -3,6 +3,19 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-sdk` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.26.0] - 2026-06-29
7
+
8
+ ### Features
9
+ - **harness-claude**: consult tool-approval policy resolver at gate sites @W-23090029 ([#624](https://github.com/forcedotcom/agentic-dx/pull/624))
10
+ - **harness-mastra**: consult tool-approval policy resolver at gate sites @W-23090028 ([#622](https://github.com/forcedotcom/agentic-dx/pull/622))
11
+ - **harness-mastra**: stable skill_bridge identity for capability-discovery meta-tools @W-23177289 ([#621](https://github.com/forcedotcom/agentic-dx/pull/621))
12
+ - **agent-sdk**: tool-approval policy resolver, types, and remember persistence @W-23090026 ([#620](https://github.com/forcedotcom/agentic-dx/pull/620))
13
+
14
+ ## [0.25.0] - 2026-06-24
15
+
16
+ ### Chores
17
+ - update dependencies and disable Mastra PostHog telemetry @W-23054815 ([#619](https://github.com/forcedotcom/agentic-dx/pull/619))
18
+
6
19
  ## [0.24.0] - 2026-06-23
7
20
 
8
21
  ### Chores
package/README.md CHANGED
@@ -159,22 +159,22 @@ keeps unparameterized call sites working.
159
159
 
160
160
  A single conversation thread.
161
161
 
162
- | Method | Signature | Description |
163
- | ------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
164
- | `getId` | `() => string` | Session/thread identifier. |
165
- | `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. The returned `eventStream` is the single iterator for the entire chat turn. |
166
- | `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<void>` | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream. |
167
- | `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call. Control message on the existing turn post-resume events flow on the same stream. |
168
- | `declineToolCall` | `(toolCallId: string) => Promise<void>` | Decline a pending tool call. Control message on the existing turn post-resume events flow on the same stream. |
169
- | `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
170
- | `clearHistory` | `() => Promise<void>` | Delete all messages. |
171
- | `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
172
- | `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
173
- | `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
174
- | `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
175
- | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
176
- | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
177
- | `dispose` | `() => void` | Release session-level event resources. Idempotent. |
162
+ | Method | Signature | Description |
163
+ | ------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
164
+ | `getId` | `() => string` | Session/thread identifier. |
165
+ | `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. The returned `eventStream` is the single iterator for the entire chat turn. |
166
+ | `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<void>` | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream. |
167
+ | `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call. `{ remember: true }` ("Allow always") appends an `allow` rule to `AgentConfig.toolPolicies` and persists it before settling. Control message on the existing turn. |
168
+ | `declineToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Decline a pending tool call. `{ remember: true }` ("Deny always") appends a `deny` rule and persists it before settling. Control message on the existing turn. |
169
+ | `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
170
+ | `clearHistory` | `() => Promise<void>` | Delete all messages. |
171
+ | `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
172
+ | `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
173
+ | `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
174
+ | `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
175
+ | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
176
+ | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
177
+ | `dispose` | `() => void` | Release session-level event resources. Idempotent. |
178
178
 
179
179
  ### `ChatStreamResult`
180
180
 
@@ -232,30 +232,97 @@ function onApprovalRequest(event: ToolApprovalRequestEvent): Promise<boolean> {
232
232
 
233
233
  #### `AgentConfig`
234
234
 
235
- | Field | Type | Description |
236
- | --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
237
- | `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
238
- | `modelId?` | `ModelName \| Model` | LLM model selector. Pass a `ModelName` enum value for an in-tree model (e.g. `'llmgateway__OpenAIGPT5'`), or a pre-built `Model` instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see `createClaudeModel(gatewayId, overrides)` exported from this package. |
239
- | `name?` | `string` | Human-readable agent name. |
240
- | `description?` | `string` | Agent purpose description. |
241
- | `instructions?` | `string` | System instructions for the agent. |
242
- | `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
243
- | `mcpServers?` | `MCPConfiguration` | MCP server connections. |
244
- | `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
245
- | `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
235
+ | Field | Type | Description |
236
+ | ---------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
237
+ | `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
238
+ | `modelId?` | `ModelName \| Model` | LLM model selector. Pass a `ModelName` enum value for an in-tree model (e.g. `'llmgateway__OpenAIGPT5'`), or a pre-built `Model` instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see `createClaudeModel(gatewayId, overrides)` exported from this package. |
239
+ | `name?` | `string` | Human-readable agent name. |
240
+ | `description?` | `string` | Agent purpose description. |
241
+ | `instructions?` | `string` | System instructions for the agent. |
242
+ | `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
243
+ | `mcpServers?` | `MCPConfiguration` | MCP server connections. |
244
+ | `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
245
+ | `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
246
+ | `toolPolicies?` | `ToolPolicyRule[]` | Ordered per-tool approval rules resolved by `resolveToolApprovalPolicy` (cross-tier deny-wins / within-tier last-wins). Author directly or via `definePolicy(...)`. See "Tool Approval Policy" below. Has no effect until a harness wires the resolver (Phase 2); until then gating uses the deprecated `StreamOptions.requireToolApproval`. |
247
+ | `defaultToolDecision?` | `Decision` | Fallback decision when no rule matches. Defaults to `'allow'` (no policy ⇒ no gating). Set to `'require-approval'` for a fail-closed posture (recommended for catalogs with un-annotated MCP servers). |
246
248
 
247
249
  #### `StreamOptions`
248
250
 
249
- | Field | Type | Description |
250
- | ---------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
251
- | `abortSignal?` | `AbortSignal` | Abort the streaming operation. |
252
- | `requireToolApproval?` | `boolean \| ToolApprovalMode` | Gates native tool execution behind a `tool-approval-request` event. `true` / `'serial'` (the default) emits one approval per stream — safe for any iterator pattern. `'batch'` opts into parallel-approval UX: when the model emits parallel `tool_use` blocks, all approvals surface on the same stream so the consumer can render a batch approval card. **`'batch'` requires Pattern A iterators** (collect-all-approvals-then-settle); a `break`-on-first-approval loop will hang. See "Tool Approval Flow" below. |
253
- | `maxSteps?` | `number` | Maximum number of LLM call steps the agent may take per `stream()` invocation. Each step is one LLM call (which may produce text, tool calls, or both). Must be `>= 1`. Defaults to `DEFAULT_MAX_STEPS` (1024) high enough to be effectively unlimited for real tasks; the practical ceiling is the context window and cost. The constant is exported so consumers and harness authors share one source of truth. |
251
+ | Field | Type | Description |
252
+ | ---------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
+ | `abortSignal?` | `AbortSignal` | Abort the streaming operation. |
254
+ | `requireToolApproval?` | `boolean \| ToolApprovalMode` | **Deprecated.** Per-call all-or-nothing gating. Superseded by per-tool policy on `AgentConfig.toolPolicies`; the serial-vs-batch UX axis moved to `batchApprovals`. Kept functional for one release so harness PRs migrate independently. `true` / `'serial'` emits one approval per stream; `'batch'` surfaces parallel approvals on one stream (requires Pattern A iterators). See "Tool Approval Flow" below. |
255
+ | `batchApprovals?` | `boolean` | When `true`, parallel approval-requests within a turn surface on the same stream so the consumer can render a batch approval card (requires Pattern A iterators collect-all-then-settle). Defaults to `false` (serial). The self-documenting replacement for the UX half of the deprecated `requireToolApproval` enum; the gating half moves to `AgentConfig.toolPolicies`. No effect when no tool in the turn resolves to `'require-approval'`. |
256
+ | `maxSteps?` | `number` | Maximum number of LLM call steps the agent may take per `stream()` invocation. Each step is one LLM call (which may produce text, tool calls, or both). Must be `>= 1`. Defaults to `DEFAULT_MAX_STEPS` (1024) — high enough to be effectively unlimited for real tasks; the practical ceiling is the context window and cost. The constant is exported so consumers and harness authors share one source of truth. |
254
257
 
255
- `ToolApprovalMode` is the exported type alias `'serial' | 'batch'` useful when typing a settings struct that drives
256
- `requireToolApproval`. Pair with `resolveToolApprovalMode(boolean | ToolApprovalMode | undefined)` (also exported) to
257
- normalize consumer input the same way the SDK does internally (`undefined` / `false` → `undefined`, `true` → `'serial'`,
258
- strings pass through, unknown strings throw).
258
+ `ToolApprovalMode` is the exported (deprecated) type alias `'serial' | 'batch'`. Pair with the (deprecated)
259
+ `resolveToolApprovalMode(boolean | ToolApprovalMode | undefined)` to normalize consumer input the same way the SDK does
260
+ internally (`undefined` / `false` → `undefined`, `true` → `'serial'`, strings pass through, unknown strings throw). New
261
+ code should configure `AgentConfig.toolPolicies` and set `batchApprovals` for the UX axis instead.
262
+
263
+ #### Tool Approval Policy
264
+
265
+ Per-tool approval is configured on the agent, not per `chat()` call. `AgentConfig.toolPolicies` is an ordered list of
266
+ `ToolPolicyRule`s; for each harness-executed tool call the harness consults `resolveToolApprovalPolicy(...)`, a pure
267
+ function exported from this package.
268
+
269
+ ```typescript
270
+ export type Decision = 'allow' | 'deny' | 'require-approval';
271
+
272
+ export type ToolMatcher =
273
+ | { type: 'builtin'; name: string } // harness built-in (e.g. Claude `Bash`)
274
+ | { type: 'mcp'; serverName?: string; toolName?: string } // MCP tool(s); omitted fields are wildcards
275
+ | { type: 'mcp-annotation'; readOnlyHint?: boolean; destructiveHint?: boolean }; // by discovered annotation
276
+
277
+ export type ToolPolicyRule = {
278
+ matcher: ToolMatcher;
279
+ decision: Decision;
280
+ source?: 'built-in' | 'agent-config' | 'remember'; // advisory provenance; ignored by the resolver
281
+ };
282
+ ```
283
+
284
+ **Resolution — cross-tier deny-wins / within-tier last-wins.** The resolver concatenates four tiers in precedence order
285
+ — `[...BUILT_IN_TOOL_POLICIES, ...harness, ...factory, ...AgentConfig.toolPolicies]` — then: any matching `'deny'` in
286
+ any tier wins; otherwise the last matching non-deny rule decides; if nothing matched, the result is
287
+ `AgentConfig.defaultToolDecision ?? 'allow'`. So a later rule overrides an earlier one of the same matcher (a consumer
288
+ rule beats a built-in), but no consumer rule can override a `deny`.
289
+
290
+ **Built-in rules.** `BUILT_IN_TOOL_POLICIES` (exported, frozen) ships cross-harness rules only: MCP-annotation defaults
291
+ (`destructiveHint ⇒ require-approval`, `readOnlyHint ⇒ allow`) and the `skill_bridge` capability-discovery meta-tools
292
+ (anchored on the exported `SKILL_BRIDGE_SERVER_ID`). Harness-specific built-ins (Claude's `Bash`, Mastra's
293
+ `updateWorkingMemory`) live in the harness packages' `<HARNESS>_BUILT_IN_TOOL_POLICIES` arrays, not here.
294
+
295
+ **`definePolicy` helper.** Compresses the common cases into a `ToolPolicyRule[]` (`source: 'agent-config'`):
296
+
297
+ ```typescript
298
+ import { definePolicy } from '@salesforce/sfdx-agent-sdk';
299
+
300
+ const config: AgentConfig = {
301
+ defaultToolDecision: 'require-approval', // fail-closed
302
+ toolPolicies: definePolicy({
303
+ Bash: 'deny', // builtin matcher
304
+ 'mcp:sfdx': 'require-approval', // every tool from the 'sfdx' MCP server
305
+ 'mcp:sfdx/list_orgs': 'allow', // one tool from the 'sfdx' server
306
+ }),
307
+ };
308
+ ```
309
+
310
+ `definePolicy` covers `builtin` and `mcp:server[/tool]` keys only — author the structured `ToolPolicyRule` form for
311
+ `mcp-annotation` matchers, the bare `{ type: 'mcp' }` wildcard, or tool names containing `/`.
312
+
313
+ **AG-UI button mapping.** The settle methods grow a symmetric, honored `remember` flag:
314
+
315
+ | AG-UI button | SDK call | Effect |
316
+ | ------------ | ----------------------------------------- | -------------------------------------------------------- |
317
+ | Allow | `approveToolCall(id)` | Settle only. |
318
+ | Allow always | `approveToolCall(id, { remember: true })` | Append an `allow` `remember` rule, persist, then settle. |
319
+ | Deny | `declineToolCall(id)` | Settle only. |
320
+ | Deny always | `declineToolCall(id, { remember: true })` | Append a `deny` `remember` rule, persist, then settle. |
321
+
322
+ With `{ remember: true }` the SDK derives the matcher from the pending `tool-approval-request`'s
323
+ `(toolName, serverName?)`, appends a `source: 'remember'` rule to `AgentConfig.toolPolicies`, and persists it via
324
+ `updateAgentConfig` **before** settling — so the decision survives a restart and a persistence failure surfaces as the
325
+ settle's rejection. A `remember` settle for a `toolCallId` with no pending approval throws `TOOL_CALL_NOT_FOUND`.
259
326
 
260
327
  #### `MCPConfiguration`
261
328
 
package/dist/agent.js CHANGED
@@ -332,10 +332,19 @@ export class DefaultAgent {
332
332
  // `contextWindow`; #507's decoupling work must preserve that, so this
333
333
  // access is contractually safe.
334
334
  const getContextWindow = () => this.modelConnectivityInfo.model.contextWindow;
335
+ // Persists a `'remember'` policy rule for an `approveToolCall` /
336
+ // `declineToolCall` settle with `{ remember: true }`. Reads `this.config`
337
+ // live (so concurrent sessions and prior remembers compound correctly)
338
+ // and forwards a `toolPolicies`-only partial — `updateAgentConfig` skips
339
+ // connectivity re-resolution when neither `orgAlias` nor `modelId` is
340
+ // present, so an "Allow always" click never re-mints the org JWT.
341
+ const persistRememberedRule = async (rule) => {
342
+ await this.updateAgentConfig({ toolPolicies: [...(this.config.toolPolicies ?? []), rule] });
343
+ };
335
344
  const session = new DefaultChatSession(this.harness, this.agentId, threadId, slice, {
336
345
  telemetry: this.telemetryBus,
337
346
  log: this.logBus,
338
- }, getContextWindow, this.clock, this.idGenerator);
347
+ }, getContextWindow, { clock: this.clock, idGenerator: this.idGenerator, persistRememberedRule });
339
348
  this.sessions.set(threadId, session);
340
349
  this.sessionSliceUnregisters.set(threadId, () => this.router.unregisterSession(threadId));
341
350
  this.telemetryBus.emit({
@@ -5,12 +5,42 @@ import type { TelemetrySlice } from './internal/telemetry-router.js';
5
5
  import type { ChatEvent, ChatStreamResult } from './types/events.js';
6
6
  import type { Message, MessagePart } from './types/messages.js';
7
7
  import type { TelemetryBus, TelemetryEventCallback } from './types/telemetry-events.js';
8
- import type { ToolResultInfo } from './types/tools.js';
8
+ import type { ToolPolicyRule, ToolResultInfo } from './types/tools.js';
9
9
  import type { ContextUsage } from './types/usage.js';
10
10
  /**
11
11
  * Options for a single chat interaction.
12
12
  */
13
13
  export type ChatOptions = StreamOptions;
14
+ /**
15
+ * Persists a `'remember'` tool-policy rule appended by a settle call with
16
+ * `{ remember: true }`. Injected by {@link DefaultAgent} so the session can
17
+ * write through to the agent's config (and the SDK-owned identity store)
18
+ * without holding an `Agent` reference. The implementation reads the agent's
19
+ * current `toolPolicies`, appends `rule`, and calls `updateAgentConfig` with a
20
+ * `toolPolicies`-only partial so no connectivity re-resolution is triggered.
21
+ */
22
+ export type RememberedRulePersister = (rule: ToolPolicyRule) => Promise<void>;
23
+ /**
24
+ * Optional injected dependencies for a {@link DefaultChatSession}. Bundled into
25
+ * one trailing bag (rather than a growing tail of positional params) so a new
26
+ * dependency is a one-field addition here plus a one-line spread at the
27
+ * construction site, not a positional append every caller must match.
28
+ *
29
+ * Every field is optional with a production default resolved in the
30
+ * constructor — tests override only what they need.
31
+ */
32
+ export type ChatSessionDeps = {
33
+ /** Source of monotonic timestamps for telemetry events. Defaults to `RealClock`. */
34
+ clock?: Clock;
35
+ /** Source of message ids for `addContext()`. Defaults to `UUIDGenerator`. */
36
+ idGenerator?: UniqueIDGenerator;
37
+ /**
38
+ * Persists a `'remember'` policy rule for a settle call with `{ remember: true }`.
39
+ * Supplied by `DefaultAgent`; omitted in unit tests, where a `remember: true` settle
40
+ * then behaves as a one-shot with no write. See {@link RememberedRulePersister}.
41
+ */
42
+ persistRememberedRule?: RememberedRulePersister;
43
+ };
14
44
  /**
15
45
  * Parent bus pair used to wire upward forwarding at construction time.
16
46
  */
@@ -99,10 +129,14 @@ export interface ChatSession {
99
129
  *
100
130
  * @param toolCallId - ID of the pending tool call to approve.
101
131
  * @param options - Optional approval metadata.
102
- * @param options.remember - When `true`, signals that the consumer wants to
103
- * persist this approval decision (e.g., "always allow this tool").
104
- * The harness does not implement the persistence — the consumer manages
105
- * its own permission cache. Borrowed from OpenCode's `once/always/reject` model.
132
+ * @param options.remember - When `true` ("Allow always"), the SDK appends a
133
+ * `{ decision: 'allow', source: 'remember' }` rule to the agent's
134
+ * `toolPolicies` (matching this tool) and persists it via
135
+ * `updateAgentConfig` **before** settling the approval, so the decision
136
+ * survives a restart. Throws `TOOL_CALL_NOT_FOUND` if `toolCallId` does
137
+ * not match a pending `tool-approval-request` (e.g. a consumer-executed
138
+ * tool, which is never gated). When `false`/omitted, the approval is a
139
+ * one-shot settle with no persistence.
106
140
  */
107
141
  approveToolCall(toolCallId: string, options?: {
108
142
  remember?: boolean;
@@ -118,8 +152,17 @@ export interface ChatSession {
118
152
  * `ErrorEvent` + `FinishEvent` on the chat stream before the rejection.
119
153
  *
120
154
  * @param toolCallId - ID of the pending tool call to decline.
155
+ * @param options - Optional decline metadata.
156
+ * @param options.remember - When `true` ("Deny always"), the SDK appends a
157
+ * `{ decision: 'deny', source: 'remember' }` rule to the agent's
158
+ * `toolPolicies` (matching this tool) and persists it via
159
+ * `updateAgentConfig` **before** settling the decline. Symmetric with
160
+ * {@link approveToolCall}'s `remember`. Throws `TOOL_CALL_NOT_FOUND` if
161
+ * `toolCallId` does not match a pending `tool-approval-request`.
121
162
  */
122
- declineToolCall(toolCallId: string): Promise<void>;
163
+ declineToolCall(toolCallId: string, options?: {
164
+ remember?: boolean;
165
+ }): Promise<void>;
123
166
  /**
124
167
  * Retrieve message history for this session.
125
168
  *
@@ -200,6 +243,16 @@ export declare class DefaultChatSession implements ChatSession {
200
243
  * are stale and should not bleed into the next turn).
201
244
  */
202
245
  private readonly toolStartMs;
246
+ /**
247
+ * Tracks the `(toolName, serverName?)` of every tool call currently awaiting
248
+ * approval, keyed by `toolCallId`. Populated when a `tool-approval-request`
249
+ * ChatEvent flows through {@link wrapEventStream}; read by
250
+ * {@link approveToolCall} / {@link declineToolCall} when `remember: true` so
251
+ * the appended policy rule carries the right matcher. An entry is removed
252
+ * once its approval settles (or the turn ends), so a `remember` settle for an
253
+ * unknown / already-settled `toolCallId` throws `TOOL_CALL_NOT_FOUND`.
254
+ */
255
+ private readonly pendingApprovalsByToolCallId;
203
256
  /**
204
257
  * Live getter for the agent's currently-bound model's context window.
205
258
  * Called by {@link getContextUsage} so reads reflect the model in
@@ -207,6 +260,14 @@ export declare class DefaultChatSession implements ChatSession {
207
260
  * (an `Agent.updateAgentConfig()` swap can change it mid-life).
208
261
  */
209
262
  private readonly getContextWindow;
263
+ /**
264
+ * Persists a `'remember'` policy rule on behalf of a settle call with
265
+ * `{ remember: true }`. Injected by {@link DefaultAgent}; `undefined` only
266
+ * in unit tests that construct a session without the persister, in which
267
+ * case a `remember: true` settle is treated as a one-shot (no write). See
268
+ * {@link RememberedRulePersister}.
269
+ */
270
+ private readonly persistRememberedRule;
210
271
  /**
211
272
  * Last per-step usage reading observed on this session. Initialized
212
273
  * to `{}` (every token field undefined) so {@link getContextUsage}
@@ -226,10 +287,10 @@ export declare class DefaultChatSession implements ChatSession {
226
287
  * @param parent - Parent agent's buses; this session forwards its events upward into them.
227
288
  * @param getContextWindow - Live getter for the agent's currently-bound model's `contextWindow`.
228
289
  * Called by `getContextUsage()` so reads stay correct across `Agent.updateAgentConfig()` model swaps.
229
- * @param clock - Source of monotonic timestamps for telemetry events. Defaults to `RealClock`.
230
- * @param idGenerator - Source of message ids for `addContext()`. Defaults to `UUIDGenerator`.
290
+ * @param deps - Optional injected dependencies ({@link ChatSessionDeps}): `clock`, `idGenerator`,
291
+ * `persistRememberedRule`. Each has a production default; tests override only what they need.
231
292
  */
232
- constructor(harness: AgentHarness, agentId: string, threadId: string, inbound: TelemetrySlice, parent: ChatSessionParentBuses, getContextWindow: () => number, clock?: Clock, idGenerator?: UniqueIDGenerator);
293
+ constructor(harness: AgentHarness, agentId: string, threadId: string, inbound: TelemetrySlice, parent: ChatSessionParentBuses, getContextWindow: () => number, deps?: ChatSessionDeps);
233
294
  getId(): string;
234
295
  /**
235
296
  * @requirements
@@ -290,9 +351,12 @@ export declare class DefaultChatSession implements ChatSession {
290
351
  * `tool-approval-requested` by `toolCallId` and observe the failure on the chat-stream contract.
291
352
  * - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
292
353
  * before returning a stream result.
293
- * - The `options.remember` flag is consumer-only metadata the harness does not use it.
354
+ * - WHEN `options.remember` is `true`, MUST append an `'allow'` `'remember'` rule to the agent's
355
+ * `toolPolicies` and persist it via the injected persister BEFORE delegating the settle to the
356
+ * harness, so a persistence failure surfaces as the settle's rejection and the decision is durable
357
+ * before the tool runs. Throws `TOOL_CALL_NOT_FOUND` if `toolCallId` has no pending approval.
294
358
  */
295
- approveToolCall(toolCallId: string, _options?: {
359
+ approveToolCall(toolCallId: string, options?: {
296
360
  remember?: boolean;
297
361
  }): Promise<void>;
298
362
  /**
@@ -305,8 +369,12 @@ export declare class DefaultChatSession implements ChatSession {
305
369
  * and intentionally skip approval-resolved emission.
306
370
  * - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
307
371
  * before returning a stream result.
372
+ * - WHEN `options.remember` is `true`, MUST append a `'deny'` `'remember'` rule and persist it
373
+ * BEFORE delegating the settle to the harness — symmetric with {@link approveToolCall}.
308
374
  */
309
- declineToolCall(toolCallId: string): Promise<void>;
375
+ declineToolCall(toolCallId: string, options?: {
376
+ remember?: boolean;
377
+ }): Promise<void>;
310
378
  /**
311
379
  * @requirements
312
380
  * - MUST delegate to `this.harness.getMessages()`, passing `this.agentId` and `this.threadId`.
@@ -364,6 +432,16 @@ export declare class DefaultChatSession implements ChatSession {
364
432
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
365
433
  dispose(): void;
366
434
  private emitToolApprovalResolved;
435
+ /**
436
+ * Clears the per-turn tracking maps at a terminal `finish`. Both maps are
437
+ * scoped to one logical chat turn: `toolStartMs` pairs `tool-call` with
438
+ * `tool-result` for `tool-execution-completed.durationMs`, and
439
+ * `pendingApprovalsByToolCallId` lets a `remember` settle build the right
440
+ * matcher. A stale entry surviving into the next turn would mispair a
441
+ * duration or remember the wrong tool, so the two clears must always fire
442
+ * together — hence one helper rather than two call sites.
443
+ */
444
+ private clearPerTurnTracking;
367
445
  /**
368
446
  * Derives `tool-execution-*` and `tool-approval-requested` telemetry from `ChatEvent`s as
369
447
  * they pass through the stream wrapper. Centralizing the derivation here keeps every harness
@@ -379,6 +457,20 @@ export declare class DefaultChatSession implements ChatSession {
379
457
  * `tool-execution-started`.
380
458
  */
381
459
  private deriveToolTelemetry;
460
+ /**
461
+ * If `remember` is requested, builds the matcher for the pending approval
462
+ * referenced by `toolCallId` and persists a `'remember'` rule with the
463
+ * given `decision` via the injected {@link RememberedRulePersister}, BEFORE
464
+ * the caller settles with the harness. Returns whether a rule was written.
465
+ *
466
+ * - A `remember` settle for a `toolCallId` with no pending approval throws
467
+ * `TOOL_CALL_NOT_FOUND` — the same outcome as settling an unknown id, and
468
+ * the correct outcome for a consumer-executed tool (never gated, so never
469
+ * in the pending map).
470
+ * - When no persister was injected (unit-test construction), `remember`
471
+ * degrades to a one-shot settle: no write, returns `false`.
472
+ */
473
+ private maybePersistRememberedRule;
382
474
  /**
383
475
  * Emits a `chat-stream-started` telemetry event and returns the `startedAt` timestamp the
384
476
  * caller threads through to the stream wrapper / pre-stream error notifier so terminal