@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,231 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { tool } from '@openai/agents';
|
|
6
|
+
import { AgentSDKError, AgentSDKErrorType } from '@salesforce/sfdx-agent-sdk';
|
|
7
|
+
import { applyRedaction } from './openai-tool-redaction.js';
|
|
8
|
+
/**
|
|
9
|
+
* Per-turn registry for consumer-executed (client-side) tools — those declared
|
|
10
|
+
* in `AgentConfig.tools` with no `execute`. The harness registers them with the
|
|
11
|
+
* model so it can call them, but never runs them: the mapped tool's `execute`
|
|
12
|
+
* PARKS on a promise this registry hands out, and the consumer resolves it
|
|
13
|
+
* out-of-band via `submitToolResult`. A parked `execute` resolved later lets the
|
|
14
|
+
* `@openai/agents` run loop continue on the same run — no in-process MCP bridge
|
|
15
|
+
* needed.
|
|
16
|
+
*
|
|
17
|
+
* Keyed by `toolCallId`, read directly off `details.toolCall.callId` at execute
|
|
18
|
+
* entry (the SDK populates it before invoking the tool), so binding is a direct
|
|
19
|
+
* map insert — no FIFO-by-name matching.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent settle (#589): a second `settle` on the same id, or a settle after
|
|
22
|
+
* teardown rejected it, is a silent no-op; a `toolCallId` that never parked is
|
|
23
|
+
* genuine misuse and throws `TOOL_CALL_NOT_FOUND`.
|
|
24
|
+
*/
|
|
25
|
+
export class ConsumerToolRegistry {
|
|
26
|
+
toolNames;
|
|
27
|
+
/** Parked calls awaiting a consumer result, by `toolCallId`. */
|
|
28
|
+
pending = new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Outcomes the consumer submitted BEFORE the tool's `execute` parked, by
|
|
31
|
+
* `toolCallId`. The harness emits the `tool-call` ChatEvent the moment the SDK
|
|
32
|
+
* enqueues `tool_called` — which is BEFORE it awaits `execute` — so a consumer
|
|
33
|
+
* iterating the stream can call `submitToolResult` before the parker
|
|
34
|
+
* registers. Buffer it here; `park` drains it immediately so the result is
|
|
35
|
+
* never lost to the race.
|
|
36
|
+
*/
|
|
37
|
+
preSubmitted = new Map();
|
|
38
|
+
/** Ids already settled (by consumer or teardown), so a repeat settle is a no-op. */
|
|
39
|
+
settled = new Set();
|
|
40
|
+
/**
|
|
41
|
+
* Consumer-tool call ids the consumer settled with `isError: true`. The run
|
|
42
|
+
* loop persists the resulting `function_call_result` with no `isError` field
|
|
43
|
+
* (the OpenAI item shape can't carry it), so the coordinator reads this after
|
|
44
|
+
* the turn settles and stamps `isError` onto those records out-of-band,
|
|
45
|
+
* satisfying the #647 "isError survives to history" contract.
|
|
46
|
+
*/
|
|
47
|
+
erroredIds = new Set();
|
|
48
|
+
/**
|
|
49
|
+
* Consumer-tool call ids the coordinator has surfaced as a `tool-call`
|
|
50
|
+
* ChatEvent this turn. A `settle` for a not-yet-parked id is buffered only
|
|
51
|
+
* when the id is here (an early-arriving result); an id never seen is genuine
|
|
52
|
+
* misuse and throws.
|
|
53
|
+
*/
|
|
54
|
+
emitted = new Set();
|
|
55
|
+
/** Whether the turn has torn down — a park after teardown resolves immediately as an error. */
|
|
56
|
+
tornDown = false;
|
|
57
|
+
/** Error to resolve late parks with after teardown. */
|
|
58
|
+
teardownError;
|
|
59
|
+
/** The bare names of the consumer tools registered this turn (for dual-enforcement checks). */
|
|
60
|
+
constructor(toolNames) {
|
|
61
|
+
this.toolNames = toolNames;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Called by a mapped tool's `execute` at invocation. Returns a promise that
|
|
65
|
+
* settles when the consumer calls `submitToolResult` for `toolCallId`. It
|
|
66
|
+
* REJECTS only on teardown (so the run loop unblocks rather than hanging on a
|
|
67
|
+
* promise that will never settle); a normal settle — including an
|
|
68
|
+
* `isError: true` one — RESOLVES with the outcome, because a consumer-reported
|
|
69
|
+
* tool failure must reach the model as a recoverable tool output, not throw
|
|
70
|
+
* out of the run loop (the {@link ToolResultInfo.isError} contract). The
|
|
71
|
+
* mapper's `execute` turns the outcome into the model-visible value. A park
|
|
72
|
+
* after teardown rejects immediately.
|
|
73
|
+
*/
|
|
74
|
+
park(toolCallId) {
|
|
75
|
+
if (this.tornDown)
|
|
76
|
+
return Promise.reject(this.teardownError ?? new Error('Turn ended.'));
|
|
77
|
+
// Drain a pre-submitted outcome (consumer settled before execute parked).
|
|
78
|
+
const early = this.preSubmitted.get(toolCallId);
|
|
79
|
+
if (early !== undefined) {
|
|
80
|
+
this.preSubmitted.delete(toolCallId);
|
|
81
|
+
return Promise.resolve(early);
|
|
82
|
+
}
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
this.pending.set(toolCallId, { resolve, reject });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Record that the coordinator surfaced `toolCallId` as a `tool-call`
|
|
89
|
+
* ChatEvent — so a `submitToolResult` that races ahead of the parker (the
|
|
90
|
+
* SDK emits `tool_called` before awaiting `execute`) is buffered rather than
|
|
91
|
+
* rejected.
|
|
92
|
+
*/
|
|
93
|
+
markEmitted(toolCallId) {
|
|
94
|
+
this.emitted.add(toolCallId);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Settle a consumer tool call with the consumer's result. Resolves the parked
|
|
98
|
+
* `execute` if it has registered; otherwise buffers the outcome for the
|
|
99
|
+
* imminent `park` (the emit-before-await race) when the id was surfaced this
|
|
100
|
+
* turn. Idempotent (#589); throws `TOOL_CALL_NOT_FOUND` for an id the
|
|
101
|
+
* coordinator never surfaced.
|
|
102
|
+
*/
|
|
103
|
+
settle(toolCallId, outcome) {
|
|
104
|
+
if (this.settled.has(toolCallId))
|
|
105
|
+
return;
|
|
106
|
+
const parked = this.pending.get(toolCallId);
|
|
107
|
+
if (parked !== undefined) {
|
|
108
|
+
this.settled.add(toolCallId);
|
|
109
|
+
if (outcome.isError)
|
|
110
|
+
this.erroredIds.add(toolCallId);
|
|
111
|
+
this.pending.delete(toolCallId);
|
|
112
|
+
parked.resolve(outcome);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Not parked yet. Buffer for the imminent `park` iff the coordinator
|
|
116
|
+
// surfaced this id this turn; a never-seen id is genuine misuse.
|
|
117
|
+
if (this.emitted.has(toolCallId)) {
|
|
118
|
+
this.settled.add(toolCallId);
|
|
119
|
+
if (outcome.isError)
|
|
120
|
+
this.erroredIds.add(toolCallId);
|
|
121
|
+
this.preSubmitted.set(toolCallId, outcome);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
throw new AgentSDKError(`No parked consumer tool call for id "${toolCallId}".`, AgentSDKErrorType.TOOL_CALL_NOT_FOUND);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The consumer-tool call ids settled with `isError: true` this turn — the
|
|
128
|
+
* records the coordinator stamps `isError` on after the run settles, since
|
|
129
|
+
* the run loop persists the `function_call_result` without it.
|
|
130
|
+
*/
|
|
131
|
+
erroredToolCallIds() {
|
|
132
|
+
return [...this.erroredIds];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Reject every parked call on teardown so the run loop unblocks instead of
|
|
136
|
+
* hanging on a promise that will never settle. Marks all pending ids settled
|
|
137
|
+
* so a late consumer `submitToolResult` is a silent no-op (#589).
|
|
138
|
+
*/
|
|
139
|
+
rejectAll(error) {
|
|
140
|
+
this.tornDown = true;
|
|
141
|
+
this.teardownError = error;
|
|
142
|
+
for (const [toolCallId, parked] of this.pending) {
|
|
143
|
+
this.settled.add(toolCallId);
|
|
144
|
+
// Reject the parked execute so the SDK run loop unblocks and the pump's
|
|
145
|
+
// catch synthesizes the terminal error + finish pair. (A normal settle
|
|
146
|
+
// resolves; only teardown rejects.)
|
|
147
|
+
parked.reject(error);
|
|
148
|
+
}
|
|
149
|
+
this.pending.clear();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Map an agent's consumer-tool `ToolDefinition[]` to `@openai/agents` function
|
|
154
|
+
* tools whose `execute` parks on the shared {@link ConsumerToolRegistry}. Every
|
|
155
|
+
* tool gets `needsApproval: false` — consumer tools are the consumer's
|
|
156
|
+
* responsibility via `submitToolResult` and are NEVER approval-gated (the
|
|
157
|
+
* dual-enforcement contract: the coordinator also short-circuits them by name
|
|
158
|
+
* before consulting the policy resolver).
|
|
159
|
+
*
|
|
160
|
+
* The mapped `execute` reads its own `toolCallId` from `details.toolCall.callId`
|
|
161
|
+
* (populated by the SDK before invocation) and parks under it; the returned
|
|
162
|
+
* value becomes the tool's output the model sees.
|
|
163
|
+
*
|
|
164
|
+
* When `redaction` is supplied (the agent has an `onToolResult` hook), the
|
|
165
|
+
* parked result passes through the redactor at this seam — before it becomes the
|
|
166
|
+
* model-visible output — with full fidelity (`agentId` / `threadId` /
|
|
167
|
+
* `toolCallId` / `toolName` / `serverName` all known). A redactor throw
|
|
168
|
+
* propagates out of `execute` (the pump's `catch` synthesizes the terminal
|
|
169
|
+
* error + finish pair), per the SDK's throws-propagate contract.
|
|
170
|
+
*/
|
|
171
|
+
export function mapConsumerTools(defs, registry, redaction) {
|
|
172
|
+
return defs.map((def) => {
|
|
173
|
+
const execute = async (_input, _context, details) => {
|
|
174
|
+
const toolCallId = details?.toolCall?.callId;
|
|
175
|
+
if (toolCallId === undefined) {
|
|
176
|
+
// Without a callId the harness can't pair a `submitToolResult`, so
|
|
177
|
+
// fail the call rather than park forever.
|
|
178
|
+
throw new AgentSDKError(`Consumer tool "${def.name}" was invoked without a tool call id.`, AgentSDKErrorType.TOOL_CALL_NOT_FOUND);
|
|
179
|
+
}
|
|
180
|
+
const { result, isError } = await registry.park(toolCallId);
|
|
181
|
+
// A consumer-reported failure must reach the model as a recoverable
|
|
182
|
+
// tool output (the `isError` contract), not throw. The gateway's
|
|
183
|
+
// `function_call_result` has no `isError` field, so the signal rides
|
|
184
|
+
// in the output text; the harness persists the structured `isError`
|
|
185
|
+
// on the session record for history (#647).
|
|
186
|
+
const modelVisible = isError ? errorOutput(result) : result;
|
|
187
|
+
if (redaction === undefined)
|
|
188
|
+
return modelVisible;
|
|
189
|
+
// Redact the model-visible value in place. `output` is what the model
|
|
190
|
+
// would otherwise see (error-prefixed on the failure path), so the
|
|
191
|
+
// redactor scrubs the same string that reaches the transcript.
|
|
192
|
+
return applyRedaction(redaction, {
|
|
193
|
+
toolCallId,
|
|
194
|
+
toolName: def.name,
|
|
195
|
+
output: modelVisible,
|
|
196
|
+
isError: isError === true,
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
// A consumer `inputSchema` is an arbitrary JSON schema. `strict: false`
|
|
200
|
+
// selects the non-strict tool branch (Zod-only schemas need `strict: true`);
|
|
201
|
+
// the options are cast as one object because a consumer's free-form schema
|
|
202
|
+
// won't structurally match the SDK's `JsonObjectSchemaNonStrict` generic and
|
|
203
|
+
// the execute arg is `unknown` on the non-strict branch. Consumer tools are
|
|
204
|
+
// never gated (`needsApproval: false`) — they run client-side by definition.
|
|
205
|
+
return tool({
|
|
206
|
+
name: def.name,
|
|
207
|
+
description: def.description ?? '',
|
|
208
|
+
parameters: def.inputSchema,
|
|
209
|
+
strict: false,
|
|
210
|
+
needsApproval: false,
|
|
211
|
+
execute,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Shape a consumer-reported error outcome into a model-visible tool output. The
|
|
217
|
+
* consumer's `result` is passed through when it is already a string/serializable
|
|
218
|
+
* value; an `Error` becomes its message. Prefixed so the model reads it as a
|
|
219
|
+
* failure it can recover from rather than a normal result.
|
|
220
|
+
*/
|
|
221
|
+
function errorOutput(result) {
|
|
222
|
+
const detail = result instanceof Error
|
|
223
|
+
? result.message
|
|
224
|
+
: typeof result === 'string'
|
|
225
|
+
? result
|
|
226
|
+
: result === undefined
|
|
227
|
+
? 'unknown error'
|
|
228
|
+
: JSON.stringify(result);
|
|
229
|
+
return `Tool execution failed: ${detail}`;
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=openai-tool-mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-tool-mapper.js","sourceRoot":"","sources":["../src/openai-tool-mapper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAa,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAuB,MAAM,4BAA4B,CAAC;AACnG,OAAO,EAAE,cAAc,EAAyB,MAAM,4BAA4B,CAAC;AAKnF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,oBAAoB;IAsCR;IArCrB,gEAAgE;IAC/C,OAAO,GAAG,IAAI,GAAG,EAG/B,CAAC;IACJ;;;;;;;OAOG;IACc,YAAY,GAAG,IAAI,GAAG,EAA+B,CAAC;IACvE,oFAAoF;IACnE,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAC7C;;;;;;OAMG;IACc,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IAChD;;;;;OAKG;IACc,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAC7C,+FAA+F;IACvF,QAAQ,GAAG,KAAK,CAAC;IACzB,uDAAuD;IAC/C,aAAa,CAAS;IAE9B,+FAA+F;IAC/F,YAAqB,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAEvD;;;;;;;;;;OAUG;IACH,IAAI,CAAC,UAAkB;QACnB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,IAAI,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC;QACzF,0EAA0E;QAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACtB,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACrC,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,IAAI,OAAO,CAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,UAAkB;QAC1B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACjC,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,UAAkB,EAAE,OAA4B;QACnD,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;YAAE,OAAO;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC7B,IAAI,OAAO,CAAC,OAAO;gBAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAChC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACxB,OAAO;QACX,CAAC;QACD,qEAAqE;QACrE,iEAAiE;QACjE,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC7B,IAAI,OAAO,CAAC,OAAO;gBAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC3C,OAAO;QACX,CAAC;QACD,MAAM,IAAI,aAAa,CACnB,wCAAwC,UAAU,IAAI,EACtD,iBAAiB,CAAC,mBAAmB,CACxC,CAAC;IACN,CAAC;IAED;;;;OAIG;IACH,kBAAkB;QACd,OAAO,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,KAAY;QAClB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3B,KAAK,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC9C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC7B,wEAAwE;YACxE,uEAAuE;YACvE,oCAAoC;YACpC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;CACJ;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,gBAAgB,CAC5B,IAAsB,EACtB,QAA8B,EAC9B,SAA4B;IAE5B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACpB,MAAM,OAAO,GAAG,KAAK,EACjB,MAAe,EACf,QAAiB,EACjB,OAA4C,EAC5B,EAAE;YAClB,MAAM,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC;YAC7C,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;gBAC3B,mEAAmE;gBACnE,0CAA0C;gBAC1C,MAAM,IAAI,aAAa,CACnB,kBAAkB,GAAG,CAAC,IAAI,uCAAuC,EACjE,iBAAiB,CAAC,mBAAmB,CACxC,CAAC;YACN,CAAC;YACD,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC5D,oEAAoE;YACpE,iEAAiE;YACjE,qEAAqE;YACrE,oEAAoE;YACpE,4CAA4C;YAC5C,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAC5D,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAO,YAAY,CAAC;YACjD,sEAAsE;YACtE,mEAAmE;YACnE,+DAA+D;YAC/D,OAAO,cAAc,CAAC,SAAS,EAAE;gBAC7B,UAAU;gBACV,QAAQ,EAAE,GAAG,CAAC,IAAI;gBAClB,MAAM,EAAE,YAAY;gBACpB,OAAO,EAAE,OAAO,KAAK,IAAI;aAC5B,CAAC,CAAC;QACP,CAAC,CAAC;QACF,wEAAwE;QACxE,6EAA6E;QAC7E,2EAA2E;QAC3E,6EAA6E;QAC7E,4EAA4E;QAC5E,6EAA6E;QAC7E,OAAO,IAAI,CAAC;YACR,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,EAAE;YAClC,UAAU,EAAE,GAAG,CAAC,WAAW;YAC3B,MAAM,EAAE,KAAK;YACb,aAAa,EAAE,KAAK;YACpB,OAAO;SAC+B,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,MAAe;IAChC,MAAM,MAAM,GACR,MAAM,YAAY,KAAK;QACnB,CAAC,CAAC,MAAM,CAAC,OAAO;QAChB,CAAC,CAAC,OAAO,MAAM,KAAK,QAAQ;YAC1B,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,MAAM,KAAK,SAAS;gBACpB,CAAC,CAAC,eAAe;gBACjB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACrC,OAAO,0BAA0B,MAAM,EAAE,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type MCPServer, type Tool } from '@openai/agents';
|
|
2
|
+
import type { ToolResultRedactor } from '@salesforce/sfdx-agent-sdk';
|
|
3
|
+
import type { McpCatalogEntry } from './openai-mcp-state.js';
|
|
4
|
+
/**
|
|
5
|
+
* Everything a redaction seam needs to call the consumer's
|
|
6
|
+
* {@link ToolResultRedactor} with a faithful {@link ToolResultRedactionInput}.
|
|
7
|
+
* Built per-turn in `stream()` (so `threadId` is stable across approval
|
|
8
|
+
* resumes) and threaded into both the consumer-tool `execute` seam and the MCP
|
|
9
|
+
* `invoke`-wrap seam. `undefined` when the agent has no `onToolResult` hook —
|
|
10
|
+
* both seams then take their pass-through path with zero overhead.
|
|
11
|
+
*/
|
|
12
|
+
export type RedactionContext = {
|
|
13
|
+
readonly redactor: ToolResultRedactor;
|
|
14
|
+
readonly agentId: string;
|
|
15
|
+
readonly threadId: string;
|
|
16
|
+
/** Bare-tool-name → `{ serverName, annotations }`; supplies `serverName` for MCP tools. */
|
|
17
|
+
readonly mcpCatalog: ReadonlyMap<string, McpCatalogEntry>;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Run the consumer's redactor over one tool result and return the value the
|
|
21
|
+
* model should see. `{ output }` replaces; `undefined` (or a malformed return)
|
|
22
|
+
* passes the original through. **Throws propagate** — the SDK contract requires
|
|
23
|
+
* a redactor exception to ride the harness's native error path so the original
|
|
24
|
+
* value never reaches the model; callers MUST NOT catch it.
|
|
25
|
+
*
|
|
26
|
+
* `serverName` is looked up from the catalog (populated for MCP tools, absent
|
|
27
|
+
* for consumer / built-in tools, matching the cross-harness `tool-result`
|
|
28
|
+
* enrichment shape).
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyRedaction(ctx: RedactionContext, args: {
|
|
31
|
+
toolCallId: string;
|
|
32
|
+
toolName: string;
|
|
33
|
+
output: unknown;
|
|
34
|
+
isError: boolean;
|
|
35
|
+
}): Promise<unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Materialize an agent's MCP tools as function tools (via {@link getAllMcpTools},
|
|
38
|
+
* the same call the SDK makes internally for a native `Agent({ mcpServers })`
|
|
39
|
+
* attach) and wrap each one's `invoke` with a redaction shim. Used only when the
|
|
40
|
+
* agent has an `onToolResult` hook — otherwise the harness keeps the native
|
|
41
|
+
* `mcpServers` attach unchanged (the #541-proven path).
|
|
42
|
+
*
|
|
43
|
+
* The `@openai/agents` MCP attach exposes no per-result rewrite hook, so owning
|
|
44
|
+
* the function-tool `invoke` is the only model-visible seam that also carries
|
|
45
|
+
* the `toolCallId` (`details.toolCall.callId`). The shim closure-captures the
|
|
46
|
+
* {@link RedactionContext}, so `threadId` / `agentId` need no `RunContext`
|
|
47
|
+
* plumbing.
|
|
48
|
+
*
|
|
49
|
+
* **#541 is preserved:** `getAllMcpTools` calls `listTools()` per server, which
|
|
50
|
+
* returns the warm instance cache (`cacheToolsList: true`) with no network
|
|
51
|
+
* `tools/list`. **`isError` caveat:** the wrapped `invoke` sees only the
|
|
52
|
+
* model-visible string output, not the SDK's internal error flag, so MCP
|
|
53
|
+
* `isError` is best-effort (duck-typed) — matching Claude's `PostToolUse`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildRedactedMcpTools(servers: MCPServer[], ctx: RedactionContext): Promise<Tool[]>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { getAllMcpTools } from '@openai/agents';
|
|
6
|
+
/**
|
|
7
|
+
* Run the consumer's redactor over one tool result and return the value the
|
|
8
|
+
* model should see. `{ output }` replaces; `undefined` (or a malformed return)
|
|
9
|
+
* passes the original through. **Throws propagate** — the SDK contract requires
|
|
10
|
+
* a redactor exception to ride the harness's native error path so the original
|
|
11
|
+
* value never reaches the model; callers MUST NOT catch it.
|
|
12
|
+
*
|
|
13
|
+
* `serverName` is looked up from the catalog (populated for MCP tools, absent
|
|
14
|
+
* for consumer / built-in tools, matching the cross-harness `tool-result`
|
|
15
|
+
* enrichment shape).
|
|
16
|
+
*/
|
|
17
|
+
export async function applyRedaction(ctx, args) {
|
|
18
|
+
const serverName = ctx.mcpCatalog.get(args.toolName)?.serverName;
|
|
19
|
+
const result = await ctx.redactor({
|
|
20
|
+
agentId: ctx.agentId,
|
|
21
|
+
threadId: ctx.threadId,
|
|
22
|
+
toolCallId: args.toolCallId,
|
|
23
|
+
toolName: args.toolName,
|
|
24
|
+
...(serverName !== undefined ? { serverName } : {}),
|
|
25
|
+
output: args.output,
|
|
26
|
+
isError: args.isError,
|
|
27
|
+
});
|
|
28
|
+
// `{ output }` replaces; anything else (incl. `undefined`) passes through.
|
|
29
|
+
return result && typeof result === 'object' && 'output' in result ? result.output : args.output;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Materialize an agent's MCP tools as function tools (via {@link getAllMcpTools},
|
|
33
|
+
* the same call the SDK makes internally for a native `Agent({ mcpServers })`
|
|
34
|
+
* attach) and wrap each one's `invoke` with a redaction shim. Used only when the
|
|
35
|
+
* agent has an `onToolResult` hook — otherwise the harness keeps the native
|
|
36
|
+
* `mcpServers` attach unchanged (the #541-proven path).
|
|
37
|
+
*
|
|
38
|
+
* The `@openai/agents` MCP attach exposes no per-result rewrite hook, so owning
|
|
39
|
+
* the function-tool `invoke` is the only model-visible seam that also carries
|
|
40
|
+
* the `toolCallId` (`details.toolCall.callId`). The shim closure-captures the
|
|
41
|
+
* {@link RedactionContext}, so `threadId` / `agentId` need no `RunContext`
|
|
42
|
+
* plumbing.
|
|
43
|
+
*
|
|
44
|
+
* **#541 is preserved:** `getAllMcpTools` calls `listTools()` per server, which
|
|
45
|
+
* returns the warm instance cache (`cacheToolsList: true`) with no network
|
|
46
|
+
* `tools/list`. **`isError` caveat:** the wrapped `invoke` sees only the
|
|
47
|
+
* model-visible string output, not the SDK's internal error flag, so MCP
|
|
48
|
+
* `isError` is best-effort (duck-typed) — matching Claude's `PostToolUse`.
|
|
49
|
+
*/
|
|
50
|
+
export async function buildRedactedMcpTools(servers, ctx) {
|
|
51
|
+
const tools = await getAllMcpTools({ mcpServers: servers });
|
|
52
|
+
return tools.map((tool) => (tool.type === 'function' ? wrapFunctionToolInvoke(tool, ctx) : tool));
|
|
53
|
+
}
|
|
54
|
+
/** Wrap a function tool's `invoke` so its result passes through the redactor before the model sees it. */
|
|
55
|
+
function wrapFunctionToolInvoke(tool, ctx) {
|
|
56
|
+
const originalInvoke = tool.invoke.bind(tool);
|
|
57
|
+
return {
|
|
58
|
+
...tool,
|
|
59
|
+
invoke: async (runContext, input, details) => {
|
|
60
|
+
const output = await originalInvoke(runContext, input, details);
|
|
61
|
+
const toolCallId = details?.toolCall?.callId;
|
|
62
|
+
// Without a callId the redaction input can't be faithfully attributed;
|
|
63
|
+
// still redact (the redactor keys on toolName/output), passing an empty
|
|
64
|
+
// id rather than dropping the hook.
|
|
65
|
+
const redacted = await applyRedaction(ctx, {
|
|
66
|
+
toolCallId: toolCallId ?? '',
|
|
67
|
+
toolName: tool.name,
|
|
68
|
+
output,
|
|
69
|
+
isError: isErrorOutput(output),
|
|
70
|
+
});
|
|
71
|
+
// `invoke` must return `string | Result`; a redactor may return any
|
|
72
|
+
// shape (the consumer owns matching the tool's expected shape, per the
|
|
73
|
+
// ToolResultRedactor contract — the harness does not validate it).
|
|
74
|
+
return redacted;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Best-effort MCP error detection: the model-visible output carries no structured flag, so duck-type it. */
|
|
79
|
+
function isErrorOutput(output) {
|
|
80
|
+
return typeof output === 'object' && output !== null && output.isError === true;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=openai-tool-redaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-tool-redaction.js","sourceRoot":"","sources":["../src/openai-tool-redaction.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAA6B,MAAM,gBAAgB,CAAC;AAoB3E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAChC,GAAqB,EACrB,IAAiF;IAEjF,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC;IACjE,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC;QAC9B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;KACxB,CAAC,CAAC;IACH,2EAA2E;IAC3E,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;AACpG,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAoB,EAAE,GAAqB;IACnF,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;IAC5D,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACtG,CAAC;AAED,0GAA0G;AAC1G,SAAS,sBAAsB,CAAC,IAAyC,EAAE,GAAqB;IAC5F,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO;QACH,GAAG,IAAI;QACP,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACzC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAChE,MAAM,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC;YAC7C,uEAAuE;YACvE,wEAAwE;YACxE,oCAAoC;YACpC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE;gBACvC,UAAU,EAAE,UAAU,IAAI,EAAE;gBAC5B,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,MAAM;gBACN,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC;aACjC,CAAC,CAAC;YACH,oEAAoE;YACpE,uEAAuE;YACvE,mEAAmE;YACnE,OAAO,QAAkB,CAAC;QAC9B,CAAC;KACJ,CAAC;AACN,CAAC;AAED,6GAA6G;AAC7G,SAAS,aAAa,CAAC,MAAe;IAClC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAK,MAAgC,CAAC,OAAO,KAAK,IAAI,CAAC;AAC/G,CAAC"}
|