@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 @@
|
|
|
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[];
|