@f5-sales-demo/pi-agent-core 21.29.0 → 21.29.2
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/package.json +3 -3
- package/src/agent-loop.ts +168 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-agent-core",
|
|
4
|
-
"version": "21.29.
|
|
4
|
+
"version": "21.29.2",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@f5-sales-demo/pi-ai": "21.29.
|
|
39
|
-
"@f5-sales-demo/pi-utils": "21.29.
|
|
38
|
+
"@f5-sales-demo/pi-ai": "21.29.2",
|
|
39
|
+
"@f5-sales-demo/pi-utils": "21.29.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@sinclair/typebox": "0.34.52",
|
package/src/agent-loop.ts
CHANGED
|
@@ -127,6 +127,125 @@ function normalizeMessagesForProvider(
|
|
|
127
127
|
return changed ? normalized : messages;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function exactToolName(choice: AgentLoopConfig["toolChoice"]): string | undefined {
|
|
131
|
+
if (!choice || typeof choice === "string") return undefined;
|
|
132
|
+
if (choice.type === "tool") return choice.name;
|
|
133
|
+
return "function" in choice ? choice.function.name : choice.name;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function forcedToolRetryContext(context: Context, toolName: string): Context {
|
|
137
|
+
return {
|
|
138
|
+
...context,
|
|
139
|
+
messages: [
|
|
140
|
+
...context.messages,
|
|
141
|
+
{
|
|
142
|
+
role: "developer",
|
|
143
|
+
content: `The previous response ended before the required tool call. Call ${toolName} now without explanatory text.`,
|
|
144
|
+
attribution: "agent",
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface BufferedAssistantResponse {
|
|
152
|
+
message: AssistantMessage;
|
|
153
|
+
events: Array<Extract<AgentEvent, { type: "message_start" | "message_update" | "message_end" }>>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function emptyUsage(): AssistantMessage["usage"] {
|
|
157
|
+
return {
|
|
158
|
+
input: 0,
|
|
159
|
+
output: 0,
|
|
160
|
+
cacheRead: 0,
|
|
161
|
+
cacheWrite: 0,
|
|
162
|
+
totalTokens: 0,
|
|
163
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sanitizedForcedToolMessage(
|
|
168
|
+
model: AgentLoopConfig["model"],
|
|
169
|
+
stopReason: "aborted" | "error",
|
|
170
|
+
): AssistantMessage {
|
|
171
|
+
return {
|
|
172
|
+
role: "assistant",
|
|
173
|
+
content: [],
|
|
174
|
+
api: model.api,
|
|
175
|
+
provider: model.provider,
|
|
176
|
+
model: model.id,
|
|
177
|
+
usage: emptyUsage(),
|
|
178
|
+
stopReason,
|
|
179
|
+
...(stopReason === "error"
|
|
180
|
+
? { errorMessage: "Required tool invocation failed." }
|
|
181
|
+
: { errorMessage: "Request was aborted" }),
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function bufferAssistantResponse(
|
|
187
|
+
response: Awaited<ReturnType<StreamFn>>,
|
|
188
|
+
model: AgentLoopConfig["model"],
|
|
189
|
+
signal: AbortSignal | undefined,
|
|
190
|
+
): Promise<BufferedAssistantResponse> {
|
|
191
|
+
const events: BufferedAssistantResponse["events"] = [];
|
|
192
|
+
let partialMessage: AssistantMessage | null = null;
|
|
193
|
+
let addedPartial = false;
|
|
194
|
+
|
|
195
|
+
for await (const event of response) {
|
|
196
|
+
if (signal?.aborted) {
|
|
197
|
+
const message = sanitizedForcedToolMessage(model, "aborted");
|
|
198
|
+
return {
|
|
199
|
+
message,
|
|
200
|
+
events: [
|
|
201
|
+
{ type: "message_start", message: { ...message } },
|
|
202
|
+
{ type: "message_end", message },
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
switch (event.type) {
|
|
208
|
+
case "start":
|
|
209
|
+
partialMessage = event.partial;
|
|
210
|
+
addedPartial = true;
|
|
211
|
+
events.push({ type: "message_start", message: { ...partialMessage } as AssistantMessage });
|
|
212
|
+
break;
|
|
213
|
+
case "text_start":
|
|
214
|
+
case "text_delta":
|
|
215
|
+
case "text_end":
|
|
216
|
+
case "thinking_start":
|
|
217
|
+
case "thinking_delta":
|
|
218
|
+
case "thinking_end":
|
|
219
|
+
case "toolcall_start":
|
|
220
|
+
case "toolcall_delta":
|
|
221
|
+
case "toolcall_end":
|
|
222
|
+
case "server_tool_start":
|
|
223
|
+
case "server_tool_end":
|
|
224
|
+
if (partialMessage) {
|
|
225
|
+
partialMessage = event.partial;
|
|
226
|
+
events.push({
|
|
227
|
+
type: "message_update",
|
|
228
|
+
assistantMessageEvent: event,
|
|
229
|
+
message: { ...partialMessage } as AssistantMessage,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
case "done":
|
|
234
|
+
case "error": {
|
|
235
|
+
const message = await response.result();
|
|
236
|
+
if (!addedPartial) events.push({ type: "message_start", message: { ...message } });
|
|
237
|
+
events.push({ type: "message_end", message });
|
|
238
|
+
return { message, events };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const message = await response.result();
|
|
244
|
+
if (!addedPartial) events.push({ type: "message_start", message: { ...message } });
|
|
245
|
+
if (!events.some(event => event.type === "message_end")) events.push({ type: "message_end", message });
|
|
246
|
+
return { message, events };
|
|
247
|
+
}
|
|
248
|
+
|
|
130
249
|
export const INTENT_FIELD = "_i";
|
|
131
250
|
|
|
132
251
|
function injectIntentIntoSchema(schema: unknown): unknown {
|
|
@@ -340,11 +459,59 @@ async function streamAssistantResponse(
|
|
|
340
459
|
(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
|
341
460
|
|
|
342
461
|
const dynamicToolChoice = config.getToolChoice?.();
|
|
462
|
+
const selectedToolChoice = dynamicToolChoice ?? config.toolChoice;
|
|
463
|
+
const requiredToolName = exactToolName(selectedToolChoice);
|
|
464
|
+
if (requiredToolName) {
|
|
465
|
+
// A named choice is a safety boundary, not a hint. Keep each attempt private until the
|
|
466
|
+
// requested call is complete so truncated prose and substituted tools cannot reach state,
|
|
467
|
+
// subscribers, or execution. A provider may exhaust its output budget before emitting the
|
|
468
|
+
// call; retry that pre-invocation failure once with the exact same choice.
|
|
469
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
470
|
+
const attemptContext = attempt === 0 ? llmContext : forcedToolRetryContext(llmContext, requiredToolName);
|
|
471
|
+
const response = await logger.ttftAttr("ttft.stream-fn", () =>
|
|
472
|
+
streamFunction(config.model, attemptContext, {
|
|
473
|
+
...config,
|
|
474
|
+
apiKey: resolvedApiKey,
|
|
475
|
+
toolChoice: selectedToolChoice,
|
|
476
|
+
signal,
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
const buffered = await bufferAssistantResponse(response, config.model, signal);
|
|
480
|
+
if (buffered.message.stopReason === "aborted") {
|
|
481
|
+
context.messages.push(buffered.message);
|
|
482
|
+
for (const event of buffered.events) stream.push(event);
|
|
483
|
+
return buffered.message;
|
|
484
|
+
}
|
|
485
|
+
if (buffered.message.stopReason === "error") break;
|
|
486
|
+
|
|
487
|
+
const toolCalls = buffered.message.content.filter(content => content.type === "toolCall");
|
|
488
|
+
const exactInvocation = toolCalls.length === 1 && toolCalls[0]?.name === requiredToolName;
|
|
489
|
+
if (exactInvocation) {
|
|
490
|
+
context.messages.push(buffered.message);
|
|
491
|
+
for (const event of buffered.events) {
|
|
492
|
+
if (event.type === "message_update") {
|
|
493
|
+
config.onAssistantMessageEvent?.(event.message as AssistantMessage, event.assistantMessageEvent);
|
|
494
|
+
}
|
|
495
|
+
stream.push(event);
|
|
496
|
+
}
|
|
497
|
+
return buffered.message;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (attempt === 0 && buffered.message.stopReason === "length" && toolCalls.length === 0) continue;
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const failure = sanitizedForcedToolMessage(config.model, signal?.aborted ? "aborted" : "error");
|
|
505
|
+
context.messages.push(failure);
|
|
506
|
+
stream.push({ type: "message_start", message: { ...failure } });
|
|
507
|
+
stream.push({ type: "message_end", message: failure });
|
|
508
|
+
return failure;
|
|
509
|
+
}
|
|
343
510
|
const response = await logger.ttftAttr("ttft.stream-fn", () =>
|
|
344
511
|
streamFunction(config.model, llmContext, {
|
|
345
512
|
...config,
|
|
346
513
|
apiKey: resolvedApiKey,
|
|
347
|
-
toolChoice:
|
|
514
|
+
toolChoice: selectedToolChoice,
|
|
348
515
|
signal,
|
|
349
516
|
}),
|
|
350
517
|
);
|