@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,79 @@
|
|
|
1
|
+
import { type Message, type MessagePart } from '@salesforce/sfdx-agent-sdk';
|
|
2
|
+
import type { SessionRecord } from './openai-session-store.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bridges the `@openai/agents` flat `AgentInputItem[]` history and the SDK's
|
|
5
|
+
* `Message[]` contract in both directions.
|
|
6
|
+
*
|
|
7
|
+
* **Read** ({@link recordsToMessages}): the OpenAI run loop persists a completed
|
|
8
|
+
* tool call as a `function_call` item and a separate `function_call_result` item
|
|
9
|
+
* sharing `callId`. Each item maps to its own `Message`;
|
|
10
|
+
* the shared {@link splitToolResultsIntoToolMessages} normalizer then produces
|
|
11
|
+
* the canonical two-message layout (`assistant`(tool-call) + separate
|
|
12
|
+
* `role:'tool'`(tool-result), #647). Running the shared normalizer — rather than
|
|
13
|
+
* hand-emitting the layout — is the single-site contract that keeps the three
|
|
14
|
+
* harnesses from drifting. `createdAt` rides on the {@link SessionRecord}; a
|
|
15
|
+
* tool result's `isError` rides on the record too and is restored onto the
|
|
16
|
+
* `tool-result` part.
|
|
17
|
+
*
|
|
18
|
+
* **Write** ({@link messagesToRecords}): the `addContext` path. Each `Message`
|
|
19
|
+
* part lowers to a flat item — a `tool-call` part → `function_call`, a
|
|
20
|
+
* `tool-result` part → `function_call_result`, text/reasoning → message /
|
|
21
|
+
* reasoning items. OpenAI stores these separately natively, so — unlike the
|
|
22
|
+
* Mastra harness, which coalesces call+result into one object and needs
|
|
23
|
+
* `mergeToolResultsIntoAssistant` on write — no merge step is required; the split
|
|
24
|
+
* IS the native representation, and a `getMessages()` → `addContext()` round-trip
|
|
25
|
+
* is lossless.
|
|
26
|
+
*
|
|
27
|
+
* **Supported parts:** text, reasoning, and tool parts. Multimodal
|
|
28
|
+
* (`image` / `file`) parts are rejected with `INVALID_MESSAGE_CONTENT`, matching
|
|
29
|
+
* the text-only stream input the harness accepts; multimodal is not yet
|
|
30
|
+
* implemented.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Map persisted records to the canonical `Message[]` layout. Does NOT sort — the
|
|
34
|
+
* caller sorts ascending by `createdAt` after the normalizer (the #464 read
|
|
35
|
+
* contract), so the normalizer's inherited-`createdAt` tool messages land
|
|
36
|
+
* adjacent to their originating call under a stable sort.
|
|
37
|
+
*/
|
|
38
|
+
export declare function recordsToMessages(records: SessionRecord[]): Message[];
|
|
39
|
+
/**
|
|
40
|
+
* Lower `Message[]` (already `createdAt`-backfilled by the caller) to
|
|
41
|
+
* {@link SessionRecord}s. Each message's parts expand to one or more flat items;
|
|
42
|
+
* every record inherits its source message's `createdAt`, so a message that
|
|
43
|
+
* expands to a `function_call` + `function_call_result` pair keeps them adjacent
|
|
44
|
+
* under the read-side stable sort (equal `createdAt` → insertion order).
|
|
45
|
+
*/
|
|
46
|
+
export declare function messagesToRecords(messages: Message[]): SessionRecord[];
|
|
47
|
+
/**
|
|
48
|
+
* Render a {@link MessagePart} as a plain-text transcript fragment. Byte-identical
|
|
49
|
+
* to the Mastra and Claude harnesses' copies — the shared
|
|
50
|
+
* `@salesforce/harness-conformance` `TRANSCRIPT_FIXTURES` table is the drift-guard
|
|
51
|
+
* (cross-harness imports are lint-blocked, so each harness keeps its own copy).
|
|
52
|
+
*
|
|
53
|
+
* Tool args/results render verbatim (the compaction char budget is the safety
|
|
54
|
+
* net, not per-part truncation). Image/file parts render as a size-aware
|
|
55
|
+
* placeholder and NEVER inline their base64 blob.
|
|
56
|
+
*/
|
|
57
|
+
export declare function partToTranscriptText(part: MessagePart): string;
|
|
58
|
+
/**
|
|
59
|
+
* Maximum flattened-transcript length (in characters) sent to the model in a
|
|
60
|
+
* single `compactThread` summarization call. Transcripts above this size are
|
|
61
|
+
* split into roughly-equal chunks, each chunk is summarized separately, and the
|
|
62
|
+
* concatenated chunk-summaries are then summarized into the final summary.
|
|
63
|
+
*
|
|
64
|
+
* ~400k characters maps to ~100k tokens at a conservative 4:1 chars-per-token
|
|
65
|
+
* ratio, leaving headroom under typical 200k-token model context windows for the
|
|
66
|
+
* prompt scaffolding and the model's own response. A byte-identical copy of the
|
|
67
|
+
* Mastra and Claude harnesses' constant — cross-harness imports are lint-blocked,
|
|
68
|
+
* so each harness keeps its own copy.
|
|
69
|
+
*/
|
|
70
|
+
export declare const MAX_TRANSCRIPT_CHARS = 400000;
|
|
71
|
+
/**
|
|
72
|
+
* Splits a string into ≤ `maxChars`-sized chunks, preferring line boundaries so
|
|
73
|
+
* message turns are not torn across chunks. Falls back to a hard slice if a
|
|
74
|
+
* single line itself exceeds the budget. A byte-identical copy of the sibling
|
|
75
|
+
* harnesses' `splitIntoChunks` (drift-guarded socially, not by a shared table —
|
|
76
|
+
* the transcript conformance fixtures cover `partToTranscriptText`, not the
|
|
77
|
+
* chunker).
|
|
78
|
+
*/
|
|
79
|
+
export declare function splitIntoChunks(text: string, maxChars: number): string[];
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { AgentSDKError, AgentSDKErrorType } from '@salesforce/sfdx-agent-sdk';
|
|
6
|
+
import { splitToolResultsIntoToolMessages } from '@salesforce/sfdx-agent-sdk/harness';
|
|
7
|
+
/**
|
|
8
|
+
* Bridges the `@openai/agents` flat `AgentInputItem[]` history and the SDK's
|
|
9
|
+
* `Message[]` contract in both directions.
|
|
10
|
+
*
|
|
11
|
+
* **Read** ({@link recordsToMessages}): the OpenAI run loop persists a completed
|
|
12
|
+
* tool call as a `function_call` item and a separate `function_call_result` item
|
|
13
|
+
* sharing `callId`. Each item maps to its own `Message`;
|
|
14
|
+
* the shared {@link splitToolResultsIntoToolMessages} normalizer then produces
|
|
15
|
+
* the canonical two-message layout (`assistant`(tool-call) + separate
|
|
16
|
+
* `role:'tool'`(tool-result), #647). Running the shared normalizer — rather than
|
|
17
|
+
* hand-emitting the layout — is the single-site contract that keeps the three
|
|
18
|
+
* harnesses from drifting. `createdAt` rides on the {@link SessionRecord}; a
|
|
19
|
+
* tool result's `isError` rides on the record too and is restored onto the
|
|
20
|
+
* `tool-result` part.
|
|
21
|
+
*
|
|
22
|
+
* **Write** ({@link messagesToRecords}): the `addContext` path. Each `Message`
|
|
23
|
+
* part lowers to a flat item — a `tool-call` part → `function_call`, a
|
|
24
|
+
* `tool-result` part → `function_call_result`, text/reasoning → message /
|
|
25
|
+
* reasoning items. OpenAI stores these separately natively, so — unlike the
|
|
26
|
+
* Mastra harness, which coalesces call+result into one object and needs
|
|
27
|
+
* `mergeToolResultsIntoAssistant` on write — no merge step is required; the split
|
|
28
|
+
* IS the native representation, and a `getMessages()` → `addContext()` round-trip
|
|
29
|
+
* is lossless.
|
|
30
|
+
*
|
|
31
|
+
* **Supported parts:** text, reasoning, and tool parts. Multimodal
|
|
32
|
+
* (`image` / `file`) parts are rejected with `INVALID_MESSAGE_CONTENT`, matching
|
|
33
|
+
* the text-only stream input the harness accepts; multimodal is not yet
|
|
34
|
+
* implemented.
|
|
35
|
+
*/
|
|
36
|
+
// ── Read path ────────────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Map persisted records to the canonical `Message[]` layout. Does NOT sort — the
|
|
39
|
+
* caller sorts ascending by `createdAt` after the normalizer (the #464 read
|
|
40
|
+
* contract), so the normalizer's inherited-`createdAt` tool messages land
|
|
41
|
+
* adjacent to their originating call under a stable sort.
|
|
42
|
+
*/
|
|
43
|
+
export function recordsToMessages(records) {
|
|
44
|
+
const mapped = [];
|
|
45
|
+
records.forEach((record, index) => {
|
|
46
|
+
const message = itemToMessage(record, index);
|
|
47
|
+
if (message !== undefined)
|
|
48
|
+
mapped.push(message);
|
|
49
|
+
});
|
|
50
|
+
return splitToolResultsIntoToolMessages(mapped);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Map one persisted record to a `Message`, or `undefined` for an item type with
|
|
54
|
+
* no SDK-message equivalent (compaction markers, hosted-tool calls, etc.) so
|
|
55
|
+
* history retrieval never crashes on an unmapped upstream item.
|
|
56
|
+
*/
|
|
57
|
+
function itemToMessage(record, index) {
|
|
58
|
+
const item = record.item;
|
|
59
|
+
const createdAt = new Date(record.createdAt);
|
|
60
|
+
// Message items carry a `role`; every other item type is discriminated on
|
|
61
|
+
// `type`. (A `function_call` has no `role`, so this order is unambiguous.)
|
|
62
|
+
if (item.role === 'user' || item.role === 'assistant' || item.role === 'system') {
|
|
63
|
+
return messageItemToMessage(item, item.role, createdAt, index);
|
|
64
|
+
}
|
|
65
|
+
switch (item.type) {
|
|
66
|
+
case 'function_call': {
|
|
67
|
+
if (item.callId === undefined || item.name === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
const part = {
|
|
70
|
+
type: 'tool-call',
|
|
71
|
+
toolCallId: item.callId,
|
|
72
|
+
toolName: item.name,
|
|
73
|
+
args: parseArgs(item.arguments),
|
|
74
|
+
};
|
|
75
|
+
return { id: item.id ?? `toolcall-${item.callId}`, role: 'assistant', content: [part], createdAt };
|
|
76
|
+
}
|
|
77
|
+
case 'function_call_result': {
|
|
78
|
+
if (item.callId === undefined || item.name === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
const part = {
|
|
81
|
+
type: 'tool-result',
|
|
82
|
+
toolCallId: item.callId,
|
|
83
|
+
toolName: item.name,
|
|
84
|
+
result: normalizeToolOutput(item.output),
|
|
85
|
+
...(record.isError === true ? { isError: true } : {}),
|
|
86
|
+
};
|
|
87
|
+
// Emit on a non-`tool` message so the shared normalizer hoists it
|
|
88
|
+
// onto its own trailing `role:'tool'` message (the single-site
|
|
89
|
+
// layout contract). Assistant is the pre-normalizer carrier here;
|
|
90
|
+
// the normalizer drops this now-empty message and emits the tool one.
|
|
91
|
+
return { id: item.id ?? `toolresult-${item.callId}`, role: 'assistant', content: [part], createdAt };
|
|
92
|
+
}
|
|
93
|
+
case 'reasoning': {
|
|
94
|
+
const text = readTextContent(item.content);
|
|
95
|
+
return {
|
|
96
|
+
id: item.id ?? `reasoning-${index}`,
|
|
97
|
+
role: 'assistant',
|
|
98
|
+
content: [{ type: 'reasoning', text }],
|
|
99
|
+
createdAt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
// Unmapped item type (hosted_tool_call, computer_call, compaction,
|
|
104
|
+
// unknown, …): drop it from history rather than surface an
|
|
105
|
+
// un-renderable message.
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function messageItemToMessage(item, role, createdAt, index) {
|
|
110
|
+
const id = item.id ?? `msg-${index}`;
|
|
111
|
+
if (typeof item.content === 'string') {
|
|
112
|
+
return { id, role, content: item.content, createdAt };
|
|
113
|
+
}
|
|
114
|
+
const parts = [];
|
|
115
|
+
for (const c of item.content ?? []) {
|
|
116
|
+
if (typeof c !== 'object' || c === null)
|
|
117
|
+
continue;
|
|
118
|
+
const block = c;
|
|
119
|
+
// Both input (`input_text`) and output (`output_text`) text carry `text`.
|
|
120
|
+
if ((block.type === 'input_text' || block.type === 'output_text') && typeof block.text === 'string') {
|
|
121
|
+
parts.push({ type: 'text', text: block.text });
|
|
122
|
+
}
|
|
123
|
+
else if (block.type === 'refusal' && typeof block.refusal === 'string') {
|
|
124
|
+
parts.push({ type: 'text', text: block.refusal });
|
|
125
|
+
}
|
|
126
|
+
// Non-text content (image/audio/file) is not mapped.
|
|
127
|
+
}
|
|
128
|
+
// Collapse a lone text part to string content (the shape consumers wrote and
|
|
129
|
+
// the shape #464's history test asserts via `typeof content === 'string'`).
|
|
130
|
+
if (parts.length === 1 && parts[0].type === 'text') {
|
|
131
|
+
return { id, role, content: parts[0].text, createdAt };
|
|
132
|
+
}
|
|
133
|
+
return { id, role, content: parts, createdAt };
|
|
134
|
+
}
|
|
135
|
+
/** Join the `text` fields of a `reasoning` item's content array. */
|
|
136
|
+
function readTextContent(content) {
|
|
137
|
+
if (typeof content === 'string' || content === undefined)
|
|
138
|
+
return typeof content === 'string' ? content : '';
|
|
139
|
+
return content
|
|
140
|
+
.map((c) => typeof c === 'object' && c !== null && typeof c.text === 'string'
|
|
141
|
+
? c.text
|
|
142
|
+
: '')
|
|
143
|
+
.join('');
|
|
144
|
+
}
|
|
145
|
+
// ── Write path (addContext) ────────────────────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Lower `Message[]` (already `createdAt`-backfilled by the caller) to
|
|
148
|
+
* {@link SessionRecord}s. Each message's parts expand to one or more flat items;
|
|
149
|
+
* every record inherits its source message's `createdAt`, so a message that
|
|
150
|
+
* expands to a `function_call` + `function_call_result` pair keeps them adjacent
|
|
151
|
+
* under the read-side stable sort (equal `createdAt` → insertion order).
|
|
152
|
+
*/
|
|
153
|
+
export function messagesToRecords(messages) {
|
|
154
|
+
const records = [];
|
|
155
|
+
for (const message of messages) {
|
|
156
|
+
if (message.createdAt === undefined) {
|
|
157
|
+
// Internal contract: the caller (`addContext`) runs the shared
|
|
158
|
+
// `backfillCreatedAt` before reaching this mapper, so every message
|
|
159
|
+
// carries a `createdAt`. Throwing here surfaces a missing-backfill
|
|
160
|
+
// bug at its real call site rather than persisting a record whose
|
|
161
|
+
// timestamp would break the `getMessages` ascending sort. Mirrors
|
|
162
|
+
// Mastra's `mapMessageToMastra` guard.
|
|
163
|
+
throw new AgentSDKError(`messagesToRecords: message "${message.id}" is missing createdAt (caller must backfill before mapping).`, AgentSDKErrorType.INVALID_MESSAGE_CONTENT);
|
|
164
|
+
}
|
|
165
|
+
const createdAt = message.createdAt.toISOString();
|
|
166
|
+
for (const record of messageToItems(message)) {
|
|
167
|
+
records.push({ ...record, createdAt });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return records;
|
|
171
|
+
}
|
|
172
|
+
/** Lower one message to flat items, dropping `createdAt` (added by the caller). */
|
|
173
|
+
function messageToItems(message) {
|
|
174
|
+
if (typeof message.content === 'string') {
|
|
175
|
+
return [{ item: buildMessageItem(message.role, message.content) }];
|
|
176
|
+
}
|
|
177
|
+
const out = [];
|
|
178
|
+
const textRun = [];
|
|
179
|
+
const flushText = () => {
|
|
180
|
+
if (textRun.length === 0)
|
|
181
|
+
return;
|
|
182
|
+
out.push({ item: buildMessageItem(message.role, textRun.join('')) });
|
|
183
|
+
textRun.length = 0;
|
|
184
|
+
};
|
|
185
|
+
for (const part of message.content) {
|
|
186
|
+
switch (part.type) {
|
|
187
|
+
case 'text':
|
|
188
|
+
textRun.push(part.text);
|
|
189
|
+
break;
|
|
190
|
+
case 'reasoning':
|
|
191
|
+
flushText();
|
|
192
|
+
out.push({
|
|
193
|
+
item: { type: 'reasoning', content: [{ type: 'input_text', text: part.text }] },
|
|
194
|
+
});
|
|
195
|
+
break;
|
|
196
|
+
case 'tool-call':
|
|
197
|
+
flushText();
|
|
198
|
+
out.push({
|
|
199
|
+
item: {
|
|
200
|
+
type: 'function_call',
|
|
201
|
+
callId: part.toolCallId,
|
|
202
|
+
name: part.toolName,
|
|
203
|
+
arguments: JSON.stringify(part.args ?? {}),
|
|
204
|
+
status: 'completed',
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
case 'tool-result':
|
|
209
|
+
flushText();
|
|
210
|
+
out.push({
|
|
211
|
+
item: {
|
|
212
|
+
type: 'function_call_result',
|
|
213
|
+
callId: part.toolCallId,
|
|
214
|
+
name: part.toolName,
|
|
215
|
+
status: 'completed',
|
|
216
|
+
output: toOutputString(part.result),
|
|
217
|
+
},
|
|
218
|
+
...(part.isError === true ? { isError: true } : {}),
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
case 'image':
|
|
222
|
+
case 'file':
|
|
223
|
+
throw new AgentSDKError(`The OpenAI harness does not yet support "${part.type}" message parts in addContext (multimodal is not yet implemented).`, AgentSDKErrorType.INVALID_MESSAGE_CONTENT);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
flushText();
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
/** Build the message-shaped item for a role + plain text content. */
|
|
230
|
+
function buildMessageItem(role, text) {
|
|
231
|
+
switch (role) {
|
|
232
|
+
case 'assistant':
|
|
233
|
+
return {
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
status: 'completed',
|
|
236
|
+
content: [{ type: 'output_text', text }],
|
|
237
|
+
};
|
|
238
|
+
case 'system':
|
|
239
|
+
return { role: 'system', content: text };
|
|
240
|
+
case 'user':
|
|
241
|
+
case 'tool':
|
|
242
|
+
// A `role:'tool'` message carries only tool-result parts (handled
|
|
243
|
+
// above); its own text content, if any, lowers to a user turn so the
|
|
244
|
+
// model sees it. Plain user text is the common case.
|
|
245
|
+
return { role: 'user', content: text };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ── Transcript rendering (compaction + conformance drift-guard) ────────────────
|
|
249
|
+
/**
|
|
250
|
+
* Render a {@link MessagePart} as a plain-text transcript fragment. Byte-identical
|
|
251
|
+
* to the Mastra and Claude harnesses' copies — the shared
|
|
252
|
+
* `@salesforce/harness-conformance` `TRANSCRIPT_FIXTURES` table is the drift-guard
|
|
253
|
+
* (cross-harness imports are lint-blocked, so each harness keeps its own copy).
|
|
254
|
+
*
|
|
255
|
+
* Tool args/results render verbatim (the compaction char budget is the safety
|
|
256
|
+
* net, not per-part truncation). Image/file parts render as a size-aware
|
|
257
|
+
* placeholder and NEVER inline their base64 blob.
|
|
258
|
+
*/
|
|
259
|
+
export function partToTranscriptText(part) {
|
|
260
|
+
switch (part.type) {
|
|
261
|
+
case 'text':
|
|
262
|
+
case 'reasoning':
|
|
263
|
+
return part.text;
|
|
264
|
+
case 'tool-call': {
|
|
265
|
+
const argsStr = JSON.stringify(part.args);
|
|
266
|
+
return `[tool-call: ${part.toolName}(${argsStr})]`;
|
|
267
|
+
}
|
|
268
|
+
case 'tool-result': {
|
|
269
|
+
const resultStr = typeof part.result === 'string' ? part.result : JSON.stringify(part.result);
|
|
270
|
+
return `[tool-result: ${part.toolName} → ${resultStr}]`;
|
|
271
|
+
}
|
|
272
|
+
case 'image':
|
|
273
|
+
case 'file': {
|
|
274
|
+
const name = part.fileName ? `, name="${part.fileName}"` : '';
|
|
275
|
+
return `[${part.type}: ${part.mimeType}${name}, ~${formatApproxSize(part.data)}]`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Approximate decoded size of base64 `data` as a short human-readable string
|
|
281
|
+
* (`768 B` / `1.2 KB` / `15.0 MB`). Uses the `length * 0.75` approximation the
|
|
282
|
+
* gateway SDK uses; the transcript only needs a ballpark, never the exact count.
|
|
283
|
+
*/
|
|
284
|
+
function formatApproxSize(base64Data) {
|
|
285
|
+
const bytes = Math.floor(base64Data.length * 0.75);
|
|
286
|
+
if (bytes < 1024)
|
|
287
|
+
return `${bytes} B`;
|
|
288
|
+
const kib = bytes / 1024;
|
|
289
|
+
if (kib < 1024)
|
|
290
|
+
return `${kib.toFixed(1)} KB`;
|
|
291
|
+
return `${(kib / 1024).toFixed(1)} MB`;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Maximum flattened-transcript length (in characters) sent to the model in a
|
|
295
|
+
* single `compactThread` summarization call. Transcripts above this size are
|
|
296
|
+
* split into roughly-equal chunks, each chunk is summarized separately, and the
|
|
297
|
+
* concatenated chunk-summaries are then summarized into the final summary.
|
|
298
|
+
*
|
|
299
|
+
* ~400k characters maps to ~100k tokens at a conservative 4:1 chars-per-token
|
|
300
|
+
* ratio, leaving headroom under typical 200k-token model context windows for the
|
|
301
|
+
* prompt scaffolding and the model's own response. A byte-identical copy of the
|
|
302
|
+
* Mastra and Claude harnesses' constant — cross-harness imports are lint-blocked,
|
|
303
|
+
* so each harness keeps its own copy.
|
|
304
|
+
*/
|
|
305
|
+
export const MAX_TRANSCRIPT_CHARS = 400_000;
|
|
306
|
+
/**
|
|
307
|
+
* Splits a string into ≤ `maxChars`-sized chunks, preferring line boundaries so
|
|
308
|
+
* message turns are not torn across chunks. Falls back to a hard slice if a
|
|
309
|
+
* single line itself exceeds the budget. A byte-identical copy of the sibling
|
|
310
|
+
* harnesses' `splitIntoChunks` (drift-guarded socially, not by a shared table —
|
|
311
|
+
* the transcript conformance fixtures cover `partToTranscriptText`, not the
|
|
312
|
+
* chunker).
|
|
313
|
+
*/
|
|
314
|
+
export function splitIntoChunks(text, maxChars) {
|
|
315
|
+
const chunks = [];
|
|
316
|
+
const lines = text.split('\n');
|
|
317
|
+
let current = '';
|
|
318
|
+
for (const line of lines) {
|
|
319
|
+
if (line.length > maxChars) {
|
|
320
|
+
if (current.length > 0) {
|
|
321
|
+
chunks.push(current);
|
|
322
|
+
current = '';
|
|
323
|
+
}
|
|
324
|
+
for (let i = 0; i < line.length; i += maxChars) {
|
|
325
|
+
chunks.push(line.slice(i, i + maxChars));
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const candidate = current.length === 0 ? line : current + '\n' + line;
|
|
330
|
+
if (candidate.length > maxChars) {
|
|
331
|
+
chunks.push(current);
|
|
332
|
+
current = line;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
current = candidate;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (current.length > 0) {
|
|
339
|
+
chunks.push(current);
|
|
340
|
+
}
|
|
341
|
+
return chunks;
|
|
342
|
+
}
|
|
343
|
+
/** Parse a `function_call` item's JSON `arguments` string into an args object. */
|
|
344
|
+
function parseArgs(raw) {
|
|
345
|
+
if (typeof raw !== 'string')
|
|
346
|
+
return {};
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(raw);
|
|
349
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Normalize a `function_call_result` `output` (which the protocol types as
|
|
357
|
+
* `string | { type:'text'; text } | image | file | array`) to the value carried
|
|
358
|
+
* on the SDK `tool-result` part. A bare string or `{ type:'text' }` collapses to
|
|
359
|
+
* its text; any other structured shape passes through as-is (`result` is
|
|
360
|
+
* `unknown` on the SDK contract, so consumers keep whatever the tool produced).
|
|
361
|
+
*/
|
|
362
|
+
function normalizeToolOutput(output) {
|
|
363
|
+
if (typeof output === 'string')
|
|
364
|
+
return output;
|
|
365
|
+
if (typeof output === 'object' && output !== null && output.type === 'text') {
|
|
366
|
+
return output.text ?? '';
|
|
367
|
+
}
|
|
368
|
+
return output;
|
|
369
|
+
}
|
|
370
|
+
/** Coerce a `tool-result` part's `result` to the string `output` an item stores. */
|
|
371
|
+
function toOutputString(result) {
|
|
372
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
373
|
+
}
|
|
374
|
+
//# sourceMappingURL=openai-message-mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-message-mapper.js","sourceRoot":"","sources":["../src/openai-message-mapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ModelProvider } from '@openai/agents';
|
|
2
|
+
import { type Clock } from '@salesforce/agentic-common';
|
|
3
|
+
import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
4
|
+
/**
|
|
5
|
+
* Per-request wire-communication hooks. When supplied to
|
|
6
|
+
* {@link buildOpenAIModelProvider}, the custom fetch emits one
|
|
7
|
+
* {@link WireCommunicationEvent} `llm-request` before each outbound call and one
|
|
8
|
+
* `llm-response` after the upstream replies (or the call rejects). The harness
|
|
9
|
+
* forwards each event onto its `HarnessBusOwner.wireBus`, which is what
|
|
10
|
+
* `AgentManager.onWireCommunication` aggregates. Omit to disable emission
|
|
11
|
+
* entirely (zero cost — the fetch takes its plain header-merge path).
|
|
12
|
+
*/
|
|
13
|
+
export type WireHooks = {
|
|
14
|
+
/** Called once with each emitted event (the harness's disposal-guarded emitter). */
|
|
15
|
+
emitWireCommunication: (event: WireCommunicationEvent) => void;
|
|
16
|
+
/** Time source for the event `timestamp`s; defaults to a fresh `RealClock`. */
|
|
17
|
+
clock?: Clock;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Builds a `@openai/agents` {@link ModelProvider} bound to the live
|
|
21
|
+
* {@link ModelConnectivityInfo} bag.
|
|
22
|
+
*
|
|
23
|
+
* The provider constructs one `OpenAI` client pointed at `info.baseUrl` with a
|
|
24
|
+
* custom `fetch` that merges `await info.getHeaders()` into every outbound
|
|
25
|
+
* request — the per-request JWT-rotation seam.
|
|
26
|
+
* `getInfo` is read on each `getModel()` call so a within-shape `modelId` swap
|
|
27
|
+
* or JWT rotation between turns lands without rebuilding anything; the fetch
|
|
28
|
+
* wrapper additionally re-reads `getHeaders()` per HTTP call, so credential
|
|
29
|
+
* rotation lands on the next request even within a single turn.
|
|
30
|
+
*
|
|
31
|
+
* HTTP transport is pinned via {@link setOpenAIResponsesTransport} so the custom
|
|
32
|
+
* `fetch` is the only wire seam — a WebSocket transport would bypass the
|
|
33
|
+
* per-request header hook and auth would not rotate. That single wire seam is
|
|
34
|
+
* also where wire-communication events emit (when {@link WireHooks} is supplied).
|
|
35
|
+
*
|
|
36
|
+
* Mirrors the shipping Mastra `pass-through-openai.ts` connectivity seam; the
|
|
37
|
+
* only difference is that this harness hands the client to the OpenAI Agents
|
|
38
|
+
* SDK's own `OpenAIProvider` rather than wrapping it in a `LanguageModelV3`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildOpenAIModelProvider(opts: {
|
|
41
|
+
getInfo: () => ModelConnectivityInfo;
|
|
42
|
+
/** Proxy-aware inner fetch from the factory (honors HTTPS_PROXY / NO_PROXY). */
|
|
43
|
+
innerFetch?: typeof fetch;
|
|
44
|
+
/** Wire-communication emission hooks. Omit to disable emission. */
|
|
45
|
+
wireHooks?: WireHooks;
|
|
46
|
+
}): ModelProvider;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import { OpenAIProvider, setOpenAIResponsesTransport } from '@openai/agents';
|
|
7
|
+
import { RealClock } from '@salesforce/agentic-common';
|
|
8
|
+
/**
|
|
9
|
+
* Builds a `@openai/agents` {@link ModelProvider} bound to the live
|
|
10
|
+
* {@link ModelConnectivityInfo} bag.
|
|
11
|
+
*
|
|
12
|
+
* The provider constructs one `OpenAI` client pointed at `info.baseUrl` with a
|
|
13
|
+
* custom `fetch` that merges `await info.getHeaders()` into every outbound
|
|
14
|
+
* request — the per-request JWT-rotation seam.
|
|
15
|
+
* `getInfo` is read on each `getModel()` call so a within-shape `modelId` swap
|
|
16
|
+
* or JWT rotation between turns lands without rebuilding anything; the fetch
|
|
17
|
+
* wrapper additionally re-reads `getHeaders()` per HTTP call, so credential
|
|
18
|
+
* rotation lands on the next request even within a single turn.
|
|
19
|
+
*
|
|
20
|
+
* HTTP transport is pinned via {@link setOpenAIResponsesTransport} so the custom
|
|
21
|
+
* `fetch` is the only wire seam — a WebSocket transport would bypass the
|
|
22
|
+
* per-request header hook and auth would not rotate. That single wire seam is
|
|
23
|
+
* also where wire-communication events emit (when {@link WireHooks} is supplied).
|
|
24
|
+
*
|
|
25
|
+
* Mirrors the shipping Mastra `pass-through-openai.ts` connectivity seam; the
|
|
26
|
+
* only difference is that this harness hands the client to the OpenAI Agents
|
|
27
|
+
* SDK's own `OpenAIProvider` rather than wrapping it in a `LanguageModelV3`.
|
|
28
|
+
*/
|
|
29
|
+
export function buildOpenAIModelProvider(opts) {
|
|
30
|
+
const { getInfo, innerFetch, wireHooks } = opts;
|
|
31
|
+
// Pin the HTTP transport (never WebSocket) so the custom `fetch` below is the
|
|
32
|
+
// connectivity seam. Idempotent and safe to call per provider construction.
|
|
33
|
+
setOpenAIResponsesTransport('http');
|
|
34
|
+
return {
|
|
35
|
+
getModel(modelName) {
|
|
36
|
+
const info = getInfo();
|
|
37
|
+
const client = new OpenAI({
|
|
38
|
+
baseURL: info.baseUrl,
|
|
39
|
+
// The custom fetch injects the resolver-built `Authorization` header
|
|
40
|
+
// per request. The SDK still wants a non-empty `apiKey`; passing an
|
|
41
|
+
// opaque sentinel keeps it from reading `process.env['OPENAI_API_KEY']`.
|
|
42
|
+
apiKey: 'salesforce-gateway-passthrough',
|
|
43
|
+
fetch: buildGatewayFetch(getInfo, innerFetch, wireHooks),
|
|
44
|
+
});
|
|
45
|
+
const provider = new OpenAIProvider({ openAIClient: client });
|
|
46
|
+
// Prefer the caller-requested model name (the run loop passes the
|
|
47
|
+
// agent's configured model); fall back to the live bag's native id.
|
|
48
|
+
return provider.getModel(modelName ?? info.nativeModelId);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Bounds `llm-request` body capture so a large payload can't blow a consumer's log budget. */
|
|
53
|
+
const TRACE_BODY_BYTE_BUDGET = 256 * 1024;
|
|
54
|
+
/**
|
|
55
|
+
* Returns a `fetch` that merges the resolver's fresh headers into every
|
|
56
|
+
* outbound request. `getHeaders()` is re-evaluated per call so JWT rotation /
|
|
57
|
+
* premium-rate-limit gate flips land on the next request without rebuilding the
|
|
58
|
+
* client. Delegates to the proxy-aware `innerFetch` when supplied (so
|
|
59
|
+
* `HTTPS_PROXY` is honored), else the global `fetch`.
|
|
60
|
+
*
|
|
61
|
+
* When `wireHooks` is supplied, every outbound call emits one `llm-request`
|
|
62
|
+
* event before the fetch and one `llm-response` after it resolves — or, on a
|
|
63
|
+
* transport failure, an `llm-response` with `status: 0` + the captured error,
|
|
64
|
+
* then rethrows (a wire hook never swallows the underlying fetch). Body capture
|
|
65
|
+
* is best-effort and bounded ({@link TRACE_BODY_BYTE_BUDGET}); a decode failure
|
|
66
|
+
* never blocks the call.
|
|
67
|
+
*/
|
|
68
|
+
function buildGatewayFetch(getInfo, innerFetch, wireHooks) {
|
|
69
|
+
const baseFetch = innerFetch ?? fetch;
|
|
70
|
+
return (async (input, init) => {
|
|
71
|
+
const info = getInfo();
|
|
72
|
+
const fresh = await info.getHeaders();
|
|
73
|
+
const headers = new Headers(init?.headers);
|
|
74
|
+
for (const [key, value] of Object.entries(fresh)) {
|
|
75
|
+
if (value != null)
|
|
76
|
+
headers.set(key, value);
|
|
77
|
+
}
|
|
78
|
+
const finalInit = { ...init, headers };
|
|
79
|
+
if (wireHooks === undefined) {
|
|
80
|
+
return baseFetch(input, finalInit);
|
|
81
|
+
}
|
|
82
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
83
|
+
const method = (finalInit.method ?? 'GET').toUpperCase();
|
|
84
|
+
const model = info.nativeModelId;
|
|
85
|
+
const clock = wireHooks.clock ?? new RealClock();
|
|
86
|
+
const requestStart = performance.now();
|
|
87
|
+
const body = tryDecodeBody(finalInit.body);
|
|
88
|
+
wireHooks.emitWireCommunication({
|
|
89
|
+
type: 'llm-request',
|
|
90
|
+
timestamp: clock.now(),
|
|
91
|
+
url,
|
|
92
|
+
method,
|
|
93
|
+
model,
|
|
94
|
+
...(body !== undefined ? { body } : {}),
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
const response = await baseFetch(input, finalInit);
|
|
98
|
+
wireHooks.emitWireCommunication({
|
|
99
|
+
type: 'llm-response',
|
|
100
|
+
timestamp: clock.now(),
|
|
101
|
+
model,
|
|
102
|
+
status: response.status,
|
|
103
|
+
totalDurationMs: Math.round(performance.now() - requestStart),
|
|
104
|
+
xClientTraceId: response.headers.get('x-client-trace-id') ?? undefined,
|
|
105
|
+
});
|
|
106
|
+
return response;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
wireHooks.emitWireCommunication({
|
|
110
|
+
type: 'llm-response',
|
|
111
|
+
timestamp: clock.now(),
|
|
112
|
+
model,
|
|
113
|
+
status: 0,
|
|
114
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
115
|
+
totalDurationMs: Math.round(performance.now() - requestStart),
|
|
116
|
+
});
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Best-effort decode of a request body for the `llm-request` event. Only string
|
|
123
|
+
* bodies are decoded (streams / FormData / Blob are skipped); a body over
|
|
124
|
+
* {@link TRACE_BODY_BYTE_BUDGET} is recorded as `{ truncated, byteLength }`
|
|
125
|
+
* rather than parsed. Returns `undefined` when there's nothing worth capturing.
|
|
126
|
+
*/
|
|
127
|
+
function tryDecodeBody(body) {
|
|
128
|
+
if (body === undefined || body === null)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (typeof body !== 'string')
|
|
131
|
+
return undefined;
|
|
132
|
+
const byteLength = Buffer.byteLength(body, 'utf8');
|
|
133
|
+
if (byteLength > TRACE_BODY_BYTE_BUDGET) {
|
|
134
|
+
return { truncated: true, byteLength };
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(body);
|
|
138
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : { value: parsed };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=openai-model-provider.js.map
|