@salesforce/sfdx-agent-harness-claude 0.10.0 → 0.12.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.
Files changed (40) hide show
  1. package/README.md +137 -37
  2. package/dist/claude-approval-coordinator.d.ts +205 -0
  3. package/dist/claude-approval-coordinator.js +533 -0
  4. package/dist/claude-approval-coordinator.js.map +1 -0
  5. package/dist/claude-consumer-tool-bridge.js +1 -1
  6. package/dist/claude-event-adapter.js +16 -4
  7. package/dist/claude-event-adapter.js.map +1 -1
  8. package/dist/claude-harness-factory.d.ts +15 -0
  9. package/dist/claude-harness-factory.js +6 -1
  10. package/dist/claude-harness-factory.js.map +1 -1
  11. package/dist/claude-harness.d.ts +40 -32
  12. package/dist/claude-harness.js +296 -196
  13. package/dist/claude-harness.js.map +1 -1
  14. package/dist/claude-query-options-builder.d.ts +2 -1
  15. package/dist/claude-query-options-builder.js +69 -2
  16. package/dist/claude-query-options-builder.js.map +1 -1
  17. package/dist/disk-backed-session-store.d.ts +25 -0
  18. package/dist/disk-backed-session-store.js +88 -0
  19. package/dist/disk-backed-session-store.js.map +1 -1
  20. package/dist/gen-sink.d.ts +6 -19
  21. package/dist/gen-sink.js +6 -47
  22. package/dist/gen-sink.js.map +1 -1
  23. package/dist/mcp-client-factory.js +20 -0
  24. package/dist/mcp-client-factory.js.map +1 -1
  25. package/dist/salesforce-bedrock-transport-resolver.d.ts +19 -12
  26. package/dist/salesforce-bedrock-transport-resolver.js +4 -3
  27. package/dist/salesforce-bedrock-transport-resolver.js.map +1 -1
  28. package/dist/test/tsconfig.tsbuildinfo +1 -1
  29. package/dist/text-stream.d.ts +1 -1
  30. package/dist/text-stream.js +1 -1
  31. package/dist/thread-context-store.d.ts +14 -0
  32. package/dist/thread-context-store.js +19 -2
  33. package/dist/thread-context-store.js.map +1 -1
  34. package/package.json +5 -5
  35. package/dist/claude-stream-broker.d.ts +0 -202
  36. package/dist/claude-stream-broker.js +0 -489
  37. package/dist/claude-stream-broker.js.map +0 -1
  38. package/dist/consumer-tool-broker.d.ts +0 -119
  39. package/dist/consumer-tool-broker.js +0 -275
  40. package/dist/consumer-tool-broker.js.map +0 -1
package/README.md CHANGED
@@ -34,7 +34,7 @@ internal architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
34
34
  | Export | Kind | Description |
35
35
  | ------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
36
  | `ClaudeHarnessFactory` | class | `HarnessFactory` that produces a Claude-backed `AgentHarness` bound to a storage root. |
37
- | `ClaudeHarnessFactoryConfig` | interface | Configuration for `ClaudeHarnessFactory` (`resolver`, `permissionMode`, optional `queryDefaults`, `toolApprovalTimeoutMs`). |
37
+ | `ClaudeHarnessFactoryConfig` | interface | Configuration for `ClaudeHarnessFactory` (`resolver`, `permissionMode`, optional `queryDefaults`, `toolApprovalTimeoutMs`, `bypassApprovalTools`). |
38
38
  | `ClaudeHarnessPermissionMode` | type | Narrowed permission mode: `'bypassPermissions' \| 'acceptEdits' \| 'plan'`. |
39
39
  | `ClaudeTransportResolver` | interface | Strategy for resolving transport env, auth, and native model ID for the subprocess. |
40
40
  | `ClaudeTransportResolverResult` | type | Return type of `ClaudeTransportResolver.resolve()` (`env` + `nativeModelId`). |
@@ -68,22 +68,24 @@ mirror the gateway SDK's conservative limits so a file behaves identically here
68
68
 
69
69
  ### Tool approval
70
70
 
71
- When `StreamOptions.requireToolApproval: true` is set on `chatSession.send()`/`stream()`, the harness installs a
72
- `PreToolUse` hook that:
71
+ When `StreamOptions.requireToolApproval: true` is set on `chatSession.chat()`, the harness installs a `PreToolUse` hook
72
+ that:
73
73
 
