@salesforce/sfdx-agent-harness-openai 0.0.1
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 +37 -0
- package/LICENSE.txt +21 -0
- package/README.md +55 -0
- package/dist/gen-sink.d.ts +8 -0
- package/dist/gen-sink.js +13 -0
- package/dist/gen-sink.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-error-classifier.d.ts +36 -0
- package/dist/mcp-error-classifier.js +166 -0
- package/dist/mcp-error-classifier.js.map +1 -0
- package/dist/openai-agents-harness-factory.d.ts +36 -0
- package/dist/openai-agents-harness-factory.js +39 -0
- package/dist/openai-agents-harness-factory.js.map +1 -0
- package/dist/openai-agents-harness.d.ts +302 -0
- package/dist/openai-agents-harness.js +1014 -0
- package/dist/openai-agents-harness.js.map +1 -0
- package/dist/openai-approval-coordinator.d.ts +231 -0
- package/dist/openai-approval-coordinator.js +422 -0
- package/dist/openai-approval-coordinator.js.map +1 -0
- package/dist/openai-built-in-policies.d.ts +29 -0
- package/dist/openai-built-in-policies.js +33 -0
- package/dist/openai-built-in-policies.js.map +1 -0
- package/dist/openai-event-adapter.d.ts +119 -0
- package/dist/openai-event-adapter.js +322 -0
- package/dist/openai-event-adapter.js.map +1 -0
- package/dist/openai-mcp-config-mapper.d.ts +58 -0
- package/dist/openai-mcp-config-mapper.js +133 -0
- package/dist/openai-mcp-config-mapper.js.map +1 -0
- package/dist/openai-mcp-state.d.ts +67 -0
- package/dist/openai-mcp-state.js +6 -0
- package/dist/openai-mcp-state.js.map +1 -0
- package/dist/openai-message-mapper.d.ts +79 -0
- package/dist/openai-message-mapper.js +374 -0
- package/dist/openai-message-mapper.js.map +1 -0
- package/dist/openai-model-provider.d.ts +46 -0
- package/dist/openai-model-provider.js +144 -0
- package/dist/openai-model-provider.js.map +1 -0
- package/dist/openai-session-store.d.ts +149 -0
- package/dist/openai-session-store.js +328 -0
- package/dist/openai-session-store.js.map +1 -0
- package/dist/openai-tool-mapper.d.ts +121 -0
- package/dist/openai-tool-mapper.js +231 -0
- package/dist/openai-tool-mapper.js.map +1 -0
- package/dist/openai-tool-redaction.d.ts +55 -0
- package/dist/openai-tool-redaction.js +82 -0
- package/dist/openai-tool-redaction.js.map +1 -0
- package/dist/test/tsconfig.tsbuildinfo +1 -0
- package/dist/text-stream.d.ts +30 -0
- package/dist/text-stream.js +103 -0
- package/dist/text-stream.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { MaxTurnsExceededError, } from '@openai/agents';
|
|
6
|
+
import { isAbortError } from '@salesforce/agentic-common';
|
|
7
|
+
import { AgentSDKError, AgentSDKErrorType, } from '@salesforce/sfdx-agent-sdk';
|
|
8
|
+
import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
|
|
9
|
+
import { OpenAIEventAdapter } from './openai-event-adapter.js';
|
|
10
|
+
import { teeTextStream } from './text-stream.js';
|
|
11
|
+
/** Default per-`toolCallId` approval timeout — mirrors both sibling harnesses (10 min). */
|
|
12
|
+
const DEFAULT_TOOL_APPROVAL_TIMEOUT_MS = 600_000;
|
|
13
|
+
/**
|
|
14
|
+
* Owns one chat turn's lifecycle for a single `(agentId, threadId)`: the
|
|
15
|
+
* `@openai/agents` run(s), the per-turn `OpenAIEventAdapter`, the
|
|
16
|
+
* `GenSink<ChatEvent>` the consumer iterates, the approval settle ledgers, and a
|
|
17
|
+
* single teardown path.
|
|
18
|
+
*
|
|
19
|
+
* **Suspend / resume.** `@openai/agents` surfaces tool approvals as a batch of
|
|
20
|
+
* `result.interruptions` (`RunToolApprovalItem[]`) on the COMPLETED streamed run
|
|
21
|
+
* — not as a mid-stream event — and resumes via a FRESH `run(agent, state)`. So
|
|
22
|
+
* the pump is a loop: drain a run onto the sink, and if it completed with
|
|
23
|
+
* interruptions, settle each (policy `allow` ⇒ silent `state.approve`; `deny` ⇒
|
|
24
|
+
* silent `state.reject` with a denial message the model sees natively via a real
|
|
25
|
+
* `function_call_result`, no synthesized tool-result; `require-approval` ⇒ emit
|
|
26
|
+
* `tool-approval-request`, arm a timer, and park until the consumer settles),
|
|
27
|
+
* then re-`run(agent, state)` and drain the fresh result onto the SAME sink.
|
|
28
|
+
* Passing `session` on every resume is safe: the runner tracks
|
|
29
|
+
* `_currentTurnPersistedItemCount` on the state and persists only new items, so
|
|
30
|
+
* it stays the sole history writer with no double-write.
|
|
31
|
+
*
|
|
32
|
+
* **Idempotent settle (#589).** `approve` / `decline` are `void`-typed. A second
|
|
33
|
+
* settle on the same `toolCallId`, or a settle after the coordinator auto-resolved
|
|
34
|
+
* it (timeout / abort / dispose / pump-error), is a silent no-op — the consumer's
|
|
35
|
+
* intended outcome (id is settled) is already true. A `toolCallId` the coordinator
|
|
36
|
+
* never emitted an approval for is genuine misuse and throws
|
|
37
|
+
* `AgentSDKError(TOOL_CALL_NOT_FOUND)`.
|
|
38
|
+
*
|
|
39
|
+
* One coordinator serves one turn. `stream()` disposes any prior coordinator on
|
|
40
|
+
* the same `(agentId, threadId)` before constructing a fresh one, so a
|
|
41
|
+
* re-`stream()` mid-turn can't leave a stale pump running.
|
|
42
|
+
*/
|
|
43
|
+
export class OpenAIApprovalCoordinator {
|
|
44
|
+
sink = new GenSink();
|
|
45
|
+
adapter;
|
|
46
|
+
/**
|
|
47
|
+
* Cancels the in-flight `run()`. Distinct from any consumer-supplied
|
|
48
|
+
* `abortSignal` so teardown can cancel the turn (dispose / destroyThread)
|
|
49
|
+
* even when the consumer passed no signal of its own.
|
|
50
|
+
*/
|
|
51
|
+
abortController = new AbortController();
|
|
52
|
+
/** Removes the consumer's external-abort listener (if one was wired). */
|
|
53
|
+
detachExternalAbort;
|
|
54
|
+
/** True once the turn has reached a terminal state by any path. */
|
|
55
|
+
settled = false;
|
|
56
|
+
runner;
|
|
57
|
+
agent;
|
|
58
|
+
input;
|
|
59
|
+
session;
|
|
60
|
+
onSettled;
|
|
61
|
+
policy;
|
|
62
|
+
externalSignal;
|
|
63
|
+
toolApprovalTimeoutMs;
|
|
64
|
+
maxTurns;
|
|
65
|
+
consumerTools;
|
|
66
|
+
onConsumerToolErrors;
|
|
67
|
+
/** Approvals emitted this turn (surfaced a `tool-approval-request`), by `toolCallId`. */
|
|
68
|
+
emittedApprovals = new Set();
|
|
69
|
+
/** Approvals awaiting a consumer settle, by `toolCallId`. */
|
|
70
|
+
pendingApprovals = new Map();
|
|
71
|
+
/** Ids the consumer settled (approve / decline), so a repeat settle is a no-op. */
|
|
72
|
+
consumerSettled = new Set();
|
|
73
|
+
/** Ids the coordinator auto-settled (teardown), so a late consumer settle is a no-op. */
|
|
74
|
+
settledByCoordinator = new Set();
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.runner = options.runner;
|
|
77
|
+
this.agent = options.agent;
|
|
78
|
+
this.input = options.input;
|
|
79
|
+
this.session = options.session;
|
|
80
|
+
this.onSettled = options.onSettled;
|
|
81
|
+
this.policy = options.policy;
|
|
82
|
+
this.externalSignal = options.externalSignal;
|
|
83
|
+
this.toolApprovalTimeoutMs = options.toolApprovalTimeoutMs ?? DEFAULT_TOOL_APPROVAL_TIMEOUT_MS;
|
|
84
|
+
this.maxTurns = options.maxTurns;
|
|
85
|
+
this.consumerTools = options.consumerTools;
|
|
86
|
+
this.onConsumerToolErrors = options.onConsumerToolErrors;
|
|
87
|
+
this.adapter = new OpenAIEventAdapter(options.mcpCatalog);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Start pumping the run loop onto the sink (background task) and return the
|
|
91
|
+
* consumer's `ChatStreamResult`. Callers MUST have already thrown on an
|
|
92
|
+
* already-aborted signal (`stream()` does this synchronously at entry, per
|
|
93
|
+
* the pre-existing contract), so `start()` only wires the live-abort path.
|
|
94
|
+
*/
|
|
95
|
+
start() {
|
|
96
|
+
if (this.externalSignal !== undefined) {
|
|
97
|
+
const onAbort = () => this.teardown(new AbortError(), 'abort');
|
|
98
|
+
this.externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
99
|
+
this.detachExternalAbort = () => this.externalSignal?.removeEventListener('abort', onAbort);
|
|
100
|
+
}
|
|
101
|
+
void this.pump();
|
|
102
|
+
return teeTextStream(this.sink.generator());
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Cancel the turn and release its resources. Used by `destroyThread`,
|
|
106
|
+
* `destroyAgent`, and `shutdown`. Idempotent (the `settled` guard in
|
|
107
|
+
* `teardown` collapses a second call to a no-op), and safe to call after the
|
|
108
|
+
* turn completed naturally.
|
|
109
|
+
*/
|
|
110
|
+
dispose() {
|
|
111
|
+
this.teardown(new AgentSDKError('Harness disposed.', AgentSDKErrorType.DISPOSED), 'DISPOSED');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Approve a pending tool call. Buffers the decision and wakes the parked pump
|
|
115
|
+
* so it drives `state.approve(item)` + resume. Idempotent (#589): a repeat or
|
|
116
|
+
* post-teardown settle is a silent no-op; an id with no emitted approval is
|
|
117
|
+
* misuse and throws `TOOL_CALL_NOT_FOUND`.
|
|
118
|
+
*/
|
|
119
|
+
approve(toolCallId) {
|
|
120
|
+
this.settleApproval('approve', toolCallId);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Decline a pending tool call. Buffers the decision and wakes the parked pump
|
|
124
|
+
* so it drives `state.reject(item, { message })` + resume — the run loop feeds
|
|
125
|
+
* a real `function_call_result` carrying the denial text back to the model, so
|
|
126
|
+
* the decline is model-visible with no synthesized result. Same idempotency
|
|
127
|
+
* contract as {@link approve}.
|
|
128
|
+
*/
|
|
129
|
+
decline(toolCallId) {
|
|
130
|
+
this.settleApproval('decline', toolCallId);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Feed a consumer-executed tool's result back into the turn. Resolves the
|
|
134
|
+
* parked `execute` in the {@link ConsumerToolRegistry} keyed by `toolCallId`;
|
|
135
|
+
* the SDK run loop — blocked inside the pump's `await result.completed` on
|
|
136
|
+
* that parked promise — then continues the SAME run (emits `tool_output`, the
|
|
137
|
+
* model's follow-up, and a natural terminal `finish`) onto the same sink. No
|
|
138
|
+
* fresh `run()` and no synthetic `finish('tool-calls')`: unlike the approval
|
|
139
|
+
* path, the run never left. Idempotent (#589) and typed-throws
|
|
140
|
+
* (`TOOL_CALL_NOT_FOUND`) are owned by the registry.
|
|
141
|
+
*/
|
|
142
|
+
submitToolResult(toolCallId, outcome) {
|
|
143
|
+
if (this.consumerTools === undefined) {
|
|
144
|
+
throw new AgentSDKError(`No parked consumer tool call for id "${toolCallId}".`, AgentSDKErrorType.TOOL_CALL_NOT_FOUND);
|
|
145
|
+
}
|
|
146
|
+
this.consumerTools.settle(toolCallId, outcome);
|
|
147
|
+
}
|
|
148
|
+
settleApproval(decision, toolCallId) {
|
|
149
|
+
// Lost-race no-op: already settled by the consumer or auto-settled by a
|
|
150
|
+
// teardown. The consumer's intended outcome (id is settled) already holds.
|
|
151
|
+
if (this.consumerSettled.has(toolCallId) || this.settledByCoordinator.has(toolCallId))
|
|
152
|
+
return;
|
|
153
|
+
const pending = this.pendingApprovals.get(toolCallId);
|
|
154
|
+
if (pending === undefined) {
|
|
155
|
+
// Never emitted an approval for this id — genuine misuse.
|
|
156
|
+
throw new AgentSDKError(`No pending tool-approval for tool call "${toolCallId}".`, AgentSDKErrorType.TOOL_CALL_NOT_FOUND);
|
|
157
|
+
}
|
|
158
|
+
this.consumerSettled.add(toolCallId);
|
|
159
|
+
clearTimeout(pending.timer);
|
|
160
|
+
this.pendingApprovals.delete(toolCallId);
|
|
161
|
+
pending.resolve(decision);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The turn's pump: run to a terminal state across one-or-more runs, draining
|
|
165
|
+
* each onto the SAME sink, settling any interruptions between runs. The SDK
|
|
166
|
+
* throws a mid-stream model error out of the iterator rather than emitting an
|
|
167
|
+
* event, so the `catch` synthesizes `error` + `finish('error')`.
|
|
168
|
+
*/
|
|
169
|
+
async pump() {
|
|
170
|
+
try {
|
|
171
|
+
let result = (await this.runner.run(this.agent, this.input, this.runOptions()));
|
|
172
|
+
await this.drainRun(result);
|
|
173
|
+
// Resume loop: while the completed run carries interruptions, settle
|
|
174
|
+
// each and re-run onto the same sink until a run finishes clean.
|
|
175
|
+
while (!this.settled && result.interruptions.length > 0) {
|
|
176
|
+
await this.settleInterruptions(result);
|
|
177
|
+
if (this.settled)
|
|
178
|
+
return;
|
|
179
|
+
result = (await this.runner.run(this.agent, result.state, this.runOptions()));
|
|
180
|
+
await this.drainRun(result);
|
|
181
|
+
}
|
|
182
|
+
if (this.settled)
|
|
183
|
+
return;
|
|
184
|
+
// Flush any `step-finish` the adapter deferred for a tool-calling
|
|
185
|
+
// step whose outputs drained without a following `response_started`
|
|
186
|
+
// (the last step of the turn), so every step closes before the
|
|
187
|
+
// terminal `finish`.
|
|
188
|
+
for (const chatEvent of this.adapter.drainDeferredStepFinish())
|
|
189
|
+
this.sink.push(chatEvent);
|
|
190
|
+
// The run loop emits no dedicated terminal event and does not throw on
|
|
191
|
+
// clean completion, so synthesize the single terminal `finish`.
|
|
192
|
+
this.settled = true;
|
|
193
|
+
this.sink.push({ type: 'finish', finishReason: 'stop', ...usageField(this.adapter.lastUsage()) });
|
|
194
|
+
this.sink.end();
|
|
195
|
+
this.cleanup();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (this.settled)
|
|
199
|
+
return;
|
|
200
|
+
for (const startEvent of this.adapter.ensureStarted()) {
|
|
201
|
+
this.sink.push(startEvent);
|
|
202
|
+
}
|
|
203
|
+
// Exceeding the step limit is a normal terminal condition, not a
|
|
204
|
+
// failure: `run(..., { maxTurns })` throws `MaxTurnsExceededError`
|
|
205
|
+
// out of the streamed iterator (like every mid-stream signal), but
|
|
206
|
+
// the harness surfaces it as a terminal `finish('max-steps')` with
|
|
207
|
+
// NO preceding `error` event — matching the Mastra / Claude harnesses
|
|
208
|
+
// and the SDK's `FinishReason` contract. Any other throw is a real
|
|
209
|
+
// mid-stream failure and keeps the `error` + `finish('error')` pair.
|
|
210
|
+
if (error instanceof MaxTurnsExceededError) {
|
|
211
|
+
for (const chatEvent of this.adapter.drainDeferredStepFinish())
|
|
212
|
+
this.sink.push(chatEvent);
|
|
213
|
+
this.releaseParkedApprovals();
|
|
214
|
+
this.settled = true;
|
|
215
|
+
this.sink.push({ type: 'finish', finishReason: 'max-steps', ...usageField(this.adapter.lastUsage()) });
|
|
216
|
+
this.sink.end();
|
|
217
|
+
this.cleanup();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const aborted = isAbortError(error) || this.abortController.signal.aborted;
|
|
221
|
+
this.releaseParkedApprovals();
|
|
222
|
+
this.settled = true;
|
|
223
|
+
this.sink.push({
|
|
224
|
+
type: 'error',
|
|
225
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
226
|
+
code: aborted ? 'abort' : 'stream-error',
|
|
227
|
+
});
|
|
228
|
+
this.sink.push({ type: 'finish', finishReason: 'error' });
|
|
229
|
+
this.sink.end();
|
|
230
|
+
this.cleanup();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The `run()` options shared by the initial run and every resume, so the two
|
|
235
|
+
* can't drift.
|
|
236
|
+
*
|
|
237
|
+
* `reasoningItemIdPolicy: 'omit'` is load-bearing against the Salesforce
|
|
238
|
+
* gateway. The gateway is a stateless `/responses` pass-through
|
|
239
|
+
* (`store: false`), so it does not persist reasoning items by id. The SDK's
|
|
240
|
+
* default re-sends prior reasoning items BY ID REFERENCE (`rs_...`) on every
|
|
241
|
+
* follow-up model call — the initial run's second step (after a tool result)
|
|
242
|
+
* and every resume run — which the gateway rejects with
|
|
243
|
+
* `400 Item with id 'rs_...' not found. Items are not persisted when 'store'
|
|
244
|
+
* is set to false.` `'omit'` strips the id so the reasoning content is sent
|
|
245
|
+
* inline rather than referenced, which the stateless gateway accepts. (Server
|
|
246
|
+
* -side conversation state via `previousResponseId` is likewise unavailable
|
|
247
|
+
* here; the disk-backed `session` is our source of truth for prior turns.)
|
|
248
|
+
*/
|
|
249
|
+
runOptions() {
|
|
250
|
+
return {
|
|
251
|
+
stream: true,
|
|
252
|
+
session: this.session,
|
|
253
|
+
signal: this.abortController.signal,
|
|
254
|
+
reasoningItemIdPolicy: 'omit',
|
|
255
|
+
// Forward the turn's step limit (`StreamOptions.maxSteps`) as
|
|
256
|
+
// `maxTurns`. Omitting the key entirely when unset preserves the
|
|
257
|
+
// SDK's own default rather than forcing `null` (unbounded).
|
|
258
|
+
...(this.maxTurns !== undefined ? { maxTurns: this.maxTurns } : {}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Drain one streamed run result: map every event onto the sink, then await
|
|
263
|
+
* the run's terminal completion so the SDK finishes writing session state
|
|
264
|
+
* (the runner owns the history write) before the caller decides
|
|
265
|
+
* the turn's next step. Does NOT emit any terminal `finish` — that is the
|
|
266
|
+
* pump's decision, since a run that ends with `interruptions` is resumed
|
|
267
|
+
* rather than finished.
|
|
268
|
+
*/
|
|
269
|
+
async drainRun(result) {
|
|
270
|
+
for await (const event of result) {
|
|
271
|
+
for (const chatEvent of this.adapter.map(event)) {
|
|
272
|
+
// Record consumer-tool call ids the moment they're surfaced so a
|
|
273
|
+
// `submitToolResult` that races ahead of the parked `execute` (the
|
|
274
|
+
// SDK emits `tool_called` before awaiting `execute`) is buffered,
|
|
275
|
+
// not rejected.
|
|
276
|
+
if (chatEvent.type === 'tool-call' && this.consumerTools?.toolNames.has(chatEvent.toolName) === true) {
|
|
277
|
+
this.consumerTools.markEmitted(chatEvent.toolCallId);
|
|
278
|
+
}
|
|
279
|
+
this.sink.push(chatEvent);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
await result.completed;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Settle every interruption on a completed run against the policy, mutating
|
|
286
|
+
* `result.state` in place so the caller can resume from it. `allow` / `deny`
|
|
287
|
+
* are resolved silently here; `require-approval` emits a
|
|
288
|
+
* `tool-approval-request`, arms a timer, and awaits the consumer's settle.
|
|
289
|
+
* Returns once all interruptions on this run have been approved or rejected
|
|
290
|
+
* (or the turn was torn down while a settle was parked).
|
|
291
|
+
*/
|
|
292
|
+
async settleInterruptions(result) {
|
|
293
|
+
for (const item of result.interruptions) {
|
|
294
|
+
if (this.settled)
|
|
295
|
+
return;
|
|
296
|
+
const toolCallId = item.rawItem?.callId;
|
|
297
|
+
const toolName = item.toolName ?? item.name ?? item.rawItem?.name;
|
|
298
|
+
// An interruption with no callId/name can't be routed or settled —
|
|
299
|
+
// approve it so the run makes progress rather than deadlocking.
|
|
300
|
+
if (toolCallId === undefined || toolName === undefined) {
|
|
301
|
+
result.state.approve(item);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const decision = this.policy?.({ toolName }) ?? 'require-approval';
|
|
305
|
+
if (decision === 'allow') {
|
|
306
|
+
result.state.approve(item);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (decision === 'deny') {
|
|
310
|
+
result.state.reject(item, { message: 'Tool call was not approved by policy.' });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const settled = await this.awaitApproval(toolCallId, toolName, item);
|
|
314
|
+
if (settled === undefined)
|
|
315
|
+
return; // torn down while parked
|
|
316
|
+
if (settled === 'approve')
|
|
317
|
+
result.state.approve(item);
|
|
318
|
+
else
|
|
319
|
+
result.state.reject(item, { message: 'User declined the tool call.' });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Emit a `tool-approval-request`, arm the per-`toolCallId` timeout, and return
|
|
324
|
+
* a promise that resolves when the consumer settles (or `undefined` if the
|
|
325
|
+
* turn is torn down while parked, in which case the pump exits).
|
|
326
|
+
*/
|
|
327
|
+
awaitApproval(toolCallId, toolName, item) {
|
|
328
|
+
this.sink.push({
|
|
329
|
+
type: 'tool-approval-request',
|
|
330
|
+
toolCall: { toolCallId, toolName, args: parseArgs(item) },
|
|
331
|
+
});
|
|
332
|
+
this.emittedApprovals.add(toolCallId);
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
const timer = setTimeout(() => {
|
|
335
|
+
this.teardown(new Error(`Tool approval timed out after ${this.toolApprovalTimeoutMs}ms for "${toolCallId}".`), 'tool-approval-timeout');
|
|
336
|
+
}, this.toolApprovalTimeoutMs);
|
|
337
|
+
this.pendingApprovals.set(toolCallId, { resolve, timer });
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Single terminal path for abort / dispose / timeout. Releases every parked
|
|
342
|
+
* approval so nothing hangs, aborts the in-flight run, emits the terminal
|
|
343
|
+
* `error` + `finish('error')` pair (a `start` first if the turn produced no
|
|
344
|
+
* events), and settles. `code` distinguishes the reason to the consumer
|
|
345
|
+
* (`'abort'` / `'DISPOSED'` / `'tool-approval-timeout'`). The `settled` guard
|
|
346
|
+
* makes it single-shot and a no-op after natural completion.
|
|
347
|
+
*/
|
|
348
|
+
teardown(error, code) {
|
|
349
|
+
if (this.settled)
|
|
350
|
+
return;
|
|
351
|
+
this.releaseParkedApprovals();
|
|
352
|
+
this.settled = true;
|
|
353
|
+
this.abortController.abort();
|
|
354
|
+
for (const startEvent of this.adapter.ensureStarted()) {
|
|
355
|
+
this.sink.push(startEvent);
|
|
356
|
+
}
|
|
357
|
+
this.sink.push({ type: 'error', error, code });
|
|
358
|
+
this.sink.push({ type: 'finish', finishReason: 'error' });
|
|
359
|
+
this.sink.end();
|
|
360
|
+
this.cleanup();
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Release every parked approval AND every parked consumer-tool call: cancel
|
|
364
|
+
* each approval timer, record the id as coordinator-settled (so a late
|
|
365
|
+
* consumer settle is a no-op, #589), resolve the parked approval waiter with
|
|
366
|
+
* `undefined` so the pump exits its `awaitApproval`, and reject every parked
|
|
367
|
+
* consumer `execute` so the SDK run loop unblocks rather than hanging on a
|
|
368
|
+
* promise that will never settle. Shared by the error path and
|
|
369
|
+
* {@link teardown}. Does not set `settled` — callers own that ordering
|
|
370
|
+
* relative to the terminal emit.
|
|
371
|
+
*/
|
|
372
|
+
releaseParkedApprovals() {
|
|
373
|
+
for (const [toolCallId, pending] of this.pendingApprovals) {
|
|
374
|
+
clearTimeout(pending.timer);
|
|
375
|
+
this.settledByCoordinator.add(toolCallId);
|
|
376
|
+
pending.resolve(undefined);
|
|
377
|
+
}
|
|
378
|
+
for (const toolCallId of this.emittedApprovals)
|
|
379
|
+
this.settledByCoordinator.add(toolCallId);
|
|
380
|
+
this.pendingApprovals.clear();
|
|
381
|
+
this.consumerTools?.rejectAll(new Error('Turn ended before the consumer tool result arrived.'));
|
|
382
|
+
}
|
|
383
|
+
/** Detach the external-abort listener and notify the harness exactly once. */
|
|
384
|
+
cleanup() {
|
|
385
|
+
this.detachExternalAbort?.();
|
|
386
|
+
this.detachExternalAbort = undefined;
|
|
387
|
+
// Stamp `isError` onto the persisted results of any consumer tool the
|
|
388
|
+
// consumer settled with `isError: true` — the run loop wrote them without
|
|
389
|
+
// it (the OpenAI item shape has no `isError` field). Fires on every
|
|
390
|
+
// terminal path (a tool can be settled with an error before an abort), and
|
|
391
|
+
// the harness's async stamp settles under the store's per-thread queue
|
|
392
|
+
// after the run's own writes.
|
|
393
|
+
const errored = this.consumerTools?.erroredToolCallIds() ?? [];
|
|
394
|
+
if (errored.length > 0)
|
|
395
|
+
this.onConsumerToolErrors?.(errored);
|
|
396
|
+
this.onSettled();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** An `AbortError`-shaped `Error` so `isAbortError` and consumers classify it. */
|
|
400
|
+
class AbortError extends Error {
|
|
401
|
+
constructor() {
|
|
402
|
+
super('The operation was aborted.');
|
|
403
|
+
this.name = 'AbortError';
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** Parse the interruption's raw `arguments` JSON into an args object (best-effort). */
|
|
407
|
+
function parseArgs(item) {
|
|
408
|
+
const raw = item.arguments;
|
|
409
|
+
if (typeof raw !== 'string')
|
|
410
|
+
return {};
|
|
411
|
+
try {
|
|
412
|
+
const parsed = JSON.parse(raw);
|
|
413
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return {};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function usageField(usage) {
|
|
420
|
+
return usage !== undefined ? { usage } : {};
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=openai-approval-coordinator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-approval-coordinator.js","sourceRoot":"","sources":["../src/openai-approval-coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ToolPolicyRule } from '@salesforce/sfdx-agent-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI harness-specific built-in tool-approval rules — the `tiers.harness`
|
|
4
|
+
* slice passed to `resolveToolApprovalPolicy`. Per the SDK's R4 layering
|
|
5
|
+
* invariant, harness-specific built-ins live here, not in the SDK's
|
|
6
|
+
* harness-agnostic `BUILT_IN_TOOL_POLICIES`.
|
|
7
|
+
*
|
|
8
|
+
* **Empty today.** Unlike Claude (`Bash` / `Edit` / … registered by the CLI
|
|
9
|
+
* subprocess) and Mastra (`updateWorkingMemory` / `skill*` registered by the
|
|
10
|
+
* runtime), the OpenAI harness registers no runtime built-in tools of its own:
|
|
11
|
+
* a turn's tools are the consumer-declared (`AgentConfig.tools`) and — once MCP
|
|
12
|
+
* support lands — MCP tools, both of which the resolver already covers via
|
|
13
|
+
* `builtin` / `mcp` matchers on the consumer tier. So there is nothing
|
|
14
|
+
* harness-specific to pre-gate here today.
|
|
15
|
+
*
|
|
16
|
+
* The array is present (not omitted) so the harness always passes a
|
|
17
|
+
* `{ harness }` tier to `resolveToolApprovalPolicy` — a stable call shape — and
|
|
18
|
+
* so a future built-in has an obvious home. The `@openai/agents` hosted tools
|
|
19
|
+
* (`shell` / `apply_patch` / `computer`) each carry their OWN `needsApproval`
|
|
20
|
+
* flag in the SDK and are not wired into this harness yet; when they are, decide
|
|
21
|
+
* per-tool whether the gate belongs on the SDK flag or as a rule here.
|
|
22
|
+
*
|
|
23
|
+
* Frozen: adding an entry is a soft contract change consumers will have noticed;
|
|
24
|
+
* removing or modifying one is a breaking change. The drift test in
|
|
25
|
+
* `test/openai-built-in-policies.test.ts` pins the shape (currently: empty, all
|
|
26
|
+
* `builtin` matchers, all `source: 'built-in'` — vacuously true while empty, and
|
|
27
|
+
* the guard the first real entry must satisfy).
|
|
28
|
+
*/
|
|
29
|
+
export declare const OPENAI_BUILT_IN_TOOL_POLICIES: ReadonlyArray<ToolPolicyRule>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* OpenAI harness-specific built-in tool-approval rules — the `tiers.harness`
|
|
7
|
+
* slice passed to `resolveToolApprovalPolicy`. Per the SDK's R4 layering
|
|
8
|
+
* invariant, harness-specific built-ins live here, not in the SDK's
|
|
9
|
+
* harness-agnostic `BUILT_IN_TOOL_POLICIES`.
|
|
10
|
+
*
|
|
11
|
+
* **Empty today.** Unlike Claude (`Bash` / `Edit` / … registered by the CLI
|
|
12
|
+
* subprocess) and Mastra (`updateWorkingMemory` / `skill*` registered by the
|
|
13
|
+
* runtime), the OpenAI harness registers no runtime built-in tools of its own:
|
|
14
|
+
* a turn's tools are the consumer-declared (`AgentConfig.tools`) and — once MCP
|
|
15
|
+
* support lands — MCP tools, both of which the resolver already covers via
|
|
16
|
+
* `builtin` / `mcp` matchers on the consumer tier. So there is nothing
|
|
17
|
+
* harness-specific to pre-gate here today.
|
|
18
|
+
*
|
|
19
|
+
* The array is present (not omitted) so the harness always passes a
|
|
20
|
+
* `{ harness }` tier to `resolveToolApprovalPolicy` — a stable call shape — and
|
|
21
|
+
* so a future built-in has an obvious home. The `@openai/agents` hosted tools
|
|
22
|
+
* (`shell` / `apply_patch` / `computer`) each carry their OWN `needsApproval`
|
|
23
|
+
* flag in the SDK and are not wired into this harness yet; when they are, decide
|
|
24
|
+
* per-tool whether the gate belongs on the SDK flag or as a rule here.
|
|
25
|
+
*
|
|
26
|
+
* Frozen: adding an entry is a soft contract change consumers will have noticed;
|
|
27
|
+
* removing or modifying one is a breaking change. The drift test in
|
|
28
|
+
* `test/openai-built-in-policies.test.ts` pins the shape (currently: empty, all
|
|
29
|
+
* `builtin` matchers, all `source: 'built-in'` — vacuously true while empty, and
|
|
30
|
+
* the guard the first real entry must satisfy).
|
|
31
|
+
*/
|
|
32
|
+
export const OPENAI_BUILT_IN_TOOL_POLICIES = Object.freeze([]);
|
|
33
|
+
//# sourceMappingURL=openai-built-in-policies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-built-in-policies.js","sourceRoot":"","sources":["../src/openai-built-in-policies.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAkC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { RunStreamEvent } from '@openai/agents';
|
|
2
|
+
import type { ChatEvent, FinishReason, UsageMetadata } from '@salesforce/sfdx-agent-sdk';
|
|
3
|
+
import type { McpCatalogEntry } from './openai-mcp-state.js';
|
|
4
|
+
/**
|
|
5
|
+
* Per-stream stateful mapper from `@openai/agents` `RunStreamEvent`s to the
|
|
6
|
+
* SDK's `ChatEvent` variants. A new instance is constructed per `stream()` call
|
|
7
|
+
* because it tracks `stepIndex` and the last-seen per-step usage.
|
|
8
|
+
*
|
|
9
|
+
* **Text + tool events.** The run loop emits three event families:
|
|
10
|
+
* `raw_model_stream_event` (the underlying model stream, carrying
|
|
11
|
+
* `output_text_delta` / `response_started` / `response_done`),
|
|
12
|
+
* `run_item_stream_event` (tool calls / outputs), and
|
|
13
|
+
* `agent_updated_stream_event` (handoffs — not used). This adapter maps the
|
|
14
|
+
* text and step-boundary events plus the `tool_called` / `tool_output` run-item
|
|
15
|
+
* events to `tool-call` / `tool-result` ChatEvents. The terminal `finish` is
|
|
16
|
+
* synthesized by the harness pump when the run stream completes, since the run
|
|
17
|
+
* loop does not emit a dedicated "run finished" event and mid-stream errors are
|
|
18
|
+
* thrown, not emitted.
|
|
19
|
+
*
|
|
20
|
+
* The adapter never emits an event whose `type` is not a member of the
|
|
21
|
+
* `ChatEvent` union. Unrecognized events are skipped.
|
|
22
|
+
*/
|
|
23
|
+
export declare class OpenAIEventAdapter {
|
|
24
|
+
private readonly mcpCatalog?;
|
|
25
|
+
private stepIndex;
|
|
26
|
+
private started;
|
|
27
|
+
private lastStepUsage;
|
|
28
|
+
/**
|
|
29
|
+
* A `step-finish` buffered because its step produced tool calls. The
|
|
30
|
+
* `@openai/agents` run loop emits `response_done` for a tool-calling step
|
|
31
|
+
* BEFORE the `tool_called` / `tool_output` run-items for that step, so
|
|
32
|
+
* emitting `step-finish` inline would place the `tool-result` AFTER its
|
|
33
|
+
* step's `step-finish` — outside the step. We hold the `step-finish` until
|
|
34
|
+
* the step's tool outputs drain (or a new step starts / the run ends), so a
|
|
35
|
+
* `tool-result` always lands inside the `step-start`/`step-finish` bracket
|
|
36
|
+
* that produced it. Mirrors the Claude adapter's deferred-`step-finish`
|
|
37
|
+
* ordering (`pendingToolUseIds`).
|
|
38
|
+
*/
|
|
39
|
+
private deferredStepFinish;
|
|
40
|
+
/** Outstanding `tool_output`s for the deferred step; `step-finish` flushes at 0. */
|
|
41
|
+
private pendingToolOutputs;
|
|
42
|
+
/**
|
|
43
|
+
* Optional MCP tool catalog (bare tool name → `{ serverName, annotations }`),
|
|
44
|
+
* used to enrich `tool-call` / `tool-result` events for MCP-sourced tools
|
|
45
|
+
* with `serverName` / `bareToolName` / `annotations`. Read live so a mid-turn
|
|
46
|
+
* discovery settle enriches later events. `undefined` (or a miss) leaves the
|
|
47
|
+
* fields unset — the contract for consumer / built-in tools.
|
|
48
|
+
*/
|
|
49
|
+
constructor(mcpCatalog?: ReadonlyMap<string, McpCatalogEntry> | undefined);
|
|
50
|
+
/**
|
|
51
|
+
* Map one `RunStreamEvent` to zero or more `ChatEvent`s. Emits a synthetic
|
|
52
|
+
* `start` before the first mapped event.
|
|
53
|
+
*/
|
|
54
|
+
map(event: RunStreamEvent): ChatEvent[];
|
|
55
|
+
/**
|
|
56
|
+
* Map a `run_item_stream_event` to a `tool-call` / `tool-result` ChatEvent.
|
|
57
|
+
* The run loop fires `tool_called` with a `RunToolCallItem` (the model's
|
|
58
|
+
* request) and `tool_output` with a `RunToolCallOutputItem` (the result).
|
|
59
|
+
*
|
|
60
|
+
* **MCP enrichment.** When the tool's (bare) name is in the {@link mcpCatalog},
|
|
61
|
+
* the event carries `serverName` / `bareToolName` / `annotations` so a consumer
|
|
62
|
+
* can build a `{ type: 'mcp', serverName, toolName }` policy matcher without
|
|
63
|
+
* string-splitting the display name. Consumer / built-in tools miss the
|
|
64
|
+
* catalog and surface with `serverName === undefined`, per the SDK contract.
|
|
65
|
+
* `isError` is not carried on the live `tool-result` event — the OpenAI
|
|
66
|
+
* `tool_output` item has no `isError` field; a consumer-reported failure is
|
|
67
|
+
* stamped onto the persisted record instead (see the session store). This
|
|
68
|
+
* maps the raw call/result the loop emits so a tool turn is observable and
|
|
69
|
+
* round-trips through history.
|
|
70
|
+
*/
|
|
71
|
+
private mapRunItem;
|
|
72
|
+
/**
|
|
73
|
+
* Look up the (bare) tool name in the MCP catalog and return the enrichment
|
|
74
|
+
* fields for a `tool-call` / `tool-result` event, or an empty object when the
|
|
75
|
+
* tool is not MCP-sourced. `bareToolName` equals the display `toolName` here
|
|
76
|
+
* because `@openai/agents` registers MCP tools under their bare name.
|
|
77
|
+
*/
|
|
78
|
+
private enrich;
|
|
79
|
+
/**
|
|
80
|
+
* The last per-step usage observed, used to populate the terminal `finish`
|
|
81
|
+
* event the pump synthesizes when the run stream completes cleanly.
|
|
82
|
+
*/
|
|
83
|
+
lastUsage(): UsageMetadata | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Emit a synthetic `start` if the stream produced no events at all (e.g. a
|
|
86
|
+
* turn that failed before the first delta). Called by the pump on the error
|
|
87
|
+
* path so a consumer always sees a `start` before the terminal pair.
|
|
88
|
+
*/
|
|
89
|
+
ensureStarted(): ChatEvent[];
|
|
90
|
+
private mapRaw;
|
|
91
|
+
/**
|
|
92
|
+
* Emit a held `step-finish` (if any) and reset the deferral state. Called
|
|
93
|
+
* when the step's tool outputs have drained, when a new step starts, and by
|
|
94
|
+
* the pump before the terminal `finish` so a deferred step-finish can never
|
|
95
|
+
* be dropped.
|
|
96
|
+
*/
|
|
97
|
+
flushDeferredStepFinish(out: ChatEvent[]): void;
|
|
98
|
+
/** Emit any held `step-finish` as a standalone event list (pump terminal path). */
|
|
99
|
+
drainDeferredStepFinish(): ChatEvent[];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Maps the OpenAI Agents SDK's `response_done` usage shape (camelCase
|
|
103
|
+
* `inputTokens` / `outputTokens` / `totalTokens`, plus optional
|
|
104
|
+
* `inputTokensDetails` / `outputTokensDetails`) onto the SDK's
|
|
105
|
+
* {@link UsageMetadata}. Returns `undefined` when no usage was reported at all,
|
|
106
|
+
* so consumers see an honest "usage not reported" signal rather than a fake
|
|
107
|
+
* all-zero object (the missing-usage-faithfulness contract, W-22692131).
|
|
108
|
+
*/
|
|
109
|
+
export declare function mapUsage(usage: OpenAIStreamUsage | undefined): UsageMetadata | undefined;
|
|
110
|
+
/** The usage shape carried on a `response_done` stream event. */
|
|
111
|
+
export type OpenAIStreamUsage = {
|
|
112
|
+
inputTokens?: number;
|
|
113
|
+
outputTokens?: number;
|
|
114
|
+
totalTokens?: number;
|
|
115
|
+
inputTokensDetails?: Record<string, number> | Array<Record<string, number>>;
|
|
116
|
+
outputTokensDetails?: Record<string, number> | Array<Record<string, number>>;
|
|
117
|
+
};
|
|
118
|
+
/** Re-export so the harness pump can reference the union without re-importing. */
|
|
119
|
+
export type { FinishReason };
|