@salesforce/sfdx-agent-harness-openai 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -8
- package/README.md +15 -4
- package/dist/openai-agents-harness.d.ts +21 -11
- package/dist/openai-agents-harness.js +164 -78
- package/dist/openai-agents-harness.js.map +1 -1
- package/dist/openai-approval-coordinator.d.ts +66 -18
- package/dist/openai-approval-coordinator.js +127 -28
- package/dist/openai-approval-coordinator.js.map +1 -1
- package/dist/openai-built-in-policies.d.ts +4 -3
- package/dist/openai-built-in-policies.js +4 -3
- package/dist/openai-built-in-policies.js.map +1 -1
- package/dist/openai-event-adapter.d.ts +8 -3
- package/dist/openai-event-adapter.js +9 -4
- package/dist/openai-event-adapter.js.map +1 -1
- package/dist/openai-mcp-state.d.ts +22 -7
- package/dist/openai-message-mapper.d.ts +58 -9
- package/dist/openai-message-mapper.js +137 -12
- package/dist/openai-message-mapper.js.map +1 -1
- package/dist/openai-skill-tools.d.ts +36 -0
- package/dist/openai-skill-tools.js +209 -0
- package/dist/openai-skill-tools.js.map +1 -0
- package/dist/openai-tool-redaction.d.ts +44 -10
- package/dist/openai-tool-redaction.js +77 -29
- package/dist/openai-tool-redaction.js.map +1 -1
- package/dist/rule-composer.d.ts +35 -0
- package/dist/rule-composer.js +88 -0
- package/dist/rule-composer.js.map +1 -0
- package/dist/skill-loader.d.ts +76 -0
- package/dist/skill-loader.js +180 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/test/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,21 +3,48 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-harness-openai` are documented in this file. Format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.2.0] - 2026-07-27
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **chat-generations**: GovCloud chat/generations fallback @W-23560954@ ([#695](https://github.com/forcedotcom/agentic-dx/pull/695))
|
|
10
|
+
- **harness-openai**: multimodal image/file input support @W-23546665 ([#702](https://github.com/forcedotcom/agentic-dx/pull/702))
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-07-27
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **harness-openai**: skills bridge (AgentConfig.skills + multi-file skills) @W-23546666
|
|
17
|
+
([#694](https://github.com/forcedotcom/agentic-dx/pull/694))
|
|
18
|
+
- **harness-openai**: rules loader (AgentConfig.rules support) @W-23546667@
|
|
19
|
+
([#689](https://github.com/forcedotcom/agentic-dx/pull/689))
|
|
20
|
+
- **harness-openai**: MCP-tool approval gating @W-23546664@ ([#688](https://github.com/forcedotcom/agentic-dx/pull/688))
|
|
21
|
+
|
|
6
22
|
## [0.0.1] - 2026-07-22
|
|
7
23
|
|
|
8
24
|
### Features
|
|
9
|
-
|
|
10
|
-
- **harness-openai**:
|
|
11
|
-
|
|
12
|
-
- **harness-openai**:
|
|
13
|
-
|
|
14
|
-
- **harness-openai**:
|
|
25
|
+
|
|
26
|
+
- **harness-openai**: full conformance gate + e2e closeout @W-23447579@
|
|
27
|
+
([#684](https://github.com/forcedotcom/agentic-dx/pull/684))
|
|
28
|
+
- **harness-openai**: compaction, telemetry, hooks @W-23447576@
|
|
29
|
+
([#683](https://github.com/forcedotcom/agentic-dx/pull/683))
|
|
30
|
+
- **harness-openai**: MCP lifecycle + updateAgent preservation @W-23447574@
|
|
31
|
+
([#678](https://github.com/forcedotcom/agentic-dx/pull/678))
|
|
32
|
+
- **harness-openai**: approval + consumer tools @W-23447572@
|
|
33
|
+
([#677](https://github.com/forcedotcom/agentic-dx/pull/677))
|
|
34
|
+
- **harness-openai**: sessions, tools, history + isError sidecar @W-23447571@
|
|
35
|
+
([#676](https://github.com/forcedotcom/agentic-dx/pull/676))
|
|
36
|
+
- **harness-openai**: scaffold OpenAI Agents SDK harness with connectivity + streamed turn @W-23447570@
|
|
37
|
+
([#675](https://github.com/forcedotcom/agentic-dx/pull/675))
|
|
15
38
|
|
|
16
39
|
### Fixes
|
|
17
|
-
|
|
40
|
+
|
|
41
|
+
- **ci**: bump openai harness off seed version so it can publish @W-23447579@
|
|
42
|
+
([#686](https://github.com/forcedotcom/agentic-dx/pull/686))
|
|
18
43
|
|
|
19
44
|
### Chores
|
|
20
|
-
|
|
45
|
+
|
|
46
|
+
- **deps-dev**: bump eslint from 10.6.0 to 10.7.0 in the eslint group
|
|
47
|
+
([#680](https://github.com/forcedotcom/agentic-dx/pull/680))
|
|
21
48
|
|
|
22
49
|
## [Unreleased]
|
|
23
50
|
|
package/README.md
CHANGED
|
@@ -10,10 +10,21 @@ OpenAI Agents SDK-backed `AgentHarness` implementation for [`@salesforce/sfdx-ag
|
|
|
10
10
|
> lifecycle + `updateAgent` preservation (#541), E's compaction / telemetry / hooks, and F's full conformance-gate + e2e
|
|
11
11
|
> closeout. Every `AgentHarness` method has a working implementation, the shared cross-harness conformance gate is
|
|
12
12
|
> green, and the harness passes the SDK's `describe.each` e2e matrix under `--harness openai` against the real gateway.
|
|
13
|
-
> **
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
13
|
+
> **MCP-tool approval gating IS supported** (added post-F): a `require-approval` / `deny` policy on an MCP tool surfaces
|
|
14
|
+
> a `tool-approval-request` and honors the parked-approval lifecycle. When a policy gates MCP tools (or a redaction hook
|
|
15
|
+
> is set), the harness materializes MCP tools via `getAllMcpTools(...)` and stamps a `needsApproval` predicate on each,
|
|
16
|
+
> so the run loop raises the interruption the coordinator already routes. Consumer-tool approval + `submitToolResult`,
|
|
17
|
+
> and tool-result redaction, are likewise supported. **`AgentConfig.rules` composition IS supported** (added post-F,
|
|
18
|
+
> W-23546667): `createAgent` / `updateAgent` load each rule file/directory, strip YAML frontmatter, and compose the
|
|
19
|
+
> bodies onto the effective system prompt — so the harness rejoins the dual-harness rules e2e. **`AgentConfig.skills`
|
|
20
|
+
> composition IS supported** (added post-F, W-23546666): `createAgent` / `updateAgent` load `config.skills` into a skill
|
|
21
|
+
> catalog exposed to the model as `load_skill` / `read_skill_file` function tools (the latter serving a skill's sibling
|
|
22
|
+
> files), so the harness rejoins the dual-harness skills e2e including multi-file skill reachability. **Multimodal
|
|
23
|
+
> image/file input IS supported** (added post-F, W-23546665): `chat()` / `stream()` and `addContext` accept `image` /
|
|
24
|
+
> `file` `MessagePart`s and round-trip them to the model, so the harness rejoined the multimodal e2e matrix. **Known
|
|
25
|
+
> deferred gap** (excluded from the e2e matrix with a documented rationale, not a silent skip): MCP **`tool-progress`**
|
|
26
|
+
> — blocked UPSTREAM (`@openai/agents` 0.13.x exposes no seam to reach MCP `notifications/progress`) and tracked as
|
|
27
|
+
> W-23558557, not a harness gap.
|
|
17
28
|
|
|
18
29
|
## What it is
|
|
19
30
|
|
|
@@ -179,19 +179,20 @@ export declare class OpenAIAgentsHarness implements OpenAIAgentsAgentHarness {
|
|
|
179
179
|
* request) with tracing off. `tools` carries the turn's consumer-executed
|
|
180
180
|
* tools (mapped from `config.tools`).
|
|
181
181
|
*
|
|
182
|
-
* **MCP attach, two paths.**
|
|
183
|
-
*
|
|
184
|
-
* via the native `mcpServers` field; the SDK fetches their tools per
|
|
185
|
-
* Because each instance has `cacheToolsList: true` (cache keyed by
|
|
182
|
+
* **MCP attach, two paths.** With neither a redaction hook nor an active
|
|
183
|
+
* approval gate (the common case), the agent's live MCP server instances
|
|
184
|
+
* attach via the native `mcpServers` field; the SDK fetches their tools per
|
|
185
|
+
* run. Because each instance has `cacheToolsList: true` (cache keyed by
|
|
186
186
|
* `server.name`) and this harness reuses the SAME connected instances across
|
|
187
187
|
* turns, a rebuilt `Agent` here does not re-run `tools/list` for a preserved
|
|
188
|
-
* server — the #541 preservation mechanic.
|
|
189
|
-
* this milestone (the native attach exposes no per-tool `needsApproval`).
|
|
188
|
+
* server — the #541 preservation mechanic.
|
|
190
189
|
*
|
|
191
|
-
* **When a redaction hook is set,** the native attach
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
190
|
+
* **When a redaction hook OR an approval gate is set,** the native attach
|
|
191
|
+
* exposes neither a per-result rewrite seam nor a per-tool `needsApproval`
|
|
192
|
+
* seam, so MCP tools are materialized via `getAllMcpTools(...)` and rebuilt
|
|
193
|
+
* with the active hooks (see {@link buildManagedMcpTools}): a redaction shim
|
|
194
|
+
* on `invoke` and/or a `needsApproval` predicate driven by the policy. They
|
|
195
|
+
* attach as `tools` rather than `mcpServers`. #541 is preserved because
|
|
195
196
|
* `getAllMcpTools` reads each server's warm `cacheToolsList` cache with no
|
|
196
197
|
* network `tools/list`. This makes the method async (the materialize awaits
|
|
197
198
|
* `listTools()`), so `stream()` awaits it.
|
|
@@ -274,7 +275,16 @@ export declare class OpenAIAgentsHarness implements OpenAIAgentsAgentHarness {
|
|
|
274
275
|
private diffMcpServers;
|
|
275
276
|
/** Close every MCP server on an agent, awaiting in-flight discovery first. */
|
|
276
277
|
private closeMcpServers;
|
|
277
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* Rebuild the catalog entries for one server from its freshly-discovered
|
|
280
|
+
* tools. Keys each entry by the tool's **sanitized display name** (what
|
|
281
|
+
* `@openai/agents` registers on materialized tools and run-item events — see
|
|
282
|
+
* {@link toFunctionToolName}), while preserving the original bare MCP name as
|
|
283
|
+
* `toolName`. Keying by the raw name would miss enrichment for any tool whose
|
|
284
|
+
* name carries a character the SDK sanitizes to `_` (e.g. `get-sum` →
|
|
285
|
+
* `get_sum`), so neither the event adapter nor the approval gate would see
|
|
286
|
+
* its `serverName`.
|
|
287
|
+
*/
|
|
278
288
|
private refreshCatalogForServer;
|
|
279
289
|
/** Drop every catalog entry that points at `serverName`. */
|
|
280
290
|
private removeCatalogForServer;
|
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { backfillCreatedAt, buildSummaryPrompt, getErrorMessage, isAbortError, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
6
6
|
import { Agent, Runner } from '@openai/agents';
|
|
7
|
-
import { AgentSDKError, AgentSDKErrorType, mcpServerConfigEqual, McpServerStatus, resolveToolApprovalPolicy, } from '@salesforce/sfdx-agent-sdk';
|
|
8
|
-
import { HarnessBusOwner, SUPPORTED_PROTOCOL_VERSIONS } from '@salesforce/sfdx-agent-sdk/harness';
|
|
7
|
+
import { AgentSDKError, AgentSDKErrorType, mcpServerConfigEqual, McpServerStatus, resolveToolApprovalPolicy, validateMultimodalFiles, } from '@salesforce/sfdx-agent-sdk';
|
|
8
|
+
import { HarnessBusOwner, lowerStreamInput, SUPPORTED_PROTOCOL_VERSIONS } from '@salesforce/sfdx-agent-sdk/harness';
|
|
9
9
|
import { OpenAIApprovalCoordinator } from './openai-approval-coordinator.js';
|
|
10
10
|
import { OPENAI_BUILT_IN_TOOL_POLICIES } from './openai-built-in-policies.js';
|
|
11
11
|
import { buildMcpErrorDetail, sanitizeMcpErrorMessage } from './mcp-error-classifier.js';
|
|
12
12
|
import { hasEnabledServers, mapToOpenAIMcpServers } from './openai-mcp-config-mapper.js';
|
|
13
|
-
import { MAX_TRANSCRIPT_CHARS, messagesToRecords, partToTranscriptText, recordsToMessages, splitIntoChunks, } from './openai-message-mapper.js';
|
|
13
|
+
import { lowerInputPartToContent, MAX_TRANSCRIPT_CHARS, messagesToRecords, partToTranscriptText, recordsToMessages, splitIntoChunks, } from './openai-message-mapper.js';
|
|
14
14
|
import { buildOpenAIModelProvider } from './openai-model-provider.js';
|
|
15
|
+
import { composeRulesAppend, composeSystemPrompt } from './rule-composer.js';
|
|
15
16
|
import { OpenAISessionStore } from './openai-session-store.js';
|
|
17
|
+
import { buildSkillMap } from './skill-loader.js';
|
|
18
|
+
import { buildSkillTools } from './openai-skill-tools.js';
|
|
16
19
|
import { ConsumerToolRegistry, mapConsumerTools } from './openai-tool-mapper.js';
|
|
17
|
-
import {
|
|
20
|
+
import { buildManagedMcpTools } from './openai-tool-redaction.js';
|
|
18
21
|
/**
|
|
19
22
|
* OpenAI Agents SDK-backed implementation of the SDK's `AgentHarness` contract.
|
|
20
23
|
*
|
|
@@ -119,11 +122,21 @@ export class OpenAIAgentsHarness {
|
|
|
119
122
|
if (this.agents.has(agentId)) {
|
|
120
123
|
throw new Error(`Agent with id "${agentId}" is already registered`);
|
|
121
124
|
}
|
|
125
|
+
options?.abortSignal?.throwIfAborted();
|
|
126
|
+
// Compose `config.rules` and load `config.skills` BEFORE registering
|
|
127
|
+
// state or starting MCP servers, so a bad/unreadable rule or skill path
|
|
128
|
+
// rejects `createAgent` before any MCP client is constructed (matching
|
|
129
|
+
// Mastra's Phase-0 posture — a non-MCP input failure can't leave servers
|
|
130
|
+
// half-cycled).
|
|
131
|
+
const rulesAppend = await composeRulesAppend(config?.rules);
|
|
132
|
+
const skillMap = await buildSkillMap(config?.skills, projectRoot, this.buses.getLogBus());
|
|
122
133
|
const state = {
|
|
123
134
|
projectRoot,
|
|
124
135
|
config: config ?? {},
|
|
125
136
|
modelConnectivityInfo,
|
|
126
137
|
hooks: options?.hooks ?? {},
|
|
138
|
+
rulesAppend,
|
|
139
|
+
skillMap,
|
|
127
140
|
// The provider reads the live bag on every `getModel()` so a
|
|
128
141
|
// within-shape modelId swap / JWT rotation lands without a rebuild.
|
|
129
142
|
modelProvider: this.modelProviderFactory(() => this.getAgentOrThrow(agentId).modelConnectivityInfo),
|
|
@@ -218,7 +231,29 @@ export class OpenAIAgentsHarness {
|
|
|
218
231
|
// fresh id must still persist under that thread rather than silently
|
|
219
232
|
// dropping history.
|
|
220
233
|
state.threads.add(threadId);
|
|
221
|
-
|
|
234
|
+
// Debug-emit the composed system prompt (config.instructions, with any
|
|
235
|
+
// rule bodies folded in by composeSystemPrompt) so consumers — notably
|
|
236
|
+
// the dual-harness rules e2e (#493) — can deterministically assert that
|
|
237
|
+
// `AgentConfig.rules` / `AgentConfig.instructions` reached the underlying
|
|
238
|
+
// runtime, without model-output marker assertions. The Mastra and Claude
|
|
239
|
+
// harnesses emit the equivalent `assembled systemPrompt` record from
|
|
240
|
+
// their own `stream()`, so the e2e captures the same record shape on
|
|
241
|
+
// every harness. Skip when the assembled body is empty (no instructions,
|
|
242
|
+
// no rules) so the emit guard matches the sibling harnesses' "skip when
|
|
243
|
+
// no system prompt is configured" posture.
|
|
244
|
+
const composedSystemPrompt = composeSystemPrompt(state.config.instructions, state.rulesAppend);
|
|
245
|
+
if (composedSystemPrompt.length > 0) {
|
|
246
|
+
this.buses.getLogBus()?.debug('openai harness assembled systemPrompt', {
|
|
247
|
+
agentId,
|
|
248
|
+
threadId,
|
|
249
|
+
systemPrompt: composedSystemPrompt,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Lower the user message to the run-loop input. A plain string passes
|
|
253
|
+
// through; a `MessagePart[]` runs the shared `lowerStreamInput` (file-cap
|
|
254
|
+
// validation against the live model's `supportedFormats` + the input-part
|
|
255
|
+
// guard) and lowers text/image/file parts to one multimodal user item.
|
|
256
|
+
const input = lowerMessage(message, state.modelConnectivityInfo.model);
|
|
222
257
|
// The disk-backed Session the runner reads history from and appends new
|
|
223
258
|
// items to. Passing it to `run(..., { session })` is what makes the turn
|
|
224
259
|
// multi-turn-aware and persistent (the runner owns the write).
|
|
@@ -227,13 +262,10 @@ export class OpenAIAgentsHarness {
|
|
|
227
262
|
// interruption, or `undefined` when gating is off for this turn (the
|
|
228
263
|
// SDK's "no policy ⇒ no gating" back-compat path). Frozen at stream start;
|
|
229
264
|
// the closure reads `state.config` live per call so a mid-turn
|
|
230
|
-
// `updateAgentConfig` ("Allow always") is honored.
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// (gating is a deferred follow-up). This decider is therefore dormant in
|
|
235
|
-
// production, though the coordinator plumbing it drives is exercised by
|
|
236
|
-
// the coordinator unit tests.
|
|
265
|
+
// `updateAgentConfig` ("Allow always") is honored. When active, the same
|
|
266
|
+
// decider also drives the `needsApproval` predicate on materialized MCP
|
|
267
|
+
// tools (below), so a `require-approval` / `deny` MCP tool raises the
|
|
268
|
+
// interruption this decider then routes.
|
|
237
269
|
const policy = this.buildPolicyGate(state);
|
|
238
270
|
// The tool-result redaction context, or `undefined` when the agent has no
|
|
239
271
|
// `onToolResult` hook (both redaction seams then take their zero-cost
|
|
@@ -253,7 +285,14 @@ export class OpenAIAgentsHarness {
|
|
|
253
285
|
? new ConsumerToolRegistry(new Set(consumerToolDefs.map((t) => t.name)))
|
|
254
286
|
: undefined;
|
|
255
287
|
const tools = consumerTools ? mapConsumerTools(consumerToolDefs, consumerTools, redaction) : [];
|
|
256
|
-
|
|
288
|
+
// MCP-tool approval gate: when a policy is active, each materialized MCP
|
|
289
|
+
// tool gets a `needsApproval` predicate resolving this same decider over
|
|
290
|
+
// the enriched invocation (bare name → `{ serverName, annotations }` via
|
|
291
|
+
// the live catalog). `undefined` when gating is off, so MCP tools carry
|
|
292
|
+
// no predicate and never suspend. Materialization is forced whenever this
|
|
293
|
+
// OR `redaction` is set — the native attach exposes neither hook.
|
|
294
|
+
const mcpApproval = policy !== undefined ? { policy, mcpCatalog: state.mcpCatalog } : undefined;
|
|
295
|
+
const { agent, runner } = await this.buildAgent(agentId, state, tools, redaction, mcpApproval);
|
|
257
296
|
// One coordinator owns this turn's run stream + sink + teardown. Dispose
|
|
258
297
|
// any prior coordinator on the same thread first so a re-`stream()`
|
|
259
298
|
// mid-turn can't leave a stale pump running against the old sink.
|
|
@@ -265,25 +304,20 @@ export class OpenAIAgentsHarness {
|
|
|
265
304
|
session,
|
|
266
305
|
policy,
|
|
267
306
|
consumerTools,
|
|
268
|
-
|
|
307
|
+
onToolResultErrors: (toolCallIds) => {
|
|
269
308
|
// Stamp `isError` onto the persisted `function_call_result`
|
|
270
|
-
// records
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
309
|
+
// records the run loop wrote without it (the OpenAI item shape
|
|
310
|
+
// has no `isError` field): consumer tools the consumer settled
|
|
311
|
+
// with an error, plus MCP tools the coordinator rejected (policy
|
|
312
|
+
// deny / user decline). Best-effort: a persistence failure here
|
|
313
|
+
// must not fail the settled turn, and the store's per-thread
|
|
314
|
+
// queue orders this after the run's writes.
|
|
315
|
+
void this.sessions.markToolResultError(agentId, threadId, toolCallIds).catch((error) => this.buses.getLogBus()?.warn('Failed to persist tool-result isError', {
|
|
276
316
|
agentId,
|
|
277
317
|
threadId,
|
|
278
318
|
error: getErrorMessage(error),
|
|
279
319
|
}));
|
|
280
320
|
},
|
|
281
|
-
onSettled: () => {
|
|
282
|
-
// The coordinator removes its own entry when the turn settles —
|
|
283
|
-
// guard against a newer turn having already replaced it.
|
|
284
|
-
if (state.coordinators.get(threadId) === coordinator)
|
|
285
|
-
state.coordinators.delete(threadId);
|
|
286
|
-
},
|
|
287
321
|
externalSignal: abortSignal,
|
|
288
322
|
// Live catalog reference so a mid-turn discovery settle enriches
|
|
289
323
|
// later MCP tool events; empty when the agent has no MCP servers.
|
|
@@ -291,6 +325,10 @@ export class OpenAIAgentsHarness {
|
|
|
291
325
|
// Factory-configured per-`toolCallId` approval timeout; the
|
|
292
326
|
// coordinator falls back to its own default when undefined.
|
|
293
327
|
toolApprovalTimeoutMs: this.toolApprovalTimeoutMs,
|
|
328
|
+
// Emission shape for a parallel `tool_use` batch's approvals: batch
|
|
329
|
+
// (all requests up front) vs serial (one at a time). Defaults to
|
|
330
|
+
// serial when unset, mirroring the sibling harnesses.
|
|
331
|
+
batchApprovals: options?.batchApprovals,
|
|
294
332
|
// The turn's step limit → `run(..., { maxTurns })`. Undefined leaves
|
|
295
333
|
// the SDK default in place; a hit surfaces as `finish('max-steps')`.
|
|
296
334
|
maxTurns: options?.maxSteps,
|
|
@@ -344,12 +382,16 @@ export class OpenAIAgentsHarness {
|
|
|
344
382
|
async updateAgent(agentId, modelConnectivityInfo, config, options) {
|
|
345
383
|
const state = this.getAgentOrThrow(agentId);
|
|
346
384
|
options?.abortSignal?.throwIfAborted();
|
|
347
|
-
// Phase 0 —
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
385
|
+
// Phase 0 — load non-MCP inputs BEFORE touching any MCP client, so a bad
|
|
386
|
+
// input can't leave servers half-cycled. Re-compose `config.rules` and
|
|
387
|
+
// re-load `config.skills`: a bad/unreadable rule or skill path rejects
|
|
388
|
+
// here, before the Phase 1 MCP diff cycles any transport (mirrors
|
|
389
|
+
// Mastra's Phase 0). `updateAgent` is atomic — a bad skill path in the
|
|
390
|
+
// new config rejects the whole update rather than applying the MCP delta
|
|
391
|
+
// and silently dropping the skill delta. Neither result is stamped onto
|
|
392
|
+
// state until Phase 2, so a rejection leaves the live agent untouched.
|
|
393
|
+
const rulesAppend = await composeRulesAppend(config?.rules);
|
|
394
|
+
const skillMap = await buildSkillMap(config?.skills, state.projectRoot, this.buses.getLogBus());
|
|
353
395
|
// Phase 1 — diff the next MCP config against the applied one, preserving
|
|
354
396
|
// structurally-equal servers and cycling the rest. Per-server Map
|
|
355
397
|
// mutations are synchronous so a re-entrant rollback `updateAgent`
|
|
@@ -358,11 +400,16 @@ export class OpenAIAgentsHarness {
|
|
|
358
400
|
this.diffMcpServers(agentId, state, next, config?.orgJwt, options?.abortSignal);
|
|
359
401
|
// Phase 2 — apply non-MCP changes. The model provider closure and
|
|
360
402
|
// `buildAgent` both read these live, so no provider/agent rebuild is
|
|
361
|
-
// required; `hooks` is re-stamped for the next turn's tool-result seam
|
|
403
|
+
// required; `hooks` is re-stamped for the next turn's tool-result seam,
|
|
404
|
+
// the freshly-composed `rulesAppend` is folded onto the next turn's
|
|
405
|
+
// instructions, and the reloaded `skillMap` backs the next turn's skill
|
|
406
|
+
// tools.
|
|
362
407
|
state.config = config ?? {};
|
|
363
408
|
state.mcpConfig = config?.mcpServers;
|
|
364
409
|
state.modelConnectivityInfo = modelConnectivityInfo;
|
|
365
410
|
state.hooks = options?.hooks ?? {};
|
|
411
|
+
state.rulesAppend = rulesAppend;
|
|
412
|
+
state.skillMap = skillMap;
|
|
366
413
|
}
|
|
367
414
|
// ── MCP Introspection / Reconnect ────────────────────────────────────
|
|
368
415
|
/**
|
|
@@ -558,47 +605,57 @@ export class OpenAIAgentsHarness {
|
|
|
558
605
|
* request) with tracing off. `tools` carries the turn's consumer-executed
|
|
559
606
|
* tools (mapped from `config.tools`).
|
|
560
607
|
*
|
|
561
|
-
* **MCP attach, two paths.**
|
|
562
|
-
*
|
|
563
|
-
* via the native `mcpServers` field; the SDK fetches their tools per
|
|
564
|
-
* Because each instance has `cacheToolsList: true` (cache keyed by
|
|
608
|
+
* **MCP attach, two paths.** With neither a redaction hook nor an active
|
|
609
|
+
* approval gate (the common case), the agent's live MCP server instances
|
|
610
|
+
* attach via the native `mcpServers` field; the SDK fetches their tools per
|
|
611
|
+
* run. Because each instance has `cacheToolsList: true` (cache keyed by
|
|
565
612
|
* `server.name`) and this harness reuses the SAME connected instances across
|
|
566
613
|
* turns, a rebuilt `Agent` here does not re-run `tools/list` for a preserved
|
|
567
|
-
* server — the #541 preservation mechanic.
|
|
568
|
-
* this milestone (the native attach exposes no per-tool `needsApproval`).
|
|
614
|
+
* server — the #541 preservation mechanic.
|
|
569
615
|
*
|
|
570
|
-
* **When a redaction hook is set,** the native attach
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
616
|
+
* **When a redaction hook OR an approval gate is set,** the native attach
|
|
617
|
+
* exposes neither a per-result rewrite seam nor a per-tool `needsApproval`
|
|
618
|
+
* seam, so MCP tools are materialized via `getAllMcpTools(...)` and rebuilt
|
|
619
|
+
* with the active hooks (see {@link buildManagedMcpTools}): a redaction shim
|
|
620
|
+
* on `invoke` and/or a `needsApproval` predicate driven by the policy. They
|
|
621
|
+
* attach as `tools` rather than `mcpServers`. #541 is preserved because
|
|
574
622
|
* `getAllMcpTools` reads each server's warm `cacheToolsList` cache with no
|
|
575
623
|
* network `tools/list`. This makes the method async (the materialize awaits
|
|
576
624
|
* `listTools()`), so `stream()` awaits it.
|
|
577
625
|
*/
|
|
578
|
-
async buildAgent(agentId, state, tools = [], redaction) {
|
|
626
|
+
async buildAgent(agentId, state, tools = [], redaction, mcpApproval) {
|
|
579
627
|
const mcpServerInstances = [...state.mcpServers.values()].map((s) => s.server);
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
// server
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
628
|
+
// Materialize + manage MCP tools when EITHER a redaction hook or an
|
|
629
|
+
// approval gate is active — the native `mcpServers` attach exposes
|
|
630
|
+
// neither the per-result rewrite seam nor the per-tool `needsApproval`
|
|
631
|
+
// seam. Only CONNECTED servers are materialized: their `cacheToolsList`
|
|
632
|
+
// cache is warm (discovery succeeded), so `getAllMcpTools` reads it with
|
|
633
|
+
// no network `tools/list` and cannot throw — where an errored /
|
|
634
|
+
// still-connecting server would make `getAllMcpTools` (which fails fast
|
|
635
|
+
// on the first server's throw) reject the whole turn PRE-stream. Skipping
|
|
636
|
+
// them keeps a failed MCP server from failing the turn (the WI-D
|
|
637
|
+
// contract) and matches the Claude harness's "only connected servers
|
|
638
|
+
// surface" posture; an errored server contributes no usable tools on the
|
|
639
|
+
// native path either.
|
|
640
|
+
const manageMcp = redaction !== undefined || mcpApproval !== undefined;
|
|
590
641
|
const connectedMcpServers = [...state.mcpServers.values()]
|
|
591
642
|
.filter((s) => s.status === McpServerStatus.Connected)
|
|
592
643
|
.map((s) => s.server);
|
|
593
|
-
const
|
|
594
|
-
? await
|
|
644
|
+
const managedMcpTools = manageMcp && connectedMcpServers.length > 0
|
|
645
|
+
? await buildManagedMcpTools(connectedMcpServers, { redaction, approval: mcpApproval })
|
|
595
646
|
: [];
|
|
596
|
-
|
|
597
|
-
//
|
|
598
|
-
|
|
647
|
+
// Skill tools (`load_skill` / `read_skill_file`) surface the agent's
|
|
648
|
+
// configured skills to the model. Built fresh per turn from the live
|
|
649
|
+
// `state.skillMap` — never cached — so an `updateAgent` that changes
|
|
650
|
+
// `config.skills` lands on the next turn. Empty when the agent has no
|
|
651
|
+
// skills, so a skill-less agent's tool surface is unchanged.
|
|
652
|
+
const skillTools = buildSkillTools(state.skillMap);
|
|
653
|
+
const allTools = [...tools, ...skillTools, ...managedMcpTools];
|
|
654
|
+
// Attach MCP natively only when we did NOT materialize them for a hook.
|
|
655
|
+
const attachMcpServers = !manageMcp && mcpServerInstances.length > 0;
|
|
599
656
|
const agent = new Agent({
|
|
600
657
|
name: agentId,
|
|
601
|
-
instructions: state.config.instructions
|
|
658
|
+
instructions: composeSystemPrompt(state.config.instructions, state.rulesAppend),
|
|
602
659
|
model: state.modelConnectivityInfo.nativeModelId,
|
|
603
660
|
...(allTools.length > 0 ? { tools: allTools } : {}),
|
|
604
661
|
...(attachMcpServers ? { mcpServers: mcpServerInstances } : {}),
|
|
@@ -888,19 +945,32 @@ export class OpenAIAgentsHarness {
|
|
|
888
945
|
state.mcpServers.clear();
|
|
889
946
|
state.mcpCatalog.clear();
|
|
890
947
|
}
|
|
891
|
-
/**
|
|
948
|
+
/**
|
|
949
|
+
* Rebuild the catalog entries for one server from its freshly-discovered
|
|
950
|
+
* tools. Keys each entry by the tool's **sanitized display name** (what
|
|
951
|
+
* `@openai/agents` registers on materialized tools and run-item events — see
|
|
952
|
+
* {@link toFunctionToolName}), while preserving the original bare MCP name as
|
|
953
|
+
* `toolName`. Keying by the raw name would miss enrichment for any tool whose
|
|
954
|
+
* name carries a character the SDK sanitizes to `_` (e.g. `get-sum` →
|
|
955
|
+
* `get_sum`), so neither the event adapter nor the approval gate would see
|
|
956
|
+
* its `serverName`.
|
|
957
|
+
*/
|
|
892
958
|
refreshCatalogForServer(state, serverName, tools) {
|
|
893
959
|
this.removeCatalogForServer(state, serverName);
|
|
894
960
|
for (const tool of tools) {
|
|
895
961
|
const annotations = tool.annotations;
|
|
896
|
-
state.mcpCatalog.set(tool.name, {
|
|
962
|
+
state.mcpCatalog.set(toFunctionToolName(tool.name), {
|
|
963
|
+
serverName,
|
|
964
|
+
toolName: tool.name,
|
|
965
|
+
...(annotations !== undefined ? { annotations } : {}),
|
|
966
|
+
});
|
|
897
967
|
}
|
|
898
968
|
}
|
|
899
969
|
/** Drop every catalog entry that points at `serverName`. */
|
|
900
970
|
removeCatalogForServer(state, serverName) {
|
|
901
|
-
for (const [
|
|
971
|
+
for (const [displayName, entry] of state.mcpCatalog) {
|
|
902
972
|
if (entry.serverName === serverName)
|
|
903
|
-
state.mcpCatalog.delete(
|
|
973
|
+
state.mcpCatalog.delete(displayName);
|
|
904
974
|
}
|
|
905
975
|
}
|
|
906
976
|
/**
|
|
@@ -993,22 +1063,38 @@ function isJsonObject(value) {
|
|
|
993
1063
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
994
1064
|
}
|
|
995
1065
|
/**
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
1066
|
+
* Sanitize an MCP tool name to the function-tool identifier `@openai/agents`
|
|
1067
|
+
* registers it under. A byte-identical copy of the SDK's internal
|
|
1068
|
+
* `toFunctionToolName` (`@openai/agents-core` `utils/tools.ts`), which is not
|
|
1069
|
+
* exported: whitespace and every non-alphanumeric character become `_`. The
|
|
1070
|
+
* SDK applies this to both the materialized function tool's `name` and the
|
|
1071
|
+
* `tool_called` / `tool_output` run-item names, so the harness must key its MCP
|
|
1072
|
+
* catalog by the same transformed name for enrichment + approval lookups to hit
|
|
1073
|
+
* (e.g. a `tools/list` name `get-sum` surfaces as `get_sum`). Kept local rather
|
|
1074
|
+
* than reached through a deep import per the harness-isolation rule; the
|
|
1075
|
+
* transform is a stable identifier-safety rule unlikely to drift.
|
|
999
1076
|
*/
|
|
1000
|
-
function
|
|
1077
|
+
function toFunctionToolName(name) {
|
|
1078
|
+
return name.replace(/\s/g, '_').replace(/[^a-zA-Z0-9]/g, '_');
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Lowers a `stream()` message to the `input` the OpenAI Agents run loop takes.
|
|
1082
|
+
*
|
|
1083
|
+
* A plain `string` passes through unchanged (the common text turn). A
|
|
1084
|
+
* `MessagePart[]` runs the shared `lowerStreamInput`, which validates any
|
|
1085
|
+
* `image` / `file` parts against the model's `supportedFormats` + the global
|
|
1086
|
+
* gateway caps (throwing `AgentSDKError(MULTIMODAL_NOT_SUPPORTED)` pre-stream on a
|
|
1087
|
+
* violation) and rejects non-input parts (`reasoning` / `tool-call` /
|
|
1088
|
+
* `tool-result`) with `INVALID_MESSAGE_CONTENT`, then lowers each valid part to an
|
|
1089
|
+
* `@openai/agents` `UserContent` block. The blocks are wrapped in one `user`
|
|
1090
|
+
* message item, so a mixed text-plus-attachment turn reaches the model as a single
|
|
1091
|
+
* multimodal user message. See {@link lowerInputPartToContent} for the wire shape
|
|
1092
|
+
* (notably the mandatory `data:` URL for images).
|
|
1093
|
+
*/
|
|
1094
|
+
function lowerMessage(message, model) {
|
|
1001
1095
|
if (typeof message === 'string')
|
|
1002
1096
|
return message;
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
if (part.type === 'text') {
|
|
1006
|
-
texts.push(part.text);
|
|
1007
|
-
}
|
|
1008
|
-
else {
|
|
1009
|
-
throw new AgentSDKError(`The OpenAI harness does not yet support "${part.type}" message parts (text only).`, AgentSDKErrorType.INVALID_MESSAGE_CONTENT);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
return texts.join('\n');
|
|
1097
|
+
const content = lowerStreamInput(message, (files) => validateMultimodalFiles(files, model), lowerInputPartToContent);
|
|
1098
|
+
return [{ role: 'user', content }];
|
|
1013
1099
|
}
|
|
1014
1100
|
//# sourceMappingURL=openai-agents-harness.js.map
|