74
- 1. Emits a `tool-approval-request` event on `ChatStreamResult.eventStream` carrying
74
+ 1. Emits a `tool-approval-request` event on the `chat()`-returned `eventStream` carrying
75
75
  `{ toolCall: { toolCallId, toolName, args } }`.
76
- 2. Suspends the stream (the `eventStream` ends after the request event) until the consumer calls
77
- `chatSession.approveToolCall(toolCallId)` or `chatSession.declineToolCall(toolCallId)`.
78
- 3. Returns a new `ChatStreamResult` whose stream carries the tool's outcome and the model's follow-up:
79
- - On **approve**: the resulting `tool-result`, then the model's follow-up `text-delta` events, then `finish`.
80
- - On **decline**: a synthetic `tool-result` with `isError: true`, then the model's acknowledgement `text-delta`
81
- events, then `finish`. The decline does **not** abort the assistant turn — the model receives the denial and can
82
- react. The decline message is hard-coded to `'User declined the tool call.'`; there is no consumer-supplied reason
83
- (matching the SDK's `declineToolCall` signature).
84
-
85
- Single-tool approve and decline, parallel batches (with per-tool sink routing and progressive disclosure), approval
86
- timeout enforcement, `abortSignal` propagation, and shutdown/`destroyThread` cleanup are supported today.
76
+ 2. Parks the SDK hook Promise until the consumer calls `chatSession.approveToolCall(toolCallId)` or
77
+ `chatSession.declineToolCall(toolCallId)`. Settle methods return `Promise<void>`.
78
+ 3. Continues delivering events on the **same** `eventStream` after the consumer settles — the resulting `tool-result`,
79
+ the model's follow-up `text-delta` events, and the terminal `finish` all arrive on that one stream. The consumer
80
+ keeps iterating until it sees `finish`.
81
+ - On **approve**: the SDK's `tool_use` runs and the `tool-result` lands on the stream.
82
+ - On **decline**: the SDK synthesizes a `tool_result` with `isError: true`, the model receives the denial and emits
83
+ an acknowledgement, then `finish`. The decline does **not** abort the assistant turn. The decline message is
84
+ hard-coded to `'User declined the tool call.'`.
85
+
86
+ This is the post-#529 single-stream-per-turn contract — see the SDK README's "Tool Approval Flow" for consumer-facing
87
+ patterns. Single-tool approve / decline, parallel batches (`requireToolApproval: 'batch'`), approval-timeout
88
+ enforcement, `abortSignal` propagation, and shutdown / `destroyThread` cleanup all share that contract.
87
89
 
88
90
  #### Abort signal
89
91
 
@@ -91,8 +93,7 @@ When `StreamOptions.abortSignal` fires during an active turn with `requireToolAp
91
93
 
92
94
  - All pending permission Promises reject with an `AbortError`.
93
95
  - The underlying Claude subprocess is killed (existing `AbortController` wiring).
94
- - Every open per-turn stream — the initial sink and any per-tool continuation sinks for already-approved tools whose
95
- `tool-result` hasn't arrived yet — ends with an `error` event (`code: 'aborted'`, `error.name === 'AbortError'`)
96
+ - The single per-turn `eventStream` ends with an `error` event (`code: 'aborted'`, `error.name === 'AbortError'`)
96
97
  followed by a `finish` event (`finishReason: 'error'`). The `'error'` finish reason is used because the SDK's
97
98
  `FinishReason` type has no `'aborted'` value; the preceding `error` event carries the abort identity.
98
99
 
@@ -101,12 +102,12 @@ If the signal is already aborted when `stream()` is called, `stream()` throws sy
101
102
 
102
103
  #### Shutdown and thread teardown
103
104
 
104
- Calling `harness.shutdown()` disposes every tracked broker (across all agents and threads). For each pending approval,
105
- the parked Promise rejects with an `AgentSDKError` whose `type` is `'DISPOSED'`; each open per-turn stream ends with a
106
- single `error` event (no `finish`) and the underlying Claude subprocess is killed. Calling
107
- `destroyThread(agentId, threadId)` performs the same cleanup but only for the broker associated with that
108
- `(agentId, threadId)` pair — brokers on other threads are untouched. Both entry points are idempotent (the harness's
109
- `shuttingDown` guard prevents double-dispose; the broker's `disposed` flag does the same).
105
+ Calling `harness.shutdown()` disposes every tracked coordinator (across all agents and threads). For each pending
106
+ approval, the parked Promise rejects with an `AgentSDKError` whose `type` is `'DISPOSED'`; the per-turn `eventStream`
107
+ ends with a single `error` event (no `finish`) and the underlying Claude subprocess is killed. Calling
108
+ `destroyThread(agentId, threadId)` performs the same cleanup but only for the coordinator associated with that
109
+ `(agentId, threadId)` pair — coordinators on other threads are untouched. Both entry points are idempotent (the
110
+ harness's `shuttingDown` guard prevents double-dispose; the coordinator's `disposed` flag does the same).
110
111
 
111
112
  #### Approval timeout
112
113
 
@@ -115,21 +116,18 @@ Each pending approval is auto-denied after `ClaudeHarnessFactoryConfig.toolAppro
115
116
 
116
117
  - The parked `PreToolUse` hook Promise resolves with `permissionDecision: 'deny'` plus `decision: 'block'` so the
117
118
  agentic loop terminates instead of letting the model retry the timed-out call.
118
- - An `error` event with `code: 'tool-approval-timeout'` is emitted on the consumer's currently-active per-turn stream,
119
+ - An `error` event with `code: 'tool-approval-timeout'` is emitted on the consumer's currently-iterating `eventStream`,
119
120
  then that stream ends.
120
- - Any subsequent `approveToolCall` / `declineToolCall` for the timed-out tool throws because the pending approval is no
121
- longer registered.
121
+ - Any subsequent `approveToolCall` / `declineToolCall` for the timed-out tool throws because the coordinator marks every
122
+ emitted approval as auto-resolved during teardown.
122
123
 
123
124
  Calling `approveToolCall` or `declineToolCall` before the timeout cancels the timer — no spurious error event surfaces
124
125
  afterwards.
125
126
 
126
- > **Consumer pattern:** to observe the timeout (or `abort` / `dispose`) error on the initial-turn `eventStream`, iterate
127
- > the stream to its natural end rather than `break`ing out after the `tool-approval-request` event. Calling
128
- > `iterator.return()` (which `break` does inside `for await`) finalizes the harness's source generator before the
129
- > terminal event can reach you. Iterate with side-effect
130
- > (`if (event.type === 'tool-approval-request') approval = event`) and let the loop exit when the stream ends. Consumers
131
- > that hand off to a continuation sink via `approveToolCall` / `declineToolCall` can use either pattern — terminal
132
- > events on the continuation flow normally.
127
+ > **Consumer pattern:** to observe the timeout (or `abort` / `dispose`) error on the `eventStream`, iterate the stream
128
+ > to its natural end rather than `break`ing out after the `tool-approval-request` event. Calling `iterator.return()`
129
+ > (which `break` does inside `for await`) finalizes the harness's source generator before the terminal event can reach
130
+ > you. Settle in-line and keep iterating; the loop exits naturally when the terminal `finish` arrives.
133
131
 
134
132
  ### Using the Salesforce Bedrock resolver
135
133
 
@@ -144,10 +142,12 @@ const harnessFactory = new ClaudeHarnessFactory({
144
142
  resolver: new SalesforceBedrockClaudeTransportResolver({
145
143
  // Pick the gateway URL for your environment — e.g. test:
146
144
  gatewayBaseUrl: 'https://test.api.salesforce.com/ai/gpt/v1',
147
- // Optional: gateway model id → native Bedrock id translation.
148
- modelIdMap: {
149
- llmgateway__BedrockAnthropicClaude46Sonnet: 'anthropic.claude-sonnet-4-6',
150
- },
145
+ // No `modelIdMap` needed in the common case: the LLM Gateway's invoke API
146
+ // accepts the gateway-prefixed canonical id (`llmgateway__Bedrock*`) for
147
+ // every Anthropic Claude model, and the SDK's `Model.name` is already that
148
+ // canonical id, so the resolver's default passthrough is correct. Set the
149
+ // map only if your consumer needs to translate to a different shape (e.g.
150
+ // a self-hosted Bedrock proxy that expects the bare-native ids).
151
151
  // Optional: override the `x-client-feature-id` header. Defaults to `orgJwt.getFeatureId()`.
152
152
  featureId: 'EinsteinGptForDevelopers',
153
153
  // Optional: getter for the `x-aip-user-ctx-tag` header. Invoked on every `resolve()`;
@@ -223,6 +223,106 @@ const harnessFactory = new ClaudeHarnessFactory({
223
223
  });
224
224
  ```
225
225
 
226
+ ### Suppressing Claude built-ins for cross-harness consumer tools
227
+
228
+ Claude's subprocess ships built-in tools the model can invoke without the host wiring them up — `AskUserQuestion`,
229
+ `Bash`, `Read`, `WebSearch`, etc. When a consumer registers an equivalent **cross-harness** tool via `AgentConfig.tools`
230
+ (which works on both Mastra and Claude), the model can wind up choosing the Claude built-in over the consumer tool,
231
+ breaking parity with Mastra. Pass `disallowedTools` through `queryDefaults` to keep the model's choice deterministic
232
+ across harnesses.
233
+
234
+ `disallowedTools` flows through to the Claude SDK verbatim (it's a field on the SDK's `Options` shape that
235
+ `ClaudeQueryDefaults` does not strip). It blocks the model from emitting `tool_use` for the named built-ins entirely.
236
+
237
+ ```ts
238
+ import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
239
+ import { ClaudeHarnessFactory, SalesforceBedrockClaudeTransportResolver } from '@salesforce/sfdx-agent-harness-claude';
240
+
241
+ const harnessFactory = new ClaudeHarnessFactory({
242
+ resolver: new SalesforceBedrockClaudeTransportResolver({ gatewayBaseUrl: '...' }),
243
+ permissionMode: 'bypassPermissions',
244
+ // Suppress the built-in `AskUserQuestion` so the cross-harness consumer
245
+ // tool below is the only ask-the-user surface the model sees on Claude
246
+ // (matching what Mastra consumers experience by default).
247
+ queryDefaults: {
248
+ disallowedTools: ['AskUserQuestion'],
249
+ },
250
+ });
251
+
252
+ const manager = await createAgentManager('/path/to/storage', harnessFactory);
253
+ const agent = await manager.createAgent(projectRoot, {
254
+ instructions: '...',
255
+ tools: [
256
+ {
257
+ name: 'ask_user_choice',
258
+ description: 'Ask the human user a multiple-choice question and wait for the answer.',
259
+ inputSchema: {
260
+ type: 'object',
261
+ properties: {
262
+ question: { type: 'string' },
263
+ options: { type: 'array', items: { type: 'string' } },
264
+ },
265
+ required: ['question', 'options'],
266
+ },
267
+ },
268
+ ],
269
+ });
270
+
271
+ // In your event loop, answer the consumer tool's `tool-call` via
272
+ // `session.submitToolResult({ toolCallId, toolName, result })`. Consumer
273
+ // tools never reach the approval gate (the harness adds the consumer-tool
274
+ // wire prefix to `bypassApprovalTools` automatically — see #529 step 2b).
275
+ // They surface as a normal `tool-call` event with no preceding
276
+ // `tool-approval-request`, even when `requireToolApproval: true`.
277
+ ```
278
+
279
+ This is the recommended pattern when the consumer wants identical behavior across Mastra and Claude. For Claude-only
280
+ consumers who prefer to drive the built-in directly, see "Bypassing the approval gate for specific tools" below.
281
+
282
+ ### Bypassing the approval gate for specific tools
283
+
284
+ When `requireToolApproval: true` is set, every built-in / MCP tool call reaches the coordinator's `PreToolUse` hook by
285
+ default — including Claude built-ins like `AskUserQuestion`. (Consumer-declared tools from `AgentConfig.tools[i]`
286
+ already bypass automatically — see "Tool approval" above.) `ClaudeHarnessFactoryConfig.bypassApprovalTools` opts
287
+ additional tool names out of the gate.
288
+
289
+ ```ts
290
+ const harnessFactory = new ClaudeHarnessFactory({
291
+ resolver: new SalesforceBedrockClaudeTransportResolver({ gatewayBaseUrl: '...' }),
292
+ permissionMode: 'bypassPermissions',
293
+ // Even when `requireToolApproval: true`, calls to these tools bypass the
294
+ // PreToolUse hook. They still emit `tool-call` / `tool-result` ChatEvents,
295
+ // so the consumer can drive their UX off the `tool-call` directly.
296
+ bypassApprovalTools: [
297
+ 'AskUserQuestion', // Claude built-in
298
+ 'ask_user_tool', // consumer-declared (also bypassed automatically; listing here is harmless)
299
+ ],
300
+ });
301
+ ```
302
+
303
+ Tool names use the **consumer-facing** form — the bare `AgentConfig.tools[i].name` for consumer-declared tools, the
304
+ SDK-built-in name for Claude built-ins (no `mcp__<bridge>__` prefix needed). The harness expands each entry into the
305
+ underlying wire-prefixed form (`mcp__sfdx-agent-sdk-consumer-tools__<name>`) where applicable so consumer tools bypass
306
+ correctly without consumers needing to know the internal namespacing.
307
+
308
+ `bypassApprovalTools` is encoded as a negative-lookahead regex on the SDK `PreToolUse` hook's `matcher` field, so
309
+ non-matching tools fall through to the SDK's permission cache and run natively under the factory-pinned
310
+ `permissionMode`. The flag is Claude-only by design; Mastra has no equivalent because its approval pipeline gates
311
+ consumer-declared tools differently.
312
+
313
+ For bypassed consumer tools, the chat stream emits the `tool-call` ChatEvent and a synthetic `finish('tool-calls')` on
314
+ the same `eventStream` (the SDK contract for client-side tools — see `ToolDefinition` JSDoc). The consumer runs the tool
315
+ locally and calls `submitToolResult` with the result; the agent loop continues on the same `eventStream` (post-#529
316
+ single-stream-per-turn contract).
317
+
318
+ > **Note:** `permissionMode: 'plan'` denies mutating built-ins (`Edit`, `Write`, `Bash`, …) at the SDK's permission
319
+ > cache rather than running them natively. Bypassing such a tool with `bypassApprovalTools` does not "unlock" it under
320
+ > plan mode — the SDK still denies it. Plan mode applies to the whole subprocess; this flag only chooses whether the
321
+ > harness's coordinator gets a say.
322
+
323
+ When `bypassApprovalTools` is undefined or empty (default), every built-in / MCP tool call reaches the coordinator — no
324
+ behavior change for existing consumers. (Consumer-declared tools always bypass.)
325
+
226
326
  ## Development
227
327
 
228
328
  See [DEVELOPING.md](DEVELOPING.md) for build, test, and packaging instructions.
@@ -0,0 +1,205 @@
1
+ import type { HookCallback, SDKMessage } from '@anthropic-ai/claude-agent-sdk';
2
+ import { type ChatEvent } from '@salesforce/sfdx-agent-sdk';
3
+ import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
4
+ import type { ClaudeEventAdapter, McpToolMetadataLookup } from './claude-event-adapter.js';
5
+ export interface ClaudeApprovalCoordinatorOptions {
6
+ adapter: ClaudeEventAdapter;
7
+ toolApprovalTimeoutMs: number;
8
+ abortController: AbortController;
9
+ /**
10
+ * Optional lookup that resolves an MCP server name + annotations from a
11
+ * Claude-namespaced tool name (`mcp__<server>__<tool>`). When present,
12
+ * `tool-approval-request` events the coordinator pushes from
13
+ * {@link preToolUseHook} carry `serverName` + `annotations` for
14
+ * MCP-bridged tools — matching the enrichment the adapter applies to
15
+ * `tool-call` / `tool-result` events.
16
+ */
17
+ lookupMcpToolMetadata?: McpToolMetadataLookup;
18
+ /**
19
+ * Approval-emission mode (#447). Defaults to `'serial'` when omitted.
20
+ *
21
+ * - `'serial'`: emit `tool-approval-request` only when the SDK's
22
+ * `PreToolUse` hook fires for that tool — one at a time. Identical
23
+ * to behavior before parallel-approval UX. Safe for consumers
24
+ * whose iterator returns on the first approval-request.
25
+ * - `'batch'`: front-load `tool-approval-request` emission off the
26
+ * adapter's `tool-call` events so parallel `tool_use` batches
27
+ * surface all approvals on the same stream. Consumers MUST iterate
28
+ * to natural park collecting approvals.
29
+ */
30
+ mode?: 'serial' | 'batch';
31
+ /**
32
+ * Logical bridge server name for the consumer-tools bridge. The
33
+ * coordinator strips this prefix on `tool-call` / `tool-result`
34
+ * ChatEvents before forwarding them to the consumer so the
35
+ * `ToolCallEvent.toolName === AgentConfig.tools[i].name` cross-harness
36
+ * contract holds.
37
+ *
38
+ * When omitted, the coordinator skips consumer-tool bookkeeping
39
+ * entirely — used by tests that drive the approval path without any
40
+ * consumer tools registered.
41
+ */
42
+ consumerToolsBridgeName?: string;
43
+ }
44
+ export declare class ClaudeApprovalCoordinator {
45
+ private readonly adapter;
46
+ private readonly toolApprovalTimeoutMs;
47
+ private readonly abortController;
48
+ private readonly lookupMcpToolMetadata?;
49
+ private readonly mode;
50
+ private readonly consumerToolBridgePrefix;
51
+ /** Single per-turn sink. Issue #529 contract: one stream per turn. */
52
+ private readonly initial;
53
+ /** Parked `PreToolUse` hook promises, keyed by `toolCallId`. */
54
+ private readonly pendingApprovals;
55
+ /** Buffered consumer decisions, keyed by `toolCallId`. */
56
+ private readonly decisions;
57
+ /** ToolCallIds for which a `tool-approval-request` has been emitted. */
58
+ private readonly emittedApprovals;
59
+ /** ToolCallIds the consumer has settled. Rejects double-settle. */
60
+ private readonly consumerSettled;
61
+ /**
62
+ * ToolCallIds the coordinator auto-resolved (timeout / abort / dispose).
63
+ * A subsequent settle for these ids rejects with "already auto-resolved".
64
+ */
65
+ private readonly settledByCoordinator;
66
+ /** Args from the adapter's `tool-call` ChatEvent, keyed by `toolCallId`. */
67
+ private readonly toolCallArgs;
68
+ /**
69
+ * Consumer-tool: tool-call events pump has seen but no parker invocation
70
+ * for yet. FIFO list per tool name. Empirically the Claude subprocess
71
+ * emits the assistant's `tool_use` block (rendered as `tool-call`) BEFORE
72
+ * dispatching the MCP `tools/call` to the bridge handler.
73
+ */
74
+ private readonly recentToolCallsByName;
75
+ /** Reverse lookup `toolCallId → toolName` for entries in {@link recentToolCallsByName}. */
76
+ private readonly recentToolCallNameById;
77
+ /** Consumer-tool: parker invocations awaiting a `tool-call` arrival. FIFO list per tool name. */
78
+ private readonly waitingParkersByName;
79
+ /** Consumer-tool: bound parkers, keyed by `toolCallId`. `submitToolResult` resolves these. */
80
+ private readonly pendingByToolCallId;
81
+ /** Consumer-tool: results submitted before the bridge handler parked. Keyed by `toolCallId`. */
82
+ private readonly preSubmittedByToolCallId;
83
+ /**
84
+ * Callbacks invoked exactly once when the coordinator tears down
85
+ * (`abort` / `dispose` / pump exit, whichever fires first). Used by
86
+ * the harness to detach the consumer-AbortSignal listener.
87
+ */
88
+ private readonly teardownCallbacks;
89
+ private disposed;
90
+ private aborted;
91
+ /**
92
+ * Set on entry to {@link teardown} so re-entry from any reason
93
+ * (`abort` / `dispose` / `timeout` / `pump-error`) is a no-op. The
94
+ * timeout path in particular calls `abortController.abort()` which
95
+ * causes the pump iterator to throw — without this guard the pump's
96
+ * catch block would observe `!aborted && !disposed` and fire a second
97
+ * teardown. Side effects in `teardown` are idempotent today
98
+ * (`pendingApprovals` clear, ended-sink push no-ops, `teardownCallbacks`
99
+ * set cleared on first run, `AbortController.abort` itself is
100
+ * idempotent), but a single guard is cheaper than auditing every
101
+ * future addition.
102
+ */
103
+ private tornDown;
104
+ constructor(opts: ClaudeApprovalCoordinatorOptions);
105
+ initialSink(): GenSink<ChatEvent>;
106
+ onTeardown(callback: () => void): void;
107
+ private runTeardownCallbacks;
108
+ start(query: AsyncIterable<SDKMessage>): void;
109
+ abort(): void;
110
+ dispose(): void;
111
+ /**
112
+ * Single teardown path for abort / dispose / approval-timeout /
113
+ * pump-error. Marks every emitted approval `settledByCoordinator`
114
+ * (so a subsequent `approve` / `decline` rejects with
115
+ * "already auto-resolved"), drains every parked `PreToolUse` hook
116
+ * Promise and per-id timer, rejects every parked consumer-tool
117
+ * parker, pushes the synthetic terminal pair on the sink, aborts
118
+ * the subprocess via the injected controller, and fires teardown
119
+ * callbacks.
120
+ *
121
+ * The `reason` parameter changes two things:
122
+ * - `'dispose'` pushes an error event only, no `finish` (matches
123
+ * pre-#529 dispose contract: disposal is consumer-triggered,
124
+ * not a normal turn end).
125
+ * - `'timeout'` resolves every parked hook with `decision: 'block'`
126
+ * so the agentic loop ends politely; the other reasons reject
127
+ * parked hooks with the underlying error so the SDK surfaces
128
+ * each as a tool failure.
129
+ *
130
+ * Mirrors the Mastra coordinator's `teardown` so the pump-throw /
131
+ * abort / dispose / timeout paths can't drift out of step on either
132
+ * harness — the audit's Finding #1 (pump-throw drain hygiene) is
133
+ * the regression this single-source teardown discipline pins.
134
+ */
135
+ private teardown;
136
+ /**
137
+ * The Claude SDK's `PreToolUse` hook. Fires before the SDK's per-tool /
138
+ * per-arg permission cache (which the deprecated `canUseTool` route hit
139
+ * AFTER the cache, so ~70% of tool calls slipped past the gate — see
140
+ * #484).
141
+ *
142
+ * Consumer-executed tools never reach this hook because the options
143
+ * builder unconditionally bypasses the consumer-tool wire prefix in the
144
+ * matcher (#529 step 2b decouple). That keeps the parker path the
145
+ * single source of truth for consumer-tool lifecycle and lets this
146
+ * hook focus on built-in / external-MCP tool gating.
147
+ */
148
+ readonly preToolUseHook: HookCallback;
149
+ private emitApprovalRequest;
150
+ private enrichmentFor;
151
+ private handleApprovalTimeout;
152
+ approve(toolCallId: string): void;
153
+ decline(toolCallId: string): void;
154
+ private settlePending;
155
+ /**
156
+ * Parker callback the in-process consumer-tools MCP bridge handler
157
+ * invokes when the Claude subprocess calls `tools/call` for a
158
+ * consumer-declared tool. Returns a Promise the consumer settles via
159
+ * {@link submitToolResult}.
160
+ *
161
+ * Three-way ladder:
162
+ * 1. A `tool-call` event with `toolCallId` already routed AND a result
163
+ * has been pre-submitted under that id → resolve immediately.
164
+ * 2. A `tool-call` event already routed (no pre-submit) → bind under
165
+ * `toolCallId` so the eventual `submitToolResult` resolves us.
166
+ * 3. Neither → queue under `toolName` (FIFO); the next `tool-call`
167
+ * for that name binds to us.
168
+ */
169
+ readonly parkConsumerToolCall: (call: {
170
+ toolName: string;
171
+ args: Record<string, unknown>;
172
+ }) => Promise<{
173
+ result: unknown;
174
+ isError?: boolean;
175
+ }>;
176
+ /**
177
+ * Settle a parked consumer-tool call. Issue #529 contract: returns
178
+ * `void`; post-resume events flow through the same `initial` sink the
179
+ * consumer is iterating from `chat()`.
180
+ */
181
+ submitToolResult(toolCallId: string, result: unknown, isError?: boolean): void;
182
+ private failAllPendingConsumerCalls;
183
+ private pump;
184
+ private route;
185
+ /**
186
+ * Strip the consumer-tool bridge prefix on `tool-call` / `tool-result`
187
+ * events and FIFO-bind `toolCallId`s for parker matching. Returns
188
+ * `undefined` if no rewrite is needed.
189
+ */
190
+ private maybeRewriteForConsumerTool;
191
+ /**
192
+ * FIFO-bind a freshly-seen consumer-tool `toolCallId` to whichever queued
193
+ * state already exists for `strippedToolName`. Three-way ladder:
194
+ *
195
+ * 1. A parker is already waiting (bridge handler arrived first) →
196
+ * pair the parker against this `toolCallId`.
197
+ * 2. Neither parker nor pre-submitted → record the `toolCallId` against
198
+ * the name so the next parker invocation for that name binds to it.
199
+ *
200
+ * (Pre-submit-before-tool-call is impossible: `submitToolResult`
201
+ * requires a `toolCallId` the consumer received from a prior
202
+ * `tool-call` event.)
203
+ */
204
+ private bindToolCallId;
205
+ }