@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +55 -0
  4. package/dist/gen-sink.d.ts +8 -0
  5. package/dist/gen-sink.js +13 -0
  6. package/dist/gen-sink.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +16 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/mcp-error-classifier.d.ts +36 -0
  11. package/dist/mcp-error-classifier.js +166 -0
  12. package/dist/mcp-error-classifier.js.map +1 -0
  13. package/dist/openai-agents-harness-factory.d.ts +36 -0
  14. package/dist/openai-agents-harness-factory.js +39 -0
  15. package/dist/openai-agents-harness-factory.js.map +1 -0
  16. package/dist/openai-agents-harness.d.ts +302 -0
  17. package/dist/openai-agents-harness.js +1014 -0
  18. package/dist/openai-agents-harness.js.map +1 -0
  19. package/dist/openai-approval-coordinator.d.ts +231 -0
  20. package/dist/openai-approval-coordinator.js +422 -0
  21. package/dist/openai-approval-coordinator.js.map +1 -0
  22. package/dist/openai-built-in-policies.d.ts +29 -0
  23. package/dist/openai-built-in-policies.js +33 -0
  24. package/dist/openai-built-in-policies.js.map +1 -0
  25. package/dist/openai-event-adapter.d.ts +119 -0
  26. package/dist/openai-event-adapter.js +322 -0
  27. package/dist/openai-event-adapter.js.map +1 -0
  28. package/dist/openai-mcp-config-mapper.d.ts +58 -0
  29. package/dist/openai-mcp-config-mapper.js +133 -0
  30. package/dist/openai-mcp-config-mapper.js.map +1 -0
  31. package/dist/openai-mcp-state.d.ts +67 -0
  32. package/dist/openai-mcp-state.js +6 -0
  33. package/dist/openai-mcp-state.js.map +1 -0
  34. package/dist/openai-message-mapper.d.ts +79 -0
  35. package/dist/openai-message-mapper.js +374 -0
  36. package/dist/openai-message-mapper.js.map +1 -0
  37. package/dist/openai-model-provider.d.ts +46 -0
  38. package/dist/openai-model-provider.js +144 -0
  39. package/dist/openai-model-provider.js.map +1 -0
  40. package/dist/openai-session-store.d.ts +149 -0
  41. package/dist/openai-session-store.js +328 -0
  42. package/dist/openai-session-store.js.map +1 -0
  43. package/dist/openai-tool-mapper.d.ts +121 -0
  44. package/dist/openai-tool-mapper.js +231 -0
  45. package/dist/openai-tool-mapper.js.map +1 -0
  46. package/dist/openai-tool-redaction.d.ts +55 -0
  47. package/dist/openai-tool-redaction.js +82 -0
  48. package/dist/openai-tool-redaction.js.map +1 -0
  49. package/dist/test/tsconfig.tsbuildinfo +1 -0
  50. package/dist/text-stream.d.ts +30 -0
  51. package/dist/text-stream.js +103 -0
  52. package/dist/text-stream.js.map +1 -0
  53. package/package.json +66 -0
