@oh-my-pi/pi-agent-core 15.13.3 → 16.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 +25 -0
- package/dist/types/agent-loop.d.ts +4 -2
- package/dist/types/agent.d.ts +4 -3
- package/dist/types/append-only-context.d.ts +2 -2
- package/dist/types/compaction/utils.d.ts +2 -2
- package/dist/types/telemetry.d.ts +1 -1
- package/dist/types/types.d.ts +14 -7
- package/package.json +6 -6
- package/src/agent-loop.ts +59 -25
- package/src/agent.ts +11 -6
- package/src/append-only-context.ts +4 -4
- package/src/compaction/branch-summarization.ts +2 -2
- package/src/compaction/compaction.ts +4 -4
- package/src/compaction/utils.ts +33 -29
- package/src/telemetry.ts +10 -7
- package/src/types.ts +14 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.1] - 2026-06-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed transient provider errors after streamed tool-call arguments so incomplete tool calls are marked as interrupted output instead of eligible for automatic retry ([#2683](https://github.com/can1357/oh-my-pi/issues/2683)).
|
|
10
|
+
- Fixed `@oh-my-pi/pi-agent-core` telemetry content capture crashing every chat turn with `TypeError: systemPrompt.map is not a function` when `captureMessageContent` is enabled (`OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`). `ChatRequestSnapshot.systemPrompt` now accepts `string | readonly string[]` and the telemetry serializers normalize a bare string to a single-element array — previously the full-system serializer called `.map` on a string (the `.length` guard passed, so it threw) and the request-message serializer iterated the string into one `system` message per character.
|
|
11
|
+
|
|
12
|
+
## [16.0.0] - 2026-06-15
|
|
13
|
+
|
|
14
|
+
### Breaking Changes
|
|
15
|
+
|
|
16
|
+
- Renamed owned tool-calling options from `toolCallSyntax`/`exampleSyntax` to `dialect`/`exampleDialect`.
|
|
17
|
+
- Changed compaction conversation serialization to use the target model's native dialect turn, thinking, tool-call, and tool-result envelopes when a dialect is selected.
|
|
18
|
+
- Renamed the owned dialect environment variable from `PI_OWNED_TOOLS` to `PI_DIALECT`.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added `onTurnEnd` hook support (`setOnTurnEnd`/`onTurnEnd`) to run awaited per-turn bookkeeping with current messages before the next model request and skip callback execution for aborted or error turns
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Renamed `toolCallSyntax` option to `dialect` in AgentOptions and AgentLoopConfig
|
|
27
|
+
- Updated conversation serialization to use dialect's native transcript rendering when a dialect is selected
|
|
28
|
+
- Changed internal references from `ToolCallSyntax` type to `Dialect` type across agent loop and compaction modules
|
|
29
|
+
|
|
5
30
|
## [15.13.3] - 2026-06-15
|
|
6
31
|
|
|
7
32
|
### Added
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
5
|
import { type Context, EventStream } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import { type
|
|
6
|
+
import { type Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
7
7
|
import { type AgentRunCoverage, type AgentRunSummary } from "./run-collector";
|
|
8
8
|
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types";
|
|
9
|
+
/** Stop-details marker for a provider error after assistant content/tool args already streamed. */
|
|
10
|
+
export declare const STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL = "stream_interrupted_after_content";
|
|
9
11
|
/**
|
|
10
12
|
* Start an agent loop with a new prompt message.
|
|
11
13
|
* The prompt is added to the context and events are emitted for it.
|
|
@@ -53,7 +55,7 @@ export declare function agentLoopContinueDetailed(context: AgentContext, config:
|
|
|
53
55
|
readonly detailed: () => Promise<AgentLoopDetailedResult>;
|
|
54
56
|
};
|
|
55
57
|
export declare const INTENT_FIELD = "_i";
|
|
56
|
-
export declare function normalizeTools(tools: AgentContext["tools"], injectIntent: boolean,
|
|
58
|
+
export declare function normalizeTools(tools: AgentContext["tools"], injectIntent: boolean, exampleDialect?: Dialect): Context["tools"];
|
|
57
59
|
/** Resolve the human-readable reason an abort carried. A caller that aborts via
|
|
58
60
|
* `AbortController.abort(reason)` with a string or a non-`AbortError` `Error`
|
|
59
61
|
* (e.g. the coding agent's user-interrupt label) gets that text surfaced on the
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ApiKeyResolveContext, type AssistantMessage, type AssistantMessageEvent, type Context, type CursorExecHandlers, type CursorToolResultHandler, type Effort, type ImageContent, type Message, type Model, type ProviderSessionState, type ServiceTier, type SimpleStreamOptions, type ThinkingBudgets, type ToolChoice } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
3
3
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
4
4
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
5
5
|
import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AsideMessage, StreamFn, ToolCallContext } from "./types";
|
|
@@ -127,8 +127,8 @@ export interface AgentOptions {
|
|
|
127
127
|
transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
|
128
128
|
/** Enable intent tracing schema injection/stripping in the harness. */
|
|
129
129
|
intentTracing?: boolean;
|
|
130
|
-
/** Owned tool-calling
|
|
131
|
-
|
|
130
|
+
/** Owned tool-calling dialect. Undefined keeps provider-native tool calling. */
|
|
131
|
+
dialect?: Dialect;
|
|
132
132
|
/**
|
|
133
133
|
* When owned tool calling is active and the model fabricates a tool result
|
|
134
134
|
* mid-turn: `true` (default) aborts the provider request immediately; `false`
|
|
@@ -299,6 +299,7 @@ export declare class Agent {
|
|
|
299
299
|
setRawSseEventInterceptor(fn: SimpleStreamOptions["onSseEvent"] | undefined): void;
|
|
300
300
|
setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
|
|
301
301
|
setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
|
|
302
|
+
setOnTurnEnd(fn: ((messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void) | undefined): void;
|
|
302
303
|
/**
|
|
303
304
|
* Provide a source of non-interrupting "aside" messages (e.g. background-job
|
|
304
305
|
* completions, late LSP diagnostics) drained at each step boundary. Never
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* message delta is a cache miss each turn.
|
|
15
15
|
*/
|
|
16
16
|
import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
|
|
17
|
-
import type {
|
|
17
|
+
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
18
18
|
import type { AgentContext } from "./types";
|
|
19
19
|
/** Frozen system prompt + tool spec snapshot. */
|
|
20
20
|
export interface StablePrefixSnapshot {
|
|
@@ -26,7 +26,7 @@ export interface StablePrefixSnapshot {
|
|
|
26
26
|
export interface BuildOptions {
|
|
27
27
|
/** Inject the `_i` intent field into tool schemas (must match agent-loop's normalizeTools). */
|
|
28
28
|
intentTracing: boolean;
|
|
29
|
-
|
|
29
|
+
exampleDialect?: Dialect;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* A frozen prefix (system prompt + tools) that produces stable byte
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared utilities for compaction and branch summarization.
|
|
3
3
|
*/
|
|
4
4
|
import type { Message } from "@oh-my-pi/pi-ai";
|
|
5
|
-
import { type
|
|
5
|
+
import { type Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
6
6
|
import type { AgentMessage } from "../types";
|
|
7
7
|
export interface FileOperations {
|
|
8
8
|
read: Set<string>;
|
|
@@ -45,5 +45,5 @@ export declare function upsertFileOperations(summary: string, readFiles: string[
|
|
|
45
45
|
* This prevents the model from treating it as a conversation to continue.
|
|
46
46
|
* Call convertToLlm() first to handle custom message types.
|
|
47
47
|
*/
|
|
48
|
-
export declare function serializeConversation(messages: Message[],
|
|
48
|
+
export declare function serializeConversation(messages: Message[], dialect?: Dialect): string;
|
|
49
49
|
export declare const SUMMARIZATION_SYSTEM_PROMPT: string;
|
|
@@ -369,7 +369,7 @@ export interface ChatRequestSnapshot {
|
|
|
369
369
|
readonly tools?: readonly {
|
|
370
370
|
readonly name: string;
|
|
371
371
|
}[];
|
|
372
|
-
readonly systemPrompt?: readonly string[];
|
|
372
|
+
readonly systemPrompt?: string | readonly string[];
|
|
373
373
|
readonly messages?: readonly Message[];
|
|
374
374
|
}
|
|
375
375
|
/**
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApiKeyResolveContext, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Context, Effort, ImageContent, Message, Model, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolChoice, ToolResultMessage, TSchema } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
3
3
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
4
4
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
5
5
|
import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
|
|
@@ -164,14 +164,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
164
164
|
*/
|
|
165
165
|
intentTracing?: boolean;
|
|
166
166
|
/**
|
|
167
|
-
* Owned tool calling
|
|
167
|
+
* Owned tool calling dialect.
|
|
168
168
|
*
|
|
169
|
-
* Undefined keeps provider-native tool calling. A
|
|
170
|
-
* native `tools`, forces `toolChoice` off, appends that
|
|
169
|
+
* Undefined keeps provider-native tool calling. A dialect value sends no
|
|
170
|
+
* native `tools`, forces `toolChoice` off, appends that dialect's tool catalog
|
|
171
171
|
* instructions, re-encodes prior tool calls/results as text, and parses the
|
|
172
172
|
* model's text output back into canonical `toolCall` blocks.
|
|
173
173
|
*/
|
|
174
|
-
|
|
174
|
+
dialect?: Dialect;
|
|
175
175
|
/**
|
|
176
176
|
* When owned (in-band) tool calling is active and the model starts
|
|
177
177
|
* fabricating a tool result inside its own turn, control how the loop reacts:
|
|
@@ -180,8 +180,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
180
180
|
* - `false`: let the request finish and silently discard everything past the
|
|
181
181
|
* fabrication boundary (keeps the connection alive but pays for the tokens
|
|
182
182
|
* the model spends on the discarded tail).
|
|
183
|
-
* Only meaningful when {@link
|
|
184
|
-
* owned
|
|
183
|
+
* Only meaningful when {@link dialect} (or `PI_DIALECT`) selects an
|
|
184
|
+
* owned dialect; native tool calling never fabricates results in text.
|
|
185
185
|
*/
|
|
186
186
|
abortOnFabricatedToolResult?: boolean;
|
|
187
187
|
/**
|
|
@@ -236,6 +236,13 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
236
236
|
* rest of the batch.
|
|
237
237
|
*/
|
|
238
238
|
beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> | BeforeToolCallResult | undefined;
|
|
239
|
+
/**
|
|
240
|
+
* Called after a turn ends and before the loop polls steering/asides for the
|
|
241
|
+
* next iteration. Use this for awaited per-turn bookkeeping that must be
|
|
242
|
+
* visible before the next model request (e.g. synchronizing an advisor's
|
|
243
|
+
* backlog so advice produced during the wait is injected as an aside).
|
|
244
|
+
*/
|
|
245
|
+
onTurnEnd?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void;
|
|
239
246
|
/**
|
|
240
247
|
* Called after a tool finishes executing, before `tool_execution_end` and the
|
|
241
248
|
* tool-result message are emitted.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "16.0.1",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "
|
|
39
|
-
"@oh-my-pi/pi-catalog": "
|
|
40
|
-
"@oh-my-pi/pi-natives": "
|
|
41
|
-
"@oh-my-pi/pi-utils": "
|
|
42
|
-
"@oh-my-pi/snapcompact": "
|
|
38
|
+
"@oh-my-pi/pi-ai": "16.0.1",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "16.0.1",
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.0.1",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.0.1",
|
|
42
|
+
"@oh-my-pi/snapcompact": "16.0.1",
|
|
43
43
|
"@opentelemetry/api": "^1.9.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -16,12 +16,12 @@ import {
|
|
|
16
16
|
zodToWireSchema,
|
|
17
17
|
} from "@oh-my-pi/pi-ai";
|
|
18
18
|
import {
|
|
19
|
+
type Dialect,
|
|
19
20
|
encodeInbandToolHistory,
|
|
20
21
|
renderInbandToolPrompt,
|
|
21
22
|
renderToolExamples,
|
|
22
|
-
type ToolCallSyntax,
|
|
23
23
|
wrapInbandToolStream,
|
|
24
|
-
} from "@oh-my-pi/pi-ai/
|
|
24
|
+
} from "@oh-my-pi/pi-ai/dialect";
|
|
25
25
|
import {
|
|
26
26
|
createHarmonyAuditEvent,
|
|
27
27
|
detectHarmonyLeakInAssistantMessage,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
recoverHarmonyToolCall,
|
|
33
33
|
signalListLabel,
|
|
34
34
|
} from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
35
|
-
import {
|
|
35
|
+
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
36
36
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
37
37
|
import { type AgentRunCoverage, type AgentRunSummary, ToolCallBlockedError } from "./run-collector";
|
|
38
38
|
import {
|
|
@@ -63,6 +63,9 @@ import type {
|
|
|
63
63
|
} from "./types";
|
|
64
64
|
import { yieldIfDue } from "./utils/yield";
|
|
65
65
|
|
|
66
|
+
/** Stop-details marker for a provider error after assistant content/tool args already streamed. */
|
|
67
|
+
export const STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL = "stream_interrupted_after_content";
|
|
68
|
+
|
|
66
69
|
/** Sentinel returned by the abort race in `streamAssistantResponse`. */
|
|
67
70
|
const ABORTED: unique symbol = Symbol("agent-loop-aborted");
|
|
68
71
|
|
|
@@ -92,7 +95,7 @@ class HarmonyLeakInterruption extends Error {
|
|
|
92
95
|
this.name = "HarmonyLeakInterruption";
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
|
-
function
|
|
98
|
+
function resolveOwnedDialectFromEnv(value: string | undefined): Dialect | undefined {
|
|
96
99
|
switch (value) {
|
|
97
100
|
case "1":
|
|
98
101
|
case "true":
|
|
@@ -371,6 +374,25 @@ function buildAgentEndEvent(
|
|
|
371
374
|
}
|
|
372
375
|
return { type: "agent_end", messages, telemetry: snapshot.summary, coverage: snapshot.coverage };
|
|
373
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Push a `turn_end` event and run the awaited per-turn hook when the run is
|
|
379
|
+
* still healthy. The hook is skipped for externally aborted or errored turns so
|
|
380
|
+
* a user interrupt does not hang on a background backlog wait.
|
|
381
|
+
*/
|
|
382
|
+
async function emitTurnEnd(
|
|
383
|
+
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
384
|
+
currentContext: AgentContext,
|
|
385
|
+
message: AgentMessage,
|
|
386
|
+
toolResults: ToolResultMessage[],
|
|
387
|
+
config: AgentLoopConfig,
|
|
388
|
+
signal?: AbortSignal,
|
|
389
|
+
): Promise<void> {
|
|
390
|
+
stream.push({ type: "turn_end", message, toolResults });
|
|
391
|
+
const isAbortedOrError =
|
|
392
|
+
message.role === "assistant" && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
393
|
+
if (signal?.aborted || isAbortedOrError) return;
|
|
394
|
+
await config.onTurnEnd?.(currentContext.messages, signal);
|
|
395
|
+
}
|
|
374
396
|
|
|
375
397
|
/**
|
|
376
398
|
* Detailed-result handle returned by {@link agentLoopDetailed}. Adds the
|
|
@@ -531,7 +553,7 @@ function injectIntentIntoSchema(schema: unknown, mode: "require" | "optional" =
|
|
|
531
553
|
export function normalizeTools(
|
|
532
554
|
tools: AgentContext["tools"],
|
|
533
555
|
injectIntent: boolean,
|
|
534
|
-
|
|
556
|
+
exampleDialect?: Dialect,
|
|
535
557
|
): Context["tools"] {
|
|
536
558
|
injectIntent = injectIntent && Bun.env.PI_NO_INTENT !== "1";
|
|
537
559
|
return tools?.map(t => {
|
|
@@ -547,8 +569,8 @@ export function normalizeTools(
|
|
|
547
569
|
}
|
|
548
570
|
const description = t.description ?? "";
|
|
549
571
|
const injectExampleIntent = injectIntent && intentMode !== "omit";
|
|
550
|
-
const examplesBlock =
|
|
551
|
-
? renderToolExamples({ ...t, parameters },
|
|
572
|
+
const examplesBlock = exampleDialect
|
|
573
|
+
? renderToolExamples({ ...t, parameters }, exampleDialect, injectExampleIntent ? INTENT_FIELD : undefined)
|
|
552
574
|
: "";
|
|
553
575
|
const finalDescription = examplesBlock ? `${description}\n\n${examplesBlock}` : description;
|
|
554
576
|
return { ...t, parameters, description: finalDescription };
|
|
@@ -754,7 +776,7 @@ async function runLoopBody(
|
|
|
754
776
|
status: message.stopReason === "aborted" ? "aborted" : "error",
|
|
755
777
|
});
|
|
756
778
|
}
|
|
757
|
-
stream
|
|
779
|
+
await emitTurnEnd(stream, currentContext, message, toolResults, config, signal);
|
|
758
780
|
|
|
759
781
|
stream.push(buildAgentEndEvent(newMessages, telemetry, stepCounter.count));
|
|
760
782
|
stream.end(newMessages);
|
|
@@ -839,7 +861,7 @@ async function runLoopBody(
|
|
|
839
861
|
hasMoreToolCalls = true;
|
|
840
862
|
}
|
|
841
863
|
|
|
842
|
-
stream
|
|
864
|
+
await emitTurnEnd(stream, currentContext, message, toolResults, config, signal);
|
|
843
865
|
|
|
844
866
|
// On external abort (user interrupt), leave the steering queue intact: the
|
|
845
867
|
// session aborts then continues, delivering the queue into a fresh run.
|
|
@@ -925,6 +947,8 @@ async function streamAssistantResponse(
|
|
|
925
947
|
const llmMessages = await config.convertToLlm(messages);
|
|
926
948
|
const normalizedMessages = normalizeMessagesForProvider(llmMessages, config.model);
|
|
927
949
|
|
|
950
|
+
const ownedDialect: Dialect | undefined = config.dialect ?? resolveOwnedDialectFromEnv(Bun.env.PI_DIALECT);
|
|
951
|
+
const exampleDialect = ownedDialect ?? preferredDialect(config.model.id);
|
|
928
952
|
// Build LLM context — append-only mode caches system prompt + tools
|
|
929
953
|
// AND keeps an append-only message log so prior-turn bytes are stable.
|
|
930
954
|
let llmContext: Context;
|
|
@@ -932,13 +956,13 @@ async function streamAssistantResponse(
|
|
|
932
956
|
config.appendOnlyContext.syncMessages(normalizedMessages);
|
|
933
957
|
llmContext = config.appendOnlyContext.build(context, {
|
|
934
958
|
intentTracing: !!config.intentTracing,
|
|
935
|
-
|
|
959
|
+
exampleDialect,
|
|
936
960
|
});
|
|
937
961
|
} else {
|
|
938
962
|
llmContext = {
|
|
939
963
|
systemPrompt: context.systemPrompt,
|
|
940
964
|
messages: normalizedMessages,
|
|
941
|
-
tools: normalizeTools(context.tools, !!config.intentTracing,
|
|
965
|
+
tools: normalizeTools(context.tools, !!config.intentTracing, exampleDialect),
|
|
942
966
|
};
|
|
943
967
|
}
|
|
944
968
|
if (config.transformProviderContext) {
|
|
@@ -946,17 +970,15 @@ async function streamAssistantResponse(
|
|
|
946
970
|
}
|
|
947
971
|
|
|
948
972
|
// Owned tool calling: take tool calls away from the provider and run them
|
|
949
|
-
// through the selected in-band prompt
|
|
950
|
-
// force-enables GLM; `
|
|
951
|
-
const ownedSyntax: ToolCallSyntax | undefined =
|
|
952
|
-
config.toolCallSyntax ?? resolveOwnedToolSyntaxFromEnv(Bun.env.PI_OWNED_TOOLS);
|
|
973
|
+
// through the selected in-band prompt dialect. `PI_DIALECT=1` still
|
|
974
|
+
// force-enables GLM; `PI_DIALECT=<dialect>` force-enables that dialect.
|
|
953
975
|
let promptToolWireTools: Context["tools"];
|
|
954
|
-
if (
|
|
976
|
+
if (ownedDialect && llmContext.tools && llmContext.tools.length > 0) {
|
|
955
977
|
promptToolWireTools = llmContext.tools;
|
|
956
978
|
llmContext = {
|
|
957
979
|
...llmContext,
|
|
958
|
-
systemPrompt: [...(llmContext.systemPrompt ?? []), renderInbandToolPrompt(promptToolWireTools,
|
|
959
|
-
messages: encodeInbandToolHistory(llmContext.messages,
|
|
980
|
+
systemPrompt: [...(llmContext.systemPrompt ?? []), renderInbandToolPrompt(promptToolWireTools, ownedDialect)],
|
|
981
|
+
messages: encodeInbandToolHistory(llmContext.messages, ownedDialect, promptToolWireTools),
|
|
960
982
|
tools: undefined,
|
|
961
983
|
};
|
|
962
984
|
}
|
|
@@ -990,7 +1012,7 @@ async function streamAssistantResponse(
|
|
|
990
1012
|
// the hallucinated turn. Merged into the provider signal ONLY (not
|
|
991
1013
|
// `requestSignal`), so it cancels the request without tripping the loop's
|
|
992
1014
|
// external-abort handling (`abortRacePromise` / `requestSignal.aborted`).
|
|
993
|
-
const promptToolAbortController =
|
|
1015
|
+
const promptToolAbortController = ownedDialect ? new AbortController() : undefined;
|
|
994
1016
|
const providerAbortSignals: AbortSignal[] = [];
|
|
995
1017
|
if (requestSignal) providerAbortSignals.push(requestSignal);
|
|
996
1018
|
providerAbortSignals.push(repetitionAbortController.signal);
|
|
@@ -1000,7 +1022,7 @@ async function streamAssistantResponse(
|
|
|
1000
1022
|
const effectiveTemperature =
|
|
1001
1023
|
harmonyRetryAttempt > 0 && config.temperature !== undefined ? config.temperature + 0.05 : config.temperature;
|
|
1002
1024
|
// Owned tool calling sends no native tools, so any tool_choice would error.
|
|
1003
|
-
const effectiveToolChoice =
|
|
1025
|
+
const effectiveToolChoice = ownedDialect ? undefined : (dynamicToolChoice ?? config.toolChoice);
|
|
1004
1026
|
const effectiveReasoning = dynamicReasoning ?? config.reasoning;
|
|
1005
1027
|
const effectiveDisableReasoning = dynamicDisableReasoning ?? config.disableReasoning;
|
|
1006
1028
|
|
|
@@ -1068,7 +1090,7 @@ async function streamAssistantResponse(
|
|
|
1068
1090
|
signal: finalRequestSignal,
|
|
1069
1091
|
onResponse: captureOnResponse,
|
|
1070
1092
|
});
|
|
1071
|
-
if (promptToolWireTools &&
|
|
1093
|
+
if (promptToolWireTools && ownedDialect) {
|
|
1072
1094
|
// Re-materialize in-band tool-call text as native toolCall content blocks
|
|
1073
1095
|
// so the rest of the loop executes them unchanged. When the model starts
|
|
1074
1096
|
// fabricating tool results, the abort callback cancels the provider — unless
|
|
@@ -1077,7 +1099,7 @@ async function streamAssistantResponse(
|
|
|
1077
1099
|
response = wrapInbandToolStream(
|
|
1078
1100
|
response,
|
|
1079
1101
|
promptToolWireTools,
|
|
1080
|
-
|
|
1102
|
+
ownedDialect,
|
|
1081
1103
|
() => promptToolAbortController?.abort(),
|
|
1082
1104
|
config.abortOnFabricatedToolResult ?? true,
|
|
1083
1105
|
);
|
|
@@ -1336,14 +1358,26 @@ function retainCompletedToolCalls(
|
|
|
1336
1358
|
completedToolCallIds: ReadonlySet<string>,
|
|
1337
1359
|
): AssistantMessage {
|
|
1338
1360
|
if (message.stopReason !== "error" && message.stopReason !== "aborted") return message;
|
|
1339
|
-
let
|
|
1361
|
+
let droppedIncompleteToolCall = false;
|
|
1340
1362
|
const content = message.content.filter(block => {
|
|
1341
1363
|
if (block.type !== "toolCall") return true;
|
|
1342
1364
|
const keep = completedToolCallIds.has(block.id);
|
|
1343
|
-
if (!keep)
|
|
1365
|
+
if (!keep) droppedIncompleteToolCall = true;
|
|
1344
1366
|
return keep;
|
|
1345
1367
|
});
|
|
1346
|
-
|
|
1368
|
+
if (!droppedIncompleteToolCall) return message;
|
|
1369
|
+
return {
|
|
1370
|
+
...message,
|
|
1371
|
+
content,
|
|
1372
|
+
stopDetails:
|
|
1373
|
+
message.stopDetails?.type === STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL
|
|
1374
|
+
? message.stopDetails
|
|
1375
|
+
: {
|
|
1376
|
+
type: STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL,
|
|
1377
|
+
category: message.stopDetails?.type ?? null,
|
|
1378
|
+
explanation: message.stopDetails?.explanation ?? null,
|
|
1379
|
+
},
|
|
1380
|
+
};
|
|
1347
1381
|
}
|
|
1348
1382
|
|
|
1349
1383
|
function emitDiscardedHarmonyPartial(
|
package/src/agent.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type ToolChoice,
|
|
23
23
|
type ToolResultMessage,
|
|
24
24
|
} from "@oh-my-pi/pi-ai";
|
|
25
|
-
import type {
|
|
25
|
+
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
26
26
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
27
27
|
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
28
28
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
@@ -221,8 +221,8 @@ export interface AgentOptions {
|
|
|
221
221
|
|
|
222
222
|
/** Enable intent tracing schema injection/stripping in the harness. */
|
|
223
223
|
intentTracing?: boolean;
|
|
224
|
-
/** Owned tool-calling
|
|
225
|
-
|
|
224
|
+
/** Owned tool-calling dialect. Undefined keeps provider-native tool calling. */
|
|
225
|
+
dialect?: Dialect;
|
|
226
226
|
/**
|
|
227
227
|
* When owned tool calling is active and the model fabricates a tool result
|
|
228
228
|
* mid-turn: `true` (default) aborts the provider request immediately; `false`
|
|
@@ -326,7 +326,7 @@ export class Agent {
|
|
|
326
326
|
#preferWebsockets?: boolean;
|
|
327
327
|
#transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
|
328
328
|
#intentTracing: boolean;
|
|
329
|
-
#
|
|
329
|
+
#dialect?: Dialect;
|
|
330
330
|
#abortOnFabricatedToolResult?: boolean;
|
|
331
331
|
#getToolChoice?: () => ToolChoice | undefined;
|
|
332
332
|
#onPayload?: SimpleStreamOptions["onPayload"];
|
|
@@ -335,6 +335,7 @@ export class Agent {
|
|
|
335
335
|
#onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
336
336
|
#onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
|
|
337
337
|
#onBeforeYield?: () => Promise<void> | void;
|
|
338
|
+
#onTurnEnd?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void;
|
|
338
339
|
#asideMessageProvider?: () => AsideMessage[] | Promise<AsideMessage[]>;
|
|
339
340
|
#telemetry?: AgentLoopConfig["telemetry"];
|
|
340
341
|
#appendOnlyContext?: AppendOnlyContextManager;
|
|
@@ -390,7 +391,7 @@ export class Agent {
|
|
|
390
391
|
this.#preferWebsockets = opts.preferWebsockets;
|
|
391
392
|
this.#transformToolCallArguments = opts.transformToolCallArguments;
|
|
392
393
|
this.#intentTracing = opts.intentTracing === true;
|
|
393
|
-
this.#
|
|
394
|
+
this.#dialect = opts.dialect;
|
|
394
395
|
this.#abortOnFabricatedToolResult = opts.abortOnFabricatedToolResult;
|
|
395
396
|
this.#getToolChoice = opts.getToolChoice;
|
|
396
397
|
this.#onAssistantMessageEvent = opts.onAssistantMessageEvent;
|
|
@@ -639,6 +640,9 @@ export class Agent {
|
|
|
639
640
|
setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void {
|
|
640
641
|
this.#onBeforeYield = fn;
|
|
641
642
|
}
|
|
643
|
+
setOnTurnEnd(fn: ((messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void) | undefined): void {
|
|
644
|
+
this.#onTurnEnd = fn;
|
|
645
|
+
}
|
|
642
646
|
|
|
643
647
|
/**
|
|
644
648
|
* Provide a source of non-interrupting "aside" messages (e.g. background-job
|
|
@@ -1037,13 +1041,14 @@ export class Agent {
|
|
|
1037
1041
|
cursorOnToolResult,
|
|
1038
1042
|
transformToolCallArguments: this.#transformToolCallArguments,
|
|
1039
1043
|
intentTracing: this.#intentTracing,
|
|
1040
|
-
|
|
1044
|
+
dialect: this.#dialect,
|
|
1041
1045
|
abortOnFabricatedToolResult: this.#abortOnFabricatedToolResult,
|
|
1042
1046
|
appendOnlyContext: this.#appendOnlyContext,
|
|
1043
1047
|
beforeToolCall: this.beforeToolCall ? (ctx, signal) => this.beforeToolCall?.(ctx, signal) : undefined,
|
|
1044
1048
|
afterToolCall: this.afterToolCall ? (ctx, signal) => this.afterToolCall?.(ctx, signal) : undefined,
|
|
1045
1049
|
onAssistantMessageEvent: this.#onAssistantMessageEvent,
|
|
1046
1050
|
onHarmonyLeak: this.#onHarmonyLeak,
|
|
1051
|
+
onTurnEnd: (messages, signal) => this.#onTurnEnd?.(messages, signal),
|
|
1047
1052
|
getToolChoice,
|
|
1048
1053
|
getReasoning: () => this.#state.thinkingLevel,
|
|
1049
1054
|
getDisableReasoning: () => this.#state.disableReasoning,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
|
|
18
|
-
import type {
|
|
18
|
+
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
19
19
|
import { normalizeTools } from "./agent-loop";
|
|
20
20
|
import type { AgentContext } from "./types";
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ export interface StablePrefixSnapshot {
|
|
|
34
34
|
export interface BuildOptions {
|
|
35
35
|
/** Inject the `_i` intent field into tool schemas (must match agent-loop's normalizeTools). */
|
|
36
36
|
intentTracing: boolean;
|
|
37
|
-
|
|
37
|
+
exampleDialect?: Dialect;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -270,7 +270,7 @@ export class AppendOnlyContextManager {
|
|
|
270
270
|
|
|
271
271
|
function takeSnapshot(context: AgentContext, options: BuildOptions): StablePrefixSnapshot {
|
|
272
272
|
const systemPrompt = [...context.systemPrompt];
|
|
273
|
-
const tools = normalizeTools(context.tools, options.intentTracing, options.
|
|
273
|
+
const tools = normalizeTools(context.tools, options.intentTracing, options.exampleDialect) ?? [];
|
|
274
274
|
return {
|
|
275
275
|
systemPrompt,
|
|
276
276
|
tools,
|
|
@@ -290,7 +290,7 @@ function computeFingerprint(systemPrompt: string[], tools: Tool[], options: Buil
|
|
|
290
290
|
cw: t.customWireName,
|
|
291
291
|
})),
|
|
292
292
|
i: options.intentTracing,
|
|
293
|
-
ex: options.
|
|
293
|
+
ex: options.exampleDialect,
|
|
294
294
|
});
|
|
295
295
|
let hash = 0;
|
|
296
296
|
for (let i = 0; i < payload.length; i++) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ApiKey, Model } from "@oh-my-pi/pi-ai";
|
|
9
|
-
import {
|
|
9
|
+
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
10
10
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
12
12
|
import type { AgentMessage } from "../types";
|
|
@@ -291,7 +291,7 @@ export async function generateBranchSummary(
|
|
|
291
291
|
// Transform to LLM-compatible messages, then serialize to text
|
|
292
292
|
// Serialization prevents the model from treating it as a conversation to continue
|
|
293
293
|
const llmMessages = (options.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
294
|
-
const conversationText = serializeConversation(llmMessages,
|
|
294
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
295
295
|
|
|
296
296
|
// Build prompt
|
|
297
297
|
const instructions = customInstructions || BRANCH_SUMMARY_PROMPT;
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type Usage,
|
|
19
19
|
withAuth,
|
|
20
20
|
} from "@oh-my-pi/pi-ai";
|
|
21
|
-
import {
|
|
21
|
+
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
22
22
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
23
23
|
import { countTokens } from "@oh-my-pi/pi-natives";
|
|
24
24
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
@@ -643,7 +643,7 @@ export async function generateSummary(
|
|
|
643
643
|
// Serialize conversation to text so model doesn't try to continue it
|
|
644
644
|
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
|
645
645
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(currentMessages);
|
|
646
|
-
const conversationText = serializeConversation(llmMessages,
|
|
646
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
647
647
|
|
|
648
648
|
// Build the prompt with conversation wrapped in tags
|
|
649
649
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
@@ -791,7 +791,7 @@ async function generateShortSummary(
|
|
|
791
791
|
): Promise<string> {
|
|
792
792
|
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
|
793
793
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(recentMessages);
|
|
794
|
-
const conversationText = serializeConversation(llmMessages,
|
|
794
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
795
795
|
|
|
796
796
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
797
797
|
if (historySummary) {
|
|
@@ -1156,7 +1156,7 @@ async function generateTurnPrefixSummary(
|
|
|
1156
1156
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
1157
1157
|
|
|
1158
1158
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
1159
|
-
const conversationText = serializeConversation(llmMessages,
|
|
1159
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
1160
1160
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
1161
1161
|
const summarizationMessages = [
|
|
1162
1162
|
{
|
package/src/compaction/utils.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Message, ToolCall } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import { type
|
|
6
|
+
import { type Dialect, getDialectDefinition } from "@oh-my-pi/pi-ai/dialect";
|
|
7
7
|
import { formatGroupedPaths, prompt } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type { AgentMessage } from "../types";
|
|
9
9
|
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
@@ -189,10 +189,7 @@ function truncateForSummary(text: string, maxChars: number): string {
|
|
|
189
189
|
* This prevents the model from treating it as a conversation to continue.
|
|
190
190
|
* Call convertToLlm() first to handle custom message types.
|
|
191
191
|
*/
|
|
192
|
-
export function serializeConversation(messages: Message[],
|
|
193
|
-
const grammar = syntax ? getInbandGrammar(syntax) : undefined;
|
|
194
|
-
const parts: string[] = [];
|
|
195
|
-
|
|
192
|
+
export function serializeConversation(messages: Message[], dialect?: Dialect): string {
|
|
196
193
|
// Tool results flagged contextually useless (and their paired calls) are
|
|
197
194
|
// dropped from the serialized text: the source region is discarded after
|
|
198
195
|
// summarization anyway, so excluding them costs nothing and keeps garbage
|
|
@@ -203,7 +200,33 @@ export function serializeConversation(messages: Message[], syntax?: ToolCallSynt
|
|
|
203
200
|
uselessCallIds.add(msg.toolCallId);
|
|
204
201
|
}
|
|
205
202
|
}
|
|
203
|
+
if (dialect) {
|
|
204
|
+
const processed: Message[] = [];
|
|
205
|
+
for (const msg of messages) {
|
|
206
|
+
if (msg.role === "assistant") {
|
|
207
|
+
const content = msg.content.filter(block => block.type !== "toolCall" || !uselessCallIds.has(block.id));
|
|
208
|
+
if (content.length > 0) processed.push(content.length === msg.content.length ? msg : { ...msg, content });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (msg.role === "toolResult") {
|
|
212
|
+
if (uselessCallIds.has(msg.toolCallId)) continue;
|
|
213
|
+
const text = msg.content
|
|
214
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
215
|
+
.map(c => c.text)
|
|
216
|
+
.join("");
|
|
217
|
+
if (!text) continue;
|
|
218
|
+
processed.push({
|
|
219
|
+
...msg,
|
|
220
|
+
content: [{ type: "text", text: truncateForSummary(text, TOOL_RESULT_MAX_CHARS) }],
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
processed.push(msg);
|
|
225
|
+
}
|
|
226
|
+
return getDialectDefinition(dialect).renderTranscript(processed);
|
|
227
|
+
}
|
|
206
228
|
|
|
229
|
+
const parts: string[] = [];
|
|
207
230
|
for (const msg of messages) {
|
|
208
231
|
if (msg.role === "user") {
|
|
209
232
|
const content =
|
|
@@ -237,7 +260,7 @@ export function serializeConversation(messages: Message[], syntax?: ToolCallSynt
|
|
|
237
260
|
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
238
261
|
}
|
|
239
262
|
if (toolCalls.length > 0) {
|
|
240
|
-
parts.push(`[Tool Call]: ${renderToolCalls(toolCalls
|
|
263
|
+
parts.push(`[Tool Call]: ${renderToolCalls(toolCalls)}`);
|
|
241
264
|
}
|
|
242
265
|
} else if (msg.role === "toolResult") {
|
|
243
266
|
if (uselessCallIds.has(msg.toolCallId)) continue;
|
|
@@ -247,9 +270,7 @@ export function serializeConversation(messages: Message[], syntax?: ToolCallSynt
|
|
|
247
270
|
.join("");
|
|
248
271
|
if (content) {
|
|
249
272
|
const text = truncateForSummary(content, TOOL_RESULT_MAX_CHARS);
|
|
250
|
-
parts.push(
|
|
251
|
-
`[Tool Result]: ${renderToolResult(msg.toolCallId, msg.toolName, msg.isError === true, text, grammar)}`,
|
|
252
|
-
);
|
|
273
|
+
parts.push(`[Tool Result]: ${text}`);
|
|
253
274
|
}
|
|
254
275
|
}
|
|
255
276
|
}
|
|
@@ -258,11 +279,10 @@ export function serializeConversation(messages: Message[], syntax?: ToolCallSynt
|
|
|
258
279
|
}
|
|
259
280
|
|
|
260
281
|
/**
|
|
261
|
-
* Render an assistant turn's tool calls
|
|
262
|
-
*
|
|
282
|
+
* Render an assistant turn's tool calls as a compact `name(args)` list for the
|
|
283
|
+
* legacy serializer.
|
|
263
284
|
*/
|
|
264
|
-
function renderToolCalls(calls: ToolCall[]
|
|
265
|
-
if (grammar) return grammar.renderAssistantToolCalls(calls);
|
|
285
|
+
function renderToolCalls(calls: ToolCall[]): string {
|
|
266
286
|
return calls
|
|
267
287
|
.map(call => {
|
|
268
288
|
const argsStr = Object.entries(call.arguments as Record<string, unknown>)
|
|
@@ -273,22 +293,6 @@ function renderToolCalls(calls: ToolCall[], grammar: Grammar | undefined): strin
|
|
|
273
293
|
.join("; ");
|
|
274
294
|
}
|
|
275
295
|
|
|
276
|
-
/**
|
|
277
|
-
* Render a single tool result. With a grammar, emit the model's native
|
|
278
|
-
* tool-result envelope; otherwise return the (already truncated) text verbatim.
|
|
279
|
-
*/
|
|
280
|
-
function renderToolResult(
|
|
281
|
-
id: string,
|
|
282
|
-
name: string,
|
|
283
|
-
isError: boolean,
|
|
284
|
-
text: string,
|
|
285
|
-
grammar: Grammar | undefined,
|
|
286
|
-
): string {
|
|
287
|
-
if (!grammar) return text;
|
|
288
|
-
const result: GrammarToolResult = { id, name, index: 0, text, isError };
|
|
289
|
-
return grammar.renderToolResults([result]);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
296
|
// ============================================================================
|
|
293
297
|
// Summarization System Prompt
|
|
294
298
|
// ============================================================================
|
package/src/telemetry.ts
CHANGED
|
@@ -731,7 +731,7 @@ export interface ChatRequestSnapshot {
|
|
|
731
731
|
readonly reasoningEffort?: string;
|
|
732
732
|
readonly toolChoice?: ToolChoice;
|
|
733
733
|
readonly tools?: readonly { readonly name: string }[];
|
|
734
|
-
readonly systemPrompt?: readonly string[];
|
|
734
|
+
readonly systemPrompt?: string | readonly string[];
|
|
735
735
|
readonly messages?: readonly Message[];
|
|
736
736
|
}
|
|
737
737
|
|
|
@@ -796,6 +796,11 @@ function applyContentCaptureForResponse(telemetry: AgentTelemetry, span: Span, m
|
|
|
796
796
|
}
|
|
797
797
|
}
|
|
798
798
|
|
|
799
|
+
function normalizeSystemPromptParts(systemPrompt: string | readonly string[] | undefined): readonly string[] {
|
|
800
|
+
if (!systemPrompt) return [];
|
|
801
|
+
return typeof systemPrompt === "string" ? [systemPrompt] : systemPrompt;
|
|
802
|
+
}
|
|
803
|
+
|
|
799
804
|
function serializeRequestMessagesForTelemetry(
|
|
800
805
|
telemetry: AgentTelemetry,
|
|
801
806
|
request: ChatRequestSnapshot,
|
|
@@ -803,10 +808,8 @@ function serializeRequestMessagesForTelemetry(
|
|
|
803
808
|
const serializer = telemetry.config.contentSerializer?.requestMessages;
|
|
804
809
|
if (serializer) return callContentSerializer(telemetry, "requestMessages", () => serializer(request));
|
|
805
810
|
const messages: TelemetryMessageSummary[] = [];
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
messages.push({ role: "system", content: summarizeTelemetryValue(text) });
|
|
809
|
-
}
|
|
811
|
+
for (const text of normalizeSystemPromptParts(request.systemPrompt))
|
|
812
|
+
messages.push({ role: "system", content: summarizeTelemetryValue(text) });
|
|
810
813
|
if (request.messages) {
|
|
811
814
|
for (const message of request.messages) {
|
|
812
815
|
messages.push({ role: message.role, content: summarizeTelemetryValue(message.content) });
|
|
@@ -874,8 +877,8 @@ interface OtelOutputMessage extends OtelInputMessage {
|
|
|
874
877
|
}
|
|
875
878
|
|
|
876
879
|
function serializeFullSystemInstructionsForTelemetry(request: ChatRequestSnapshot): string | undefined {
|
|
877
|
-
const systemPrompt = request.systemPrompt;
|
|
878
|
-
if (
|
|
880
|
+
const systemPrompt = normalizeSystemPromptParts(request.systemPrompt);
|
|
881
|
+
if (systemPrompt.length === 0) return undefined;
|
|
879
882
|
return stringifyJsonAttribute(systemPrompt.map(text => ({ type: "text", content: text }) satisfies OtelMessagePart));
|
|
880
883
|
}
|
|
881
884
|
|
package/src/types.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
ToolResultMessage,
|
|
18
18
|
TSchema,
|
|
19
19
|
} from "@oh-my-pi/pi-ai";
|
|
20
|
-
import type {
|
|
20
|
+
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
21
21
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
22
22
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
23
23
|
import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
|
|
@@ -201,14 +201,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
201
201
|
*/
|
|
202
202
|
intentTracing?: boolean;
|
|
203
203
|
/**
|
|
204
|
-
* Owned tool calling
|
|
204
|
+
* Owned tool calling dialect.
|
|
205
205
|
*
|
|
206
|
-
* Undefined keeps provider-native tool calling. A
|
|
207
|
-
* native `tools`, forces `toolChoice` off, appends that
|
|
206
|
+
* Undefined keeps provider-native tool calling. A dialect value sends no
|
|
207
|
+
* native `tools`, forces `toolChoice` off, appends that dialect's tool catalog
|
|
208
208
|
* instructions, re-encodes prior tool calls/results as text, and parses the
|
|
209
209
|
* model's text output back into canonical `toolCall` blocks.
|
|
210
210
|
*/
|
|
211
|
-
|
|
211
|
+
dialect?: Dialect;
|
|
212
212
|
/**
|
|
213
213
|
* When owned (in-band) tool calling is active and the model starts
|
|
214
214
|
* fabricating a tool result inside its own turn, control how the loop reacts:
|
|
@@ -217,8 +217,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
217
217
|
* - `false`: let the request finish and silently discard everything past the
|
|
218
218
|
* fabrication boundary (keeps the connection alive but pays for the tokens
|
|
219
219
|
* the model spends on the discarded tail).
|
|
220
|
-
* Only meaningful when {@link
|
|
221
|
-
* owned
|
|
220
|
+
* Only meaningful when {@link dialect} (or `PI_DIALECT`) selects an
|
|
221
|
+
* owned dialect; native tool calling never fabricates results in text.
|
|
222
222
|
*/
|
|
223
223
|
abortOnFabricatedToolResult?: boolean;
|
|
224
224
|
/**
|
|
@@ -282,6 +282,13 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
282
282
|
context: BeforeToolCallContext,
|
|
283
283
|
signal?: AbortSignal,
|
|
284
284
|
) => Promise<BeforeToolCallResult | undefined> | BeforeToolCallResult | undefined;
|
|
285
|
+
/**
|
|
286
|
+
* Called after a turn ends and before the loop polls steering/asides for the
|
|
287
|
+
* next iteration. Use this for awaited per-turn bookkeeping that must be
|
|
288
|
+
* visible before the next model request (e.g. synchronizing an advisor's
|
|
289
|
+
* backlog so advice produced during the wait is injected as an aside).
|
|
290
|
+
*/
|
|
291
|
+
onTurnEnd?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void;
|
|
285
292
|
|
|
286
293
|
/**
|
|
287
294
|
* Called after a tool finishes executing, before `tool_execution_end` and the
|