@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 @@
1
+ {"version":3,"file":"openai-model-provider.js","sourceRoot":"","sources":["../src/openai-model-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,149 @@
1
+ import { type Clock, type LogBus } from '@salesforce/agentic-common';
2
+ import type { AgentInputItem, Session } from '@openai/agents';
3
+ /**
4
+ * One persisted entry: the `@openai/agents` protocol item plus the two facts
5
+ * the OpenAI item shape cannot carry.
6
+ *
7
+ * The OpenAI Agents SDK stores a completed tool call as a `function_call` +
8
+ * `function_call_result` item pair sharing `callId`; neither carries a
9
+ * timestamp, and `function_call_result` has no `isError` field — a thrown tool
10
+ * error becomes a generic English string in its `output`. The SDK contract,
11
+ * however, requires `getMessages` to populate `createdAt` on every message
12
+ * (#464) and to preserve a tool result's `isError` flag (#647). This harness
13
+ * owns the entire on-disk persistence (the SDK ships only an in-memory
14
+ * `MemorySession`), so rather than a separate `callId`-keyed sidecar file (the
15
+ * Claude `ThreadContextStore` pattern, forced on Claude because its transcript is
16
+ * SDK-owned) the two facts ride on this record envelope: written transactionally
17
+ * with the item in one file, cleared with it, and keyed to the item by position
18
+ * (`createdAt`) or by `callId` lookup on the item itself (`isError`).
19
+ */
20
+ export type SessionRecord = {
21
+ /** The `@openai/agents` protocol item, stored verbatim. */
22
+ item: AgentInputItem;
23
+ /** ISO 8601. Stamped at write time (run loop) or carried from `addContext`. */
24
+ createdAt: string;
25
+ /**
26
+ * Present only on `function_call_result` records the harness knows failed.
27
+ * Absent on success paths and on non-result items. Carries the SDK's
28
+ * `ToolResultInfo.isError` across a persistence round-trip so `getMessages`
29
+ * can restore it on the `tool-result` part (#647).
30
+ */
31
+ isError?: boolean;
32
+ };
33
+ /**
34
+ * Harness-owned disk persistence for OpenAI Agents SDK sessions. One JSON file
35
+ * per `(agentId, threadId)` under `${storageRootFolder}/openai-sessions/`:
36
+ *
37
+ * ${storageRootFolder}/openai-sessions/
38
+ * <agentId>/
39
+ * <threadId>.json ← JSON array of {@link SessionRecord}
40
+ *
41
+ * The store is agent-scoped so `getThreadIds(agentId)` maps to a directory
42
+ * listing and threads survive a harness restart (`createAgent` rehydrates from
43
+ * the agent directory). Unlike the Claude harness, the OpenAI SDK's
44
+ * `clearSession()` leaves the session id reusable, so the user-facing `threadId`
45
+ * *is* the SDK session id — no id-rotation dance and no `threadId`↔`sessionId`
46
+ * divergence.
47
+ *
48
+ * **Whole-file rewrite, not append.** Each mutation reads the current records,
49
+ * mutates in memory, and writes the whole file via a `.tmp` + `rename` (atomic
50
+ * on POSIX; per-key serialization handles the Windows concurrent-rename EPERM).
51
+ * Append-only JSONL (the Claude session store's shape) can't support
52
+ * `popItem()` (remove-last, used by the run loop's orphan-tool-call cleanup) or
53
+ * an out-of-band `isError` update, both of which this store needs. Threads are
54
+ * bounded in practice, so the O(n) rewrite per turn is acceptable — the same
55
+ * trade the Claude `ThreadContextStore` makes for its per-turn sidecar rewrite.
56
+ *
57
+ * **Concurrency.** A per-`(agentId, threadId)` in-flight queue serializes
58
+ * mutations on the same thread (ported from the Claude `DiskBackedSessionStore`
59
+ * `serialize()` primitive). Two `AgentManager` instances over the same
60
+ * `storageRootFolder` is undefined behavior per the SDK ARCHITECTURE —
61
+ * cross-process locking is out of scope.
62
+ */
63
+ export declare class OpenAISessionStore {
64
+ private readonly storageRootFolder;
65
+ private readonly logBus;
66
+ private readonly clock;
67
+ private readonly inflight;
68
+ constructor(storageRootFolder: string, logBus: LogBus | undefined, clock?: Clock);
69
+ /**
70
+ * A per-thread {@link Session} adapter bound to `(agentId, threadId)`,
71
+ * satisfying the `@openai/agents` `Session` interface so it can be threaded
72
+ * into `run(agent, input, { session })`. The runner reads history via
73
+ * `getItems()` and appends new items via `addItems()` itself — the harness
74
+ * never double-writes the run loop's output.
75
+ */
76
+ session(agentId: string, threadId: string): Session;
77
+ /**
78
+ * All items for a thread, in stored order (last `limit` when provided),
79
+ * sanitized for re-submission as model INPUT.
80
+ *
81
+ * The runner reads this back and re-sends prior turns as input on every
82
+ * follow-up model call. The Salesforce gateway echoes output-only fields on
83
+ * assistant content it returns (notably `logprobs` on an `output_text`
84
+ * block), and the SDK persists the item verbatim — but the gateway's INPUT
85
+ * content-param model is strict and rejects those same fields with
86
+ * `400 Unrecognized field "logprobs" ... not marked as ignorable`. The real
87
+ * OpenAI API ignores unknown input fields; this gateway does not, so the
88
+ * harness strips them here. Sanitizing on the READ path (not the write) keeps
89
+ * stored records verbatim, so `getRecords` / `getMessages` fidelity and the
90
+ * "item stored verbatim" invariant are untouched — only what is replayed to
91
+ * the model is cleaned.
92
+ */
93
+ getItems(agentId: string, threadId: string, limit?: number): Promise<AgentInputItem[]>;
94
+ /**
95
+ * Append run-loop items, stamping `createdAt` at write time. The stamp
96
+ * seeds strictly after the last stored record's `createdAt` (via
97
+ * `clock.nextAfter`) and steps per item, so history stays strictly
98
+ * ascending even under a fixed test clock — the `getMessages` #464
99
+ * ascending post-condition holds deterministically.
100
+ */
101
+ addItems(agentId: string, threadId: string, items: AgentInputItem[]): Promise<void>;
102
+ /** Remove and return the most recent item, or `undefined` when empty. */
103
+ popItem(agentId: string, threadId: string): Promise<AgentInputItem | undefined>;
104
+ /** Truncate a thread's history to empty, leaving the thread reusable. */
105
+ clear(agentId: string, threadId: string): Promise<void>;
106
+ /**
107
+ * Append pre-built records carrying their own `createdAt` + `isError` — the
108
+ * `addContext` path, which persists consumer-supplied history without a
109
+ * model turn. `createdAt` is backfilled by the caller (shared
110
+ * `backfillCreatedAt`); `isError` rides from the `tool-result` part.
111
+ */
112
+ appendRecords(agentId: string, threadId: string, records: SessionRecord[]): Promise<void>;
113
+ /**
114
+ * Stamp `isError: true` on the `function_call_result` records whose `callId`
115
+ * is in `toolCallIds` — the out-of-band update the whole-file-rewrite store
116
+ * exists to support. The run loop persists a consumer tool's result via
117
+ * `addItems` as a bare `{ item, createdAt }` (the OpenAI `function_call_result`
118
+ * item has no `isError` field), so when a consumer settles a tool
119
+ * with `isError: true` the coordinator calls this after the run completes to
120
+ * carry the flag onto history, satisfying the #647 "isError survives" contract.
121
+ * A no-op when no matching result record exists (e.g. the id was never
122
+ * persisted). Late `addItems` from the same turn have already settled under
123
+ * the per-`(agentId, threadId)` serialize queue, so the target record is
124
+ * present by the time this runs.
125
+ */
126
+ markToolResultError(agentId: string, threadId: string, toolCallIds: Iterable<string>): Promise<void>;
127
+ /** Full records (item + `createdAt` + `isError`) for the `getMessages` read path. */
128
+ getRecords(agentId: string, threadId: string): Promise<SessionRecord[]>;
129
+ /** Delete a thread's file entirely (thread destruction). */
130
+ delete(agentId: string, threadId: string): Promise<void>;
131
+ /**
132
+ * Thread ids for an agent, read from the agent directory. Backs
133
+ * `getThreadIds` rehydration on restart. Returns `[]` when the agent has no
134
+ * directory yet (no thread has been persisted).
135
+ */
136
+ listThreadIds(agentId: string): Promise<string[]>;
137
+ private readRecords;
138
+ private writeRecords;
139
+ private agentDir;
140
+ private fileFor;
141
+ /**
142
+ * Per-`(agentId, threadId)` in-flight queue. Ported verbatim from the Claude
143
+ * `DiskBackedSessionStore` — the atomicity primitive that serializes
144
+ * read-mutate-write cycles on the same thread so a concurrent `run()` turn,
145
+ * `addContext`, and `clearMessages` never interleave a read against a
146
+ * half-written file.
147
+ */
148
+ private serialize;
149
+ }
@@ -0,0 +1,328 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
6
+ import { dirname, join } from 'node:path';
7
+ import { getErrorMessage, RealClock } from '@salesforce/agentic-common';
8
+ const SESSIONS_SUBDIR = 'openai-sessions';
9
+ const FILE_EXT = '.json';
10
+ /**
11
+ * Harness-owned disk persistence for OpenAI Agents SDK sessions. One JSON file
12
+ * per `(agentId, threadId)` under `${storageRootFolder}/openai-sessions/`:
13
+ *
14
+ * ${storageRootFolder}/openai-sessions/
15
+ * <agentId>/
16
+ * <threadId>.json ← JSON array of {@link SessionRecord}
17
+ *
18
+ * The store is agent-scoped so `getThreadIds(agentId)` maps to a directory
19
+ * listing and threads survive a harness restart (`createAgent` rehydrates from
20
+ * the agent directory). Unlike the Claude harness, the OpenAI SDK's
21
+ * `clearSession()` leaves the session id reusable, so the user-facing `threadId`
22
+ * *is* the SDK session id — no id-rotation dance and no `threadId`↔`sessionId`
23
+ * divergence.
24
+ *
25
+ * **Whole-file rewrite, not append.** Each mutation reads the current records,
26
+ * mutates in memory, and writes the whole file via a `.tmp` + `rename` (atomic
27
+ * on POSIX; per-key serialization handles the Windows concurrent-rename EPERM).
28
+ * Append-only JSONL (the Claude session store's shape) can't support
29
+ * `popItem()` (remove-last, used by the run loop's orphan-tool-call cleanup) or
30
+ * an out-of-band `isError` update, both of which this store needs. Threads are
31
+ * bounded in practice, so the O(n) rewrite per turn is acceptable — the same
32
+ * trade the Claude `ThreadContextStore` makes for its per-turn sidecar rewrite.
33
+ *
34
+ * **Concurrency.** A per-`(agentId, threadId)` in-flight queue serializes
35
+ * mutations on the same thread (ported from the Claude `DiskBackedSessionStore`
36
+ * `serialize()` primitive). Two `AgentManager` instances over the same
37
+ * `storageRootFolder` is undefined behavior per the SDK ARCHITECTURE —
38
+ * cross-process locking is out of scope.
39
+ */
40
+ export class OpenAISessionStore {
41
+ storageRootFolder;
42
+ logBus;
43
+ clock;
44
+ inflight = new Map();
45
+ constructor(storageRootFolder, logBus, clock = new RealClock()) {
46
+ this.storageRootFolder = storageRootFolder;
47
+ this.logBus = logBus;
48
+ this.clock = clock;
49
+ }
50
+ /**
51
+ * A per-thread {@link Session} adapter bound to `(agentId, threadId)`,
52
+ * satisfying the `@openai/agents` `Session` interface so it can be threaded
53
+ * into `run(agent, input, { session })`. The runner reads history via
54
+ * `getItems()` and appends new items via `addItems()` itself — the harness
55
+ * never double-writes the run loop's output.
56
+ */
57
+ session(agentId, threadId) {
58
+ return new DiskBackedSession(this, agentId, threadId);
59
+ }
60
+ // ── Session-interface primitives (delegated to by DiskBackedSession) ─────
61
+ /**
62
+ * All items for a thread, in stored order (last `limit` when provided),
63
+ * sanitized for re-submission as model INPUT.
64
+ *
65
+ * The runner reads this back and re-sends prior turns as input on every
66
+ * follow-up model call. The Salesforce gateway echoes output-only fields on
67
+ * assistant content it returns (notably `logprobs` on an `output_text`
68
+ * block), and the SDK persists the item verbatim — but the gateway's INPUT
69
+ * content-param model is strict and rejects those same fields with
70
+ * `400 Unrecognized field "logprobs" ... not marked as ignorable`. The real
71
+ * OpenAI API ignores unknown input fields; this gateway does not, so the
72
+ * harness strips them here. Sanitizing on the READ path (not the write) keeps
73
+ * stored records verbatim, so `getRecords` / `getMessages` fidelity and the
74
+ * "item stored verbatim" invariant are untouched — only what is replayed to
75
+ * the model is cleaned.
76
+ */
77
+ async getItems(agentId, threadId, limit) {
78
+ const records = await this.serialize(agentId, threadId, () => this.readRecords(agentId, threadId));
79
+ const items = records.map((r) => sanitizeForModelInput(r.item));
80
+ return limit !== undefined && limit >= 0 ? items.slice(Math.max(0, items.length - limit)) : items;
81
+ }
82
+ /**
83
+ * Append run-loop items, stamping `createdAt` at write time. The stamp
84
+ * seeds strictly after the last stored record's `createdAt` (via
85
+ * `clock.nextAfter`) and steps per item, so history stays strictly
86
+ * ascending even under a fixed test clock — the `getMessages` #464
87
+ * ascending post-condition holds deterministically.
88
+ */
89
+ async addItems(agentId, threadId, items) {
90
+ if (items.length === 0)
91
+ return;
92
+ await this.serialize(agentId, threadId, async () => {
93
+ const records = await this.readRecords(agentId, threadId);
94
+ let cursor = records.length > 0 ? new Date(records[records.length - 1].createdAt) : undefined;
95
+ for (const item of items) {
96
+ cursor = cursor ? this.clock.nextAfter(cursor) : this.clock.now();
97
+ records.push({ item, createdAt: cursor.toISOString() });
98
+ }
99
+ await this.writeRecords(agentId, threadId, records);
100
+ });
101
+ }
102
+ /** Remove and return the most recent item, or `undefined` when empty. */
103
+ async popItem(agentId, threadId) {
104
+ return this.serialize(agentId, threadId, async () => {
105
+ const records = await this.readRecords(agentId, threadId);
106
+ const last = records.pop();
107
+ if (last === undefined)
108
+ return undefined;
109
+ await this.writeRecords(agentId, threadId, records);
110
+ return last.item;
111
+ });
112
+ }
113
+ /** Truncate a thread's history to empty, leaving the thread reusable. */
114
+ async clear(agentId, threadId) {
115
+ await this.serialize(agentId, threadId, () => this.writeRecords(agentId, threadId, []));
116
+ }
117
+ // ── Harness-facing operations (createdAt/isError-aware) ──────────────────
118
+ /**
119
+ * Append pre-built records carrying their own `createdAt` + `isError` — the
120
+ * `addContext` path, which persists consumer-supplied history without a
121
+ * model turn. `createdAt` is backfilled by the caller (shared
122
+ * `backfillCreatedAt`); `isError` rides from the `tool-result` part.
123
+ */
124
+ async appendRecords(agentId, threadId, records) {
125
+ if (records.length === 0)
126
+ return;
127
+ await this.serialize(agentId, threadId, async () => {
128
+ const existing = await this.readRecords(agentId, threadId);
129
+ await this.writeRecords(agentId, threadId, [...existing, ...records]);
130
+ });
131
+ }
132
+ /**
133
+ * Stamp `isError: true` on the `function_call_result` records whose `callId`
134
+ * is in `toolCallIds` — the out-of-band update the whole-file-rewrite store
135
+ * exists to support. The run loop persists a consumer tool's result via
136
+ * `addItems` as a bare `{ item, createdAt }` (the OpenAI `function_call_result`
137
+ * item has no `isError` field), so when a consumer settles a tool
138
+ * with `isError: true` the coordinator calls this after the run completes to
139
+ * carry the flag onto history, satisfying the #647 "isError survives" contract.
140
+ * A no-op when no matching result record exists (e.g. the id was never
141
+ * persisted). Late `addItems` from the same turn have already settled under
142
+ * the per-`(agentId, threadId)` serialize queue, so the target record is
143
+ * present by the time this runs.
144
+ */
145
+ async markToolResultError(agentId, threadId, toolCallIds) {
146
+ const ids = new Set(toolCallIds);
147
+ if (ids.size === 0)
148
+ return;
149
+ await this.serialize(agentId, threadId, async () => {
150
+ const records = await this.readRecords(agentId, threadId);
151
+ let changed = false;
152
+ for (const record of records) {
153
+ const item = record.item;
154
+ if (item.type === 'function_call_result' && item.callId !== undefined && ids.has(item.callId)) {
155
+ if (record.isError !== true) {
156
+ record.isError = true;
157
+ changed = true;
158
+ }
159
+ }
160
+ }
161
+ if (changed)
162
+ await this.writeRecords(agentId, threadId, records);
163
+ });
164
+ }
165
+ /** Full records (item + `createdAt` + `isError`) for the `getMessages` read path. */
166
+ async getRecords(agentId, threadId) {
167
+ return this.serialize(agentId, threadId, () => this.readRecords(agentId, threadId));
168
+ }
169
+ /** Delete a thread's file entirely (thread destruction). */
170
+ async delete(agentId, threadId) {
171
+ await this.serialize(agentId, threadId, () => rm(this.fileFor(agentId, threadId), { force: true }));
172
+ }
173
+ /**
174
+ * Thread ids for an agent, read from the agent directory. Backs
175
+ * `getThreadIds` rehydration on restart. Returns `[]` when the agent has no
176
+ * directory yet (no thread has been persisted).
177
+ */
178
+ async listThreadIds(agentId) {
179
+ const dir = this.agentDir(agentId);
180
+ let entries;
181
+ try {
182
+ entries = await readdir(dir);
183
+ }
184
+ catch (err) {
185
+ if (err.code === 'ENOENT')
186
+ return [];
187
+ throw err;
188
+ }
189
+ return entries.filter((n) => n.endsWith(FILE_EXT)).map((n) => n.slice(0, -FILE_EXT.length));
190
+ }
191
+ // ── Internals (called only inside `serialize`) ───────────────────────────
192
+ async readRecords(agentId, threadId) {
193
+ const file = this.fileFor(agentId, threadId);
194
+ let raw;
195
+ try {
196
+ raw = await readFile(file, 'utf8');
197
+ }
198
+ catch (err) {
199
+ if (err.code === 'ENOENT')
200
+ return [];
201
+ throw err;
202
+ }
203
+ try {
204
+ const parsed = JSON.parse(raw);
205
+ return Array.isArray(parsed) ? parsed : [];
206
+ }
207
+ catch (err) {
208
+ // A corrupt file is logged and treated as empty rather than failing
209
+ // every read — matches AgentIdentityStore's soft-skip posture. The
210
+ // corrupt file stays on disk; the next write overwrites it cleanly.
211
+ this.logBus?.warn('skipping unparseable OpenAI session file', {
212
+ file,
213
+ error: getErrorMessage(err),
214
+ });
215
+ return [];
216
+ }
217
+ }
218
+ async writeRecords(agentId, threadId, records) {
219
+ const file = this.fileFor(agentId, threadId);
220
+ await mkdir(dirname(file), { recursive: true });
221
+ const tmp = `${file}.tmp`;
222
+ await writeFile(tmp, JSON.stringify(records), 'utf8');
223
+ await rename(tmp, file);
224
+ }
225
+ agentDir(agentId) {
226
+ return join(this.storageRootFolder, SESSIONS_SUBDIR, sanitize(agentId));
227
+ }
228
+ fileFor(agentId, threadId) {
229
+ return join(this.agentDir(agentId), `${sanitize(threadId)}${FILE_EXT}`);
230
+ }
231
+ /**
232
+ * Per-`(agentId, threadId)` in-flight queue. Ported verbatim from the Claude
233
+ * `DiskBackedSessionStore` — the atomicity primitive that serializes
234
+ * read-mutate-write cycles on the same thread so a concurrent `run()` turn,
235
+ * `addContext`, and `clearMessages` never interleave a read against a
236
+ * half-written file.
237
+ */
238
+ serialize(agentId, threadId, fn) {
239
+ const k = `${sanitize(agentId)} ${sanitize(threadId)}`;
240
+ const previous = this.inflight.get(k) ?? Promise.resolve();
241
+ const next = previous.catch(() => undefined).then(fn);
242
+ this.inflight.set(k, next);
243
+ next.finally(() => {
244
+ if (this.inflight.get(k) === next)
245
+ this.inflight.delete(k);
246
+ }).catch(() => undefined);
247
+ return next;
248
+ }
249
+ }
250
+ /**
251
+ * Per-thread `Session` adapter. Constructed per `stream()` call, bound to one
252
+ * `(agentId, threadId)`, delegating every operation to the shared
253
+ * {@link OpenAISessionStore}. `getSessionId()` returns the `threadId` (they are
254
+ * the same value on this harness).
255
+ */
256
+ class DiskBackedSession {
257
+ store;
258
+ agentId;
259
+ threadId;
260
+ constructor(store, agentId, threadId) {
261
+ this.store = store;
262
+ this.agentId = agentId;
263
+ this.threadId = threadId;
264
+ }
265
+ async getSessionId() {
266
+ return this.threadId;
267
+ }
268
+ async getItems(limit) {
269
+ return this.store.getItems(this.agentId, this.threadId, limit);
270
+ }
271
+ async addItems(items) {
272
+ return this.store.addItems(this.agentId, this.threadId, items);
273
+ }
274
+ async popItem() {
275
+ return this.store.popItem(this.agentId, this.threadId);
276
+ }
277
+ async clearSession() {
278
+ return this.store.clear(this.agentId, this.threadId);
279
+ }
280
+ }
281
+ /**
282
+ * Collapse anything outside `[A-Za-z0-9._-]` to `_` so agent ids and thread ids
283
+ * are safe path segments. Thread ids are UUIDs in practice, but a
284
+ * consumer-supplied explicit thread id (or agent id) is not guaranteed safe.
285
+ */
286
+ function sanitize(s) {
287
+ return s.replace(/[^A-Za-z0-9._-]/g, '_');
288
+ }
289
+ /**
290
+ * Output-only fields the gateway emits on assistant content but rejects when the
291
+ * same item is resubmitted as model INPUT. The SDK stashes these under a content
292
+ * block's `providerData` and spreads `providerData` back onto the wire object on
293
+ * resend, so `providerData.logprobs` becomes a top-level `logprobs` the gateway's
294
+ * strict input model rejects (`400 Unrecognized field "logprobs"`). `logprobs` is
295
+ * the confirmed offender; add siblings here as the wire surfaces them.
296
+ */
297
+ const OUTPUT_ONLY_CONTENT_FIELDS = ['logprobs'];
298
+ /**
299
+ * Return a copy of a persisted item safe to resubmit as model input: strip the
300
+ * gateway's output-only fields ({@link OUTPUT_ONLY_CONTENT_FIELDS}) from each
301
+ * content block's `providerData` (where the SDK stores echoed provider fields,
302
+ * confirmed against the live gateway by the `--harness openai` e2e matrix). Deep-clones
303
+ * only the touched block(s), so the common (clean) item passes through untouched.
304
+ * Non-object items and items without a content array are returned as-is.
305
+ */
306
+ function sanitizeForModelInput(item) {
307
+ const content = item.content;
308
+ if (!Array.isArray(content))
309
+ return item;
310
+ let mutated = false;
311
+ const cleanedContent = content.map((block) => {
312
+ if (block === null || typeof block !== 'object')
313
+ return block;
314
+ const providerData = block.providerData;
315
+ if (providerData === null || typeof providerData !== 'object')
316
+ return block;
317
+ const present = OUTPUT_ONLY_CONTENT_FIELDS.filter((f) => f in providerData);
318
+ if (present.length === 0)
319
+ return block;
320
+ mutated = true;
321
+ const cleanedProviderData = { ...providerData };
322
+ for (const f of present)
323
+ delete cleanedProviderData[f];
324
+ return { ...block, providerData: cleanedProviderData };
325
+ });
326
+ return mutated ? { ...item, content: cleanedContent } : item;
327
+ }
328
+ //# sourceMappingURL=openai-session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-session-store.js","sourceRoot":"","sources":["../src/openai-session-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ import { type Tool } from '@openai/agents';
2
+ import { type ToolDefinition } from '@salesforce/sfdx-agent-sdk';
3
+ import { type RedactionContext } from './openai-tool-redaction.js';
4
+ /** The outcome a consumer supplies for a parked tool call via `submitToolResult`. */
5
+ export type ConsumerToolOutcome = {
6
+ result: unknown;
7
+ isError?: boolean;
8
+ };
9
+ /**
10
+ * Per-turn registry for consumer-executed (client-side) tools — those declared
11
+ * in `AgentConfig.tools` with no `execute`. The harness registers them with the
12
+ * model so it can call them, but never runs them: the mapped tool's `execute`
13
+ * PARKS on a promise this registry hands out, and the consumer resolves it
14
+ * out-of-band via `submitToolResult`. A parked `execute` resolved later lets the
15
+ * `@openai/agents` run loop continue on the same run — no in-process MCP bridge
16
+ * needed.
17
+ *
18
+ * Keyed by `toolCallId`, read directly off `details.toolCall.callId` at execute
19
+ * entry (the SDK populates it before invoking the tool), so binding is a direct
20
+ * map insert — no FIFO-by-name matching.
21
+ *
22
+ * Idempotent settle (#589): a second `settle` on the same id, or a settle after
23
+ * teardown rejected it, is a silent no-op; a `toolCallId` that never parked is
24
+ * genuine misuse and throws `TOOL_CALL_NOT_FOUND`.
25
+ */
26
+ export declare class ConsumerToolRegistry {
27
+ readonly toolNames: ReadonlySet<string>;
28
+ /** Parked calls awaiting a consumer result, by `toolCallId`. */
29
+ private readonly pending;
30
+ /**
31
+ * Outcomes the consumer submitted BEFORE the tool's `execute` parked, by
32
+ * `toolCallId`. The harness emits the `tool-call` ChatEvent the moment the SDK
33
+ * enqueues `tool_called` — which is BEFORE it awaits `execute` — so a consumer
34
+ * iterating the stream can call `submitToolResult` before the parker
35
+ * registers. Buffer it here; `park` drains it immediately so the result is
36
+ * never lost to the race.
37
+ */
38
+ private readonly preSubmitted;
39
+ /** Ids already settled (by consumer or teardown), so a repeat settle is a no-op. */
40
+ private readonly settled;
41
+ /**
42
+ * Consumer-tool call ids the consumer settled with `isError: true`. The run
43
+ * loop persists the resulting `function_call_result` with no `isError` field
44
+ * (the OpenAI item shape can't carry it), so the coordinator reads this after
45
+ * the turn settles and stamps `isError` onto those records out-of-band,
46
+ * satisfying the #647 "isError survives to history" contract.
47
+ */
48
+ private readonly erroredIds;
49
+ /**
50
+ * Consumer-tool call ids the coordinator has surfaced as a `tool-call`
51
+ * ChatEvent this turn. A `settle` for a not-yet-parked id is buffered only
52
+ * when the id is here (an early-arriving result); an id never seen is genuine
53
+ * misuse and throws.
54
+ */
55
+ private readonly emitted;
56
+ /** Whether the turn has torn down — a park after teardown resolves immediately as an error. */
57
+ private tornDown;
58
+ /** Error to resolve late parks with after teardown. */
59
+ private teardownError?;
60
+ /** The bare names of the consumer tools registered this turn (for dual-enforcement checks). */
61
+ constructor(toolNames: ReadonlySet<string>);
62
+ /**
63
+ * Called by a mapped tool's `execute` at invocation. Returns a promise that
64
+ * settles when the consumer calls `submitToolResult` for `toolCallId`. It
65
+ * REJECTS only on teardown (so the run loop unblocks rather than hanging on a
66
+ * promise that will never settle); a normal settle — including an
67
+ * `isError: true` one — RESOLVES with the outcome, because a consumer-reported
68
+ * tool failure must reach the model as a recoverable tool output, not throw
69
+ * out of the run loop (the {@link ToolResultInfo.isError} contract). The
70
+ * mapper's `execute` turns the outcome into the model-visible value. A park
71
+ * after teardown rejects immediately.
72
+ */
73
+ park(toolCallId: string): Promise<ConsumerToolOutcome>;
74
+ /**
75
+ * Record that the coordinator surfaced `toolCallId` as a `tool-call`
76
+ * ChatEvent — so a `submitToolResult` that races ahead of the parker (the
77
+ * SDK emits `tool_called` before awaiting `execute`) is buffered rather than
78
+ * rejected.
79
+ */
80
+ markEmitted(toolCallId: string): void;
81
+ /**
82
+ * Settle a consumer tool call with the consumer's result. Resolves the parked
83
+ * `execute` if it has registered; otherwise buffers the outcome for the
84
+ * imminent `park` (the emit-before-await race) when the id was surfaced this
85
+ * turn. Idempotent (#589); throws `TOOL_CALL_NOT_FOUND` for an id the
86
+ * coordinator never surfaced.
87
+ */
88
+ settle(toolCallId: string, outcome: ConsumerToolOutcome): void;
89
+ /**
90
+ * The consumer-tool call ids settled with `isError: true` this turn — the
91
+ * records the coordinator stamps `isError` on after the run settles, since
92
+ * the run loop persists the `function_call_result` without it.
93
+ */
94
+ erroredToolCallIds(): readonly string[];
95
+ /**
96
+ * Reject every parked call on teardown so the run loop unblocks instead of
97
+ * hanging on a promise that will never settle. Marks all pending ids settled
98
+ * so a late consumer `submitToolResult` is a silent no-op (#589).
99
+ */
100
+ rejectAll(error: Error): void;
101
+ }
102
+ /**
103
+ * Map an agent's consumer-tool `ToolDefinition[]` to `@openai/agents` function
104
+ * tools whose `execute` parks on the shared {@link ConsumerToolRegistry}. Every
105
+ * tool gets `needsApproval: false` — consumer tools are the consumer's
106
+ * responsibility via `submitToolResult` and are NEVER approval-gated (the
107
+ * dual-enforcement contract: the coordinator also short-circuits them by name
108
+ * before consulting the policy resolver).
109
+ *
110
+ * The mapped `execute` reads its own `toolCallId` from `details.toolCall.callId`
111
+ * (populated by the SDK before invocation) and parks under it; the returned
112
+ * value becomes the tool's output the model sees.
113
+ *
114
+ * When `redaction` is supplied (the agent has an `onToolResult` hook), the
115
+ * parked result passes through the redactor at this seam — before it becomes the
116
+ * model-visible output — with full fidelity (`agentId` / `threadId` /
117
+ * `toolCallId` / `toolName` / `serverName` all known). A redactor throw
118
+ * propagates out of `execute` (the pump's `catch` synthesizes the terminal
119
+ * error + finish pair), per the SDK's throws-propagate contract.
120
+ */
121
+ export declare function mapConsumerTools(defs: ToolDefinition[], registry: ConsumerToolRegistry, redaction?: RedactionContext): Tool[];