@@ -0,0 +1,302 @@
1
+ import { type Clock, type LogRecord, type Unsubscribe, type UniqueIDGenerator } from '@salesforce/agentic-common';
2
+ import { type MCPServer, type ModelProvider } from '@openai/agents';
3
+ import { type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerInfo, type Message, type MessagePart, type ModelConnectivityInfo, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WithAgentConfig, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
4
+ import type { AgentHarness } from '@salesforce/sfdx-agent-sdk';
5
+ import type { AgentHooks } from '@salesforce/sfdx-agent-sdk';
6
+ /**
7
+ * The branded harness subtype consumers see when they pass
8
+ * `OpenAIAgentsHarnessFactory` to `createAgentManager`. The harness ships no
9
+ * config extensions, so the config type stays the base `HarnessAgentConfig`.
10
+ */
11
+ export type OpenAIAgentsAgentHarness = WithAgentConfig<AgentHarness & {
12
+ readonly harnessId: 'openai-agents';
13
+ readonly extensions: Record<string, never>;
14
+ }, HarnessAgentConfig>;
15
+ /** Internal constructor options (test seam; not part of the public surface). */
16
+ export type OpenAIAgentsHarnessOptions = {
17
+ /** Proxy-aware inner fetch (honors HTTPS_PROXY / NO_PROXY). */
18
+ innerFetch?: typeof fetch;
19
+ /** ID generator for thread ids; defaults to `UUIDGenerator`. */
20
+ idGenerator?: UniqueIDGenerator;
21
+ /** Time source for `createdAt` stamping; defaults to `RealClock`. */
22
+ clock?: Clock;
23
+ /**
24
+ * Builds the `@openai/agents` model provider for an agent. Defaults to the
25
+ * gateway-bound {@link buildOpenAIModelProvider}. Unit tests pass a factory
26
+ * returning a scripted-`Model` provider so the real run loop executes with
27
+ * no network and no credentials.
28
+ */
29
+ modelProviderFactory?: (getInfo: () => ModelConnectivityInfo) => ModelProvider;
30
+ /**
31
+ * Builds the live `@openai/agents` MCP server instances for a config.
32
+ * Defaults to {@link mapToOpenAIMcpServers}. Unit tests pass a factory
33
+ * returning fake `MCPServer`s so MCP discovery / lifecycle / preservation
34
+ * can be exercised with no subprocess and no network (mirrors
35
+ * {@link modelProviderFactory}).
36
+ */
37
+ mcpServerFactory?: (config: MCPConfiguration, orgJwt?: HarnessAgentConfig['orgJwt']) => Map<string, MCPServer>;
38
+ /**
39
+ * Per-`toolCallId` tool-approval timeout, in milliseconds, forwarded to each
40
+ * turn's `OpenAIApprovalCoordinator`. Defaults to the coordinator's own
41
+ * 600_000 ms when unset. Threaded from `OpenAIAgentsHarnessFactoryConfig`.
42
+ */
43
+ toolApprovalTimeoutMs?: number;
44
+ };
45
+ /**
46
+ * OpenAI Agents SDK-backed implementation of the SDK's `AgentHarness` contract.
47
+ *
48
+ * Implements connectivity, streamed turns, disk-backed sessions, thread
49
+ * lifecycle, message history (with `createdAt`/`isError` persistence), tool
50
+ * approval (`approveToolCall` / `declineToolCall`), consumer-executed tools
51
+ * (`submitToolResult`), MCP server lifecycle (`getMcpServerInfo` /
52
+ * `reconnectMcpServer` / `updateAgent` preservation), and thread compaction
53
+ * (`compactThread`). A per-`(agentId, threadId)` {@link OpenAIApprovalCoordinator}
54
+ * owns each turn's run loop, the approval suspend/resume flow, the opt-in policy
55
+ * gate, the per-`toolCallId` timeout, and the consumer-tool parker.
56
+ *
57
+ * MCP tools attach via the SDK's native `Agent({ mcpServers })` path and are
58
+ * observable (`tool-call` / `tool-result`, enriched with `serverName` /
59
+ * `bareToolName`) but are NOT approval-gated this milestone — the native attach
60
+ * exposes no per-tool `needsApproval` hook, so gating is a deferred follow-up.
61
+ * MCP discovery/status telemetry and wire-communication events emit on their
62
+ * respective seams, and the `options.hooks.onToolResult` redaction hook fires
63
+ * per tool result (consumer tools at the parked `execute`; MCP tools via a
64
+ * `getAllMcpTools` materialize-and-wrap when a redactor is set). Every
65
+ * agent-scoped method throws `AGENT_NOT_FOUND` on an unregistered id first (SDK
66
+ * invariant).
67
+ */
68
+ export declare class OpenAIAgentsHarness implements OpenAIAgentsAgentHarness {
69
+ private readonly storageRootFolder;
70
+ readonly harnessId: "openai-agents";
71
+ readonly protocolVersion: 1;
72
+ readonly extensions: Record<string, never>;
73
+ private readonly buses;
74
+ private readonly agents;
75
+ private readonly idGenerator;
76
+ private readonly clock;
77
+ private readonly innerFetch?;
78
+ private readonly modelProviderFactory;
79
+ private readonly mcpServerFactory;
80
+ private readonly sessions;
81
+ private readonly toolApprovalTimeoutMs?;
82
+ private shuttingDown;
83
+ constructor(storageRootFolder: string, options?: OpenAIAgentsHarnessOptions);
84
+ onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
85
+ onLog(callback: (record: LogRecord) => void): Unsubscribe;
86
+ onWireCommunication(callback: WireCommunicationEventCallback): Unsubscribe;
87
+ shutdown(): Promise<void>;
88
+ createAgent(agentId: string, projectRoot: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
89
+ abortSignal?: AbortSignal;
90
+ hooks?: AgentHooks;
91
+ }): Promise<void>;
92
+ destroyAgent(agentId: string): Promise<boolean>;
93
+ getAgentIds(): Promise<string[]>;
94
+ createThread(agentId: string, threadId?: string): Promise<string>;
95
+ getThreadIds(agentId: string): Promise<string[]>;
96
+ destroyThread(agentId: string, threadId: string): Promise<void>;
97
+ cloneThread(agentId: string, sourceThreadId: string): Promise<string>;
98
+ stream(agentId: string, threadId: string, message: string | MessagePart[], options?: StreamOptions): Promise<ChatStreamResult>;
99
+ getMessages(agentId: string, threadId: string): Promise<Message[]>;
100
+ clearMessages(agentId: string, threadId: string): Promise<void>;
101
+ addContext(agentId: string, threadId: string, messages: Message[]): Promise<void>;
102
+ /**
103
+ * Apply a new connectivity bag + config to a live agent, preserving any MCP
104
+ * client whose config is structurally unchanged (#541) and cycling only the
105
+ * changed/added/removed servers. No eager `Agent` rebuild is needed: this
106
+ * harness constructs a fresh `Agent` on every `stream()` (see
107
+ * {@link buildAgent}) reading `state.config` / `state.modelConnectivityInfo`
108
+ * / `state.mcpServers` live, so mutating state here is sufficient — the next
109
+ * turn picks up the change. Does NOT dispose in-flight coordinators, touch
110
+ * sessions, or persist (persistence is the SDK's responsibility).
111
+ */
112
+ updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
113
+ abortSignal?: AbortSignal;
114
+ hooks?: AgentHooks;
115
+ }): Promise<void>;
116
+ /**
117
+ * Synchronous snapshot of the agent's MCP servers: connected servers with
118
+ * their discovered tools, errored servers with a sanitized message +
119
+ * structured `errorDetail`, in-flight servers as `Connecting` /
120
+ * `Reconnecting`, and configured-but-disabled servers (never constructed) as
121
+ * `Disabled`. Status is updated asynchronously by background discovery, so a
122
+ * caller polls this to observe a `createAgent` / `reconnectMcpServer` result.
123
+ */
124
+ getMcpServerInfo(agentId: string): McpServerInfo[];
125
+ /**
126
+ * Cycle one MCP server's transport and re-discover its tools. Eager: awaits
127
+ * the close → connect → `invalidateToolsCache` → `listTools` round-trip, but
128
+ * a transient transport / discovery failure is recorded on per-server state
129
+ * (surfaced via `getMcpServerInfo`) rather than rejected — only the three
130
+ * programming errors throw. `getAgentOrThrow` runs first (AGENT_NOT_FOUND
131
+ * wins), then an unconfigured server throws `MCP_SERVER_NOT_FOUND` and a
132
+ * disabled one `MCP_SERVER_DISABLED`.
133
+ */
134
+ reconnectMcpServer(agentId: string, serverName: string): Promise<void>;
135
+ /**
136
+ * Compact a thread's history into a fresh thread seeded with an LLM-generated
137
+ * summary, then destroy the source (the `AgentHarness` contract). The OpenAI
138
+ * Agents SDK ships a server-side `OpenAIResponsesCompactionAwareSession.runCompaction`
139
+ * (via `responses.compact`), but it relies on `previous_response_id` /
140
+ * stored responses — unavailable against the stateless Salesforce gateway
141
+ * (`store: false`, the same reason the coordinator pins `reasoningItemIdPolicy: 'omit'`).
142
+ * So this hand-rolls summarize+seed+destroy, mirroring the Mastra/Claude harnesses.
143
+ *
144
+ * `getAgentOrThrow` runs first (AGENT_NOT_FOUND). An empty source is compacted
145
+ * to a fresh empty thread (leaving it attached would leak). A summarization or
146
+ * seed failure surfaces as `AgentSDKError(COMPACTION_FAILED)` with the source
147
+ * left intact; the source is destroyed best-effort only after the new thread
148
+ * is safely seeded.
149
+ */
150
+ compactThread(agentId: string, threadId: string): Promise<string>;
151
+ /**
152
+ * Feed a consumer-executed tool's result back into the thread's in-flight
153
+ * turn. `getAgentOrThrow` runs FIRST (AGENT_NOT_FOUND wins over the
154
+ * coordinator lookup, per the SDK invariant); a thread with no in-flight
155
+ * coordinator throws `TOOL_CALL_NOT_FOUND`. The coordinator resolves the
156
+ * parked `execute` keyed by `toolCallId`, the run continues on the SAME
157
+ * eventStream the consumer is already iterating, and idempotency (#589) is
158
+ * owned downstream. `error` rides through as an error-shaped outcome so a
159
+ * consumer-reported failure reaches the tool call.
160
+ */
161
+ submitToolResult(agentId: string, threadId: string, toolResult: ToolResultInfo): Promise<void>;
162
+ approveToolCall(agentId: string, threadId: string, toolCallId: string): Promise<void>;
163
+ declineToolCall(agentId: string, threadId: string, toolCallId: string): Promise<void>;
164
+ /**
165
+ * Forward an approve / decline to the thread's in-flight coordinator.
166
+ * `getAgentOrThrow` runs FIRST so an unregistered agent surfaces
167
+ * `AGENT_NOT_FOUND` (the SDK invariant: agent-registry check before the
168
+ * per-`(agentId, threadId)` coordinator lookup) — distinguishable from a
169
+ * registered agent with no in-flight run, which throws `TOOL_CALL_NOT_FOUND`.
170
+ * The coordinator owns idempotency (#589): a repeat / post-teardown settle is
171
+ * a silent no-op there. Post-resume events flow through the SAME eventStream
172
+ * the consumer is already iterating from `stream()`.
173
+ */
174
+ private settleToolApproval;
175
+ /**
176
+ * Build the `@openai/agents` `Agent` + `Runner` a turn runs against. One seam
177
+ * for the pair so a `stream()` constructs them identically each turn. The
178
+ * `Runner` binds the agent's live `modelProvider` (JWT rotation lands per
179
+ * request) with tracing off. `tools` carries the turn's consumer-executed
180
+ * tools (mapped from `config.tools`).
181
+ *
182
+ * **MCP attach, two paths.** Without a redaction hook (`redaction`
183
+ * undefined — the common case), the agent's live MCP server instances attach
184
+ * via the native `mcpServers` field; the SDK fetches their tools per run.
185
+ * Because each instance has `cacheToolsList: true` (cache keyed by
186
+ * `server.name`) and this harness reuses the SAME connected instances across
187
+ * turns, a rebuilt `Agent` here does not re-run `tools/list` for a preserved
188
+ * server — the #541 preservation mechanic. MCP tools are NOT approval-gated
189
+ * this milestone (the native attach exposes no per-tool `needsApproval`).
190
+ *
191
+ * **When a redaction hook is set,** the native attach exposes no per-result
192
+ * rewrite seam, so MCP tools are materialized via `getAllMcpTools(...)` and
193
+ * their `invoke` wrapped with the redactor (see {@link buildRedactedMcpTools});
194
+ * they attach as `tools` rather than `mcpServers`. #541 is preserved because
195
+ * `getAllMcpTools` reads each server's warm `cacheToolsList` cache with no
196
+ * network `tools/list`. This makes the method async (the materialize awaits
197
+ * `listTools()`), so `stream()` awaits it.
198
+ */
199
+ private buildAgent;
200
+ /**
201
+ * One-shot, non-agentic summarization for `compactThread`. Builds a throwaway
202
+ * `Agent` (summarization-role instructions, NO tools, NO MCP servers) bound to
203
+ * the agent's live `modelProvider`, and runs it non-streaming with no
204
+ * `session` (the summary call must not leak into the thread's persisted
205
+ * history). Returns the model's final text; throws if it produced none.
206
+ *
207
+ * `reasoningItemIdPolicy: 'omit'` is load-bearing for the same reason the
208
+ * turn coordinator pins it — the stateless gateway (`store: false`) rejects
209
+ * reasoning items sent by id reference.
210
+ */
211
+ private summarize;
212
+ /**
213
+ * Two-pass summarization for transcripts above {@link MAX_TRANSCRIPT_CHARS}.
214
+ * Splits on line boundaries, summarizes each chunk, then summarizes the
215
+ * concatenated chunk-summaries (recursing if the merge itself is still over
216
+ * budget). Mirrors the sibling harnesses' map/reduce so cross-harness
217
+ * compaction behavior matches.
218
+ */
219
+ private summarizeInChunks;
220
+ /**
221
+ * Best-effort `destroyThread`: swallows the error so `compactThread`'s outer
222
+ * flow isn't aborted by a teardown blip, but logs it at `warn` so a future
223
+ * `destroyThread` regression stays observable. Used on both the empty-source
224
+ * and non-empty compaction paths.
225
+ */
226
+ private destroyThreadBestEffort;
227
+ /**
228
+ * Construct the agent's enabled MCP servers and kick off background discovery
229
+ * for each. Synchronous: it inserts every server entry (status `Connecting`)
230
+ * then assigns its background discovery promise to `entry.ready`, so
231
+ * `createAgent` / `updateAgent` never block on `connect()` / `listTools()`.
232
+ * A no-op when the config declares no enabled servers.
233
+ */
234
+ private startMcpServers;
235
+ /**
236
+ * Connect + list tools for one MCP server, recording the outcome on
237
+ * per-server state and emitting the discovery telemetry triple
238
+ * (`started` → `completed` | `failed`) plus the terminal `status-changed`.
239
+ * Guards against an orphaned write by re-checking the server instance
240
+ * identity after each await — a superseded cycle replaces the entry with a
241
+ * fresh instance, so a stale discovery must neither clobber state nor emit
242
+ * (every terminal emit sits behind the `isCurrent()` guard). Discovery
243
+ * failure is recorded on state (status `Error`, sanitized message +
244
+ * structured detail) and never rethrown — matching the "failed server ≠
245
+ * failed create/reconnect" contract; an aborted run records the state but
246
+ * emits no `failed` telemetry.
247
+ */
248
+ private runMcpDiscovery;
249
+ /**
250
+ * Emit an MCP telemetry event, swallowing only the typed `DISPOSED` error a
251
+ * post-`shutdown()` background discovery callback would hit (the bus is gone).
252
+ * Any other emit failure rethrows so a real regression surfaces. Every MCP
253
+ * emit is background / racing shutdown, so all route through here.
254
+ */
255
+ private emitMcpTelemetry;
256
+ /**
257
+ * Flip a server's status synchronously and return a closure that emits the
258
+ * matching `mcp-server-status-changed` telemetry — or `undefined` when the
259
+ * status is unchanged (same-status transitions are deduped here so callers
260
+ * don't have to). Splitting the flip from the emit lets callers order the
261
+ * emit AFTER the discovery-terminal event (#373), while a synchronous reader
262
+ * of `getMcpServerInfo()` already sees the post-flip status.
263
+ */
264
+ private prepareMcpStatusChange;
265
+ /**
266
+ * Diff the next MCP config against the applied one and preserve / cycle / add
267
+ * / remove servers accordingly. `mcpServerConfigEqual` (SDK-exported, uniform
268
+ * across harnesses) is the preserve predicate: an equal server is left
269
+ * completely untouched (its live instance + warm tool-list cache survive — no
270
+ * `close`, no `tools/list`, the #541 checkpoint). A changed / removed server
271
+ * is closed and dropped; a changed / added enabled server is constructed
272
+ * fresh and discovered in the background.
273
+ */
274
+ private diffMcpServers;
275
+ /** Close every MCP server on an agent, awaiting in-flight discovery first. */
276
+ private closeMcpServers;
277
+ /** Rebuild the catalog entries for one server from its freshly-discovered tools. */
278
+ private refreshCatalogForServer;
279
+ /** Drop every catalog entry that points at `serverName`. */
280
+ private removeCatalogForServer;
281
+ /**
282
+ * Decide whether the tool-approval gate engages this turn and, if so, return
283
+ * the per-tool policy decider the coordinator consults. `undefined` ⇒ gate
284
+ * inactive; the coordinator suspends nothing and tools run free (the SDK's
285
+ * "no policy ⇒ no gating" back-compat invariant), avoiding a suspend/resume
286
+ * round-trip per call.
287
+ *
288
+ * Gating is **opt-in**: it engages only when the consumer set
289
+ * `config.toolPolicies` (non-empty) or `config.defaultToolDecision`. Whether
290
+ * the gate engaged is frozen at stream start (the closure is built iff gating
291
+ * is active now), but the closure itself reads `state.config` **live on every
292
+ * call** rather than a snapshot — a mid-turn `updateAgentConfig` (notably the
293
+ * `source: 'remember'` rule an "Allow always" click appends) reassigns
294
+ * `state.config` in place on this same `state` without disposing the in-flight
295
+ * coordinator, so a snapshot would leave approvals 2..N in that turn
296
+ * re-prompting even though the rule is persisted (#638). Reading live makes
297
+ * the remembered decision auto-resolve subsequent matching calls.
298
+ * `OPENAI_BUILT_IN_TOOL_POLICIES` is the `tiers.harness` slice (empty today).
299
+ */
300
+ private buildPolicyGate;
301
+ private getAgentOrThrow;
302
+ }