@oh-my-pi/pi-agent-core 15.13.2 → 16.0.0
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 +29 -0
- package/dist/types/agent-loop.d.ts +2 -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 -1
- package/dist/types/types.d.ts +23 -7
- package/package.json +6 -6
- package/src/agent-loop.ts +69 -23
- package/src/agent.ts +11 -6
- package/src/append-only-context.ts +4 -4
- package/src/compaction/branch-summarization.ts +2 -1
- package/src/compaction/compaction.ts +4 -3
- package/src/compaction/pruning.ts +12 -1
- package/src/compaction/utils.ts +50 -13
- package/src/types.ts +23 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.0] - 2026-06-15
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Renamed owned tool-calling options from `toolCallSyntax`/`exampleSyntax` to `dialect`/`exampleDialect`.
|
|
10
|
+
- 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.
|
|
11
|
+
- Renamed the owned dialect environment variable from `PI_OWNED_TOOLS` to `PI_DIALECT`.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- 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
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Renamed `toolCallSyntax` option to `dialect` in AgentOptions and AgentLoopConfig
|
|
20
|
+
- Updated conversation serialization to use dialect's native transcript rendering when a dialect is selected
|
|
21
|
+
- Changed internal references from `ToolCallSyntax` type to `Dialect` type across agent loop and compaction modules
|
|
22
|
+
|
|
23
|
+
## [15.13.3] - 2026-06-15
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Added the `interruptible` tool field: when set, the agent loop may abort the tool mid-execution to deliver a queued steering message (honored only in `immediate` interrupt mode).
|
|
28
|
+
- Added support for `gemini` and `gemma` as valid owned tool syntax values in environment configuration
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed `pruneToolOutputs` blanking tiny tool results during overflow pruning: results below `50` tokens (`MIN_PRUNE_TOKENS`) are no longer replaced with the `[Output truncated - N tokens]` placeholder, which cost more tokens than the result itself and churned the prompt cache for zero savings.
|
|
33
|
+
|
|
5
34
|
## [15.13.2] - 2026-06-15
|
|
6
35
|
|
|
7
36
|
### Breaking Changes
|
|
@@ -3,7 +3,7 @@
|
|
|
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
9
|
/**
|
|
@@ -53,7 +53,7 @@ export declare function agentLoopContinueDetailed(context: AgentContext, config:
|
|
|
53
53
|
readonly detailed: () => Promise<AgentLoopDetailedResult>;
|
|
54
54
|
};
|
|
55
55
|
export declare const INTENT_FIELD = "_i";
|
|
56
|
-
export declare function normalizeTools(tools: AgentContext["tools"], injectIntent: boolean,
|
|
56
|
+
export declare function normalizeTools(tools: AgentContext["tools"], injectIntent: boolean, exampleDialect?: Dialect): Context["tools"];
|
|
57
57
|
/** Resolve the human-readable reason an abort carried. A caller that aborts via
|
|
58
58
|
* `AbortController.abort(reason)` with a string or a non-`AbortError` `Error`
|
|
59
59
|
* (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,6 +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 Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
5
6
|
import type { AgentMessage } from "../types";
|
|
6
7
|
export interface FileOperations {
|
|
7
8
|
read: Set<string>;
|
|
@@ -44,5 +45,5 @@ export declare function upsertFileOperations(summary: string, readFiles: string[
|
|
|
44
45
|
* This prevents the model from treating it as a conversation to continue.
|
|
45
46
|
* Call convertToLlm() first to handle custom message types.
|
|
46
47
|
*/
|
|
47
|
-
export declare function serializeConversation(messages: Message[]): string;
|
|
48
|
+
export declare function serializeConversation(messages: Message[], dialect?: Dialect): string;
|
|
48
49
|
export declare const SUMMARIZATION_SYSTEM_PROMPT: string;
|
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.
|
|
@@ -428,6 +435,15 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
428
435
|
concurrency?: "shared" | "exclusive" | ((args: Partial<Static<TParameters>>) => "shared" | "exclusive");
|
|
429
436
|
/** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
|
|
430
437
|
lenientArgValidation?: boolean;
|
|
438
|
+
/**
|
|
439
|
+
* If true, the agent loop may abort this tool mid-execution to deliver a
|
|
440
|
+
* queued steering message (instead of waiting for the tool to finish on its
|
|
441
|
+
* own). Set only on tools that purely *wait* and observe their abort signal
|
|
442
|
+
* cleanly (e.g. the `job` poll), so the abort surfaces the tool's current
|
|
443
|
+
* snapshot rather than corrupting a side effect. Honored only when
|
|
444
|
+
* `interruptMode` is "immediate".
|
|
445
|
+
*/
|
|
446
|
+
interruptible?: boolean;
|
|
431
447
|
/**
|
|
432
448
|
* Controls how the INTENT_FIELD (`_i`) is handled for this tool.
|
|
433
449
|
* - `"require"` (default): `_i` is injected and required in the parameter schema.
|
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.0",
|
|
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.0",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "16.0.0",
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.0.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.0.0",
|
|
42
|
+
"@oh-my-pi/snapcompact": "16.0.0",
|
|
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 {
|
|
@@ -74,6 +74,14 @@ const ABORTED: unique symbol = Symbol("agent-loop-aborted");
|
|
|
74
74
|
*/
|
|
75
75
|
const MAX_PAUSED_TURN_CONTINUATIONS = 8;
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Cadence (ms) for polling queued steering while an `interruptible` tool is in
|
|
79
|
+
* flight, so a steer cuts the wait short instead of sitting idle until the
|
|
80
|
+
* tool's own window elapses. A cheap synchronous queue check; latency-bounded
|
|
81
|
+
* at one tick.
|
|
82
|
+
*/
|
|
83
|
+
const STEERING_INTERRUPT_POLL_MS = 250;
|
|
84
|
+
|
|
77
85
|
class HarmonyLeakInterruption extends Error {
|
|
78
86
|
constructor(
|
|
79
87
|
readonly detection: HarmonyDetection,
|
|
@@ -84,7 +92,7 @@ class HarmonyLeakInterruption extends Error {
|
|
|
84
92
|
this.name = "HarmonyLeakInterruption";
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
|
-
function
|
|
95
|
+
function resolveOwnedDialectFromEnv(value: string | undefined): Dialect | undefined {
|
|
88
96
|
switch (value) {
|
|
89
97
|
case "1":
|
|
90
98
|
case "true":
|
|
@@ -98,6 +106,8 @@ function resolveOwnedToolSyntaxFromEnv(value: string | undefined): ToolCallSynta
|
|
|
98
106
|
case "harmony":
|
|
99
107
|
case "pi":
|
|
100
108
|
case "qwen3":
|
|
109
|
+
case "gemini":
|
|
110
|
+
case "gemma":
|
|
101
111
|
return value;
|
|
102
112
|
default:
|
|
103
113
|
return undefined;
|
|
@@ -361,6 +371,25 @@ function buildAgentEndEvent(
|
|
|
361
371
|
}
|
|
362
372
|
return { type: "agent_end", messages, telemetry: snapshot.summary, coverage: snapshot.coverage };
|
|
363
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Push a `turn_end` event and run the awaited per-turn hook when the run is
|
|
376
|
+
* still healthy. The hook is skipped for externally aborted or errored turns so
|
|
377
|
+
* a user interrupt does not hang on a background backlog wait.
|
|
378
|
+
*/
|
|
379
|
+
async function emitTurnEnd(
|
|
380
|
+
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
381
|
+
currentContext: AgentContext,
|
|
382
|
+
message: AgentMessage,
|
|
383
|
+
toolResults: ToolResultMessage[],
|
|
384
|
+
config: AgentLoopConfig,
|
|
385
|
+
signal?: AbortSignal,
|
|
386
|
+
): Promise<void> {
|
|
387
|
+
stream.push({ type: "turn_end", message, toolResults });
|
|
388
|
+
const isAbortedOrError =
|
|
389
|
+
message.role === "assistant" && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
390
|
+
if (signal?.aborted || isAbortedOrError) return;
|
|
391
|
+
await config.onTurnEnd?.(currentContext.messages, signal);
|
|
392
|
+
}
|
|
364
393
|
|
|
365
394
|
/**
|
|
366
395
|
* Detailed-result handle returned by {@link agentLoopDetailed}. Adds the
|
|
@@ -521,7 +550,7 @@ function injectIntentIntoSchema(schema: unknown, mode: "require" | "optional" =
|
|
|
521
550
|
export function normalizeTools(
|
|
522
551
|
tools: AgentContext["tools"],
|
|
523
552
|
injectIntent: boolean,
|
|
524
|
-
|
|
553
|
+
exampleDialect?: Dialect,
|
|
525
554
|
): Context["tools"] {
|
|
526
555
|
injectIntent = injectIntent && Bun.env.PI_NO_INTENT !== "1";
|
|
527
556
|
return tools?.map(t => {
|
|
@@ -537,8 +566,8 @@ export function normalizeTools(
|
|
|
537
566
|
}
|
|
538
567
|
const description = t.description ?? "";
|
|
539
568
|
const injectExampleIntent = injectIntent && intentMode !== "omit";
|
|
540
|
-
const examplesBlock =
|
|
541
|
-
? renderToolExamples({ ...t, parameters },
|
|
569
|
+
const examplesBlock = exampleDialect
|
|
570
|
+
? renderToolExamples({ ...t, parameters }, exampleDialect, injectExampleIntent ? INTENT_FIELD : undefined)
|
|
542
571
|
: "";
|
|
543
572
|
const finalDescription = examplesBlock ? `${description}\n\n${examplesBlock}` : description;
|
|
544
573
|
return { ...t, parameters, description: finalDescription };
|
|
@@ -744,7 +773,7 @@ async function runLoopBody(
|
|
|
744
773
|
status: message.stopReason === "aborted" ? "aborted" : "error",
|
|
745
774
|
});
|
|
746
775
|
}
|
|
747
|
-
stream
|
|
776
|
+
await emitTurnEnd(stream, currentContext, message, toolResults, config, signal);
|
|
748
777
|
|
|
749
778
|
stream.push(buildAgentEndEvent(newMessages, telemetry, stepCounter.count));
|
|
750
779
|
stream.end(newMessages);
|
|
@@ -829,7 +858,7 @@ async function runLoopBody(
|
|
|
829
858
|
hasMoreToolCalls = true;
|
|
830
859
|
}
|
|
831
860
|
|
|
832
|
-
stream
|
|
861
|
+
await emitTurnEnd(stream, currentContext, message, toolResults, config, signal);
|
|
833
862
|
|
|
834
863
|
// On external abort (user interrupt), leave the steering queue intact: the
|
|
835
864
|
// session aborts then continues, delivering the queue into a fresh run.
|
|
@@ -915,6 +944,8 @@ async function streamAssistantResponse(
|
|
|
915
944
|
const llmMessages = await config.convertToLlm(messages);
|
|
916
945
|
const normalizedMessages = normalizeMessagesForProvider(llmMessages, config.model);
|
|
917
946
|
|
|
947
|
+
const ownedDialect: Dialect | undefined = config.dialect ?? resolveOwnedDialectFromEnv(Bun.env.PI_DIALECT);
|
|
948
|
+
const exampleDialect = ownedDialect ?? preferredDialect(config.model.id);
|
|
918
949
|
// Build LLM context — append-only mode caches system prompt + tools
|
|
919
950
|
// AND keeps an append-only message log so prior-turn bytes are stable.
|
|
920
951
|
let llmContext: Context;
|
|
@@ -922,13 +953,13 @@ async function streamAssistantResponse(
|
|
|
922
953
|
config.appendOnlyContext.syncMessages(normalizedMessages);
|
|
923
954
|
llmContext = config.appendOnlyContext.build(context, {
|
|
924
955
|
intentTracing: !!config.intentTracing,
|
|
925
|
-
|
|
956
|
+
exampleDialect,
|
|
926
957
|
});
|
|
927
958
|
} else {
|
|
928
959
|
llmContext = {
|
|
929
960
|
systemPrompt: context.systemPrompt,
|
|
930
961
|
messages: normalizedMessages,
|
|
931
|
-
tools: normalizeTools(context.tools, !!config.intentTracing,
|
|
962
|
+
tools: normalizeTools(context.tools, !!config.intentTracing, exampleDialect),
|
|
932
963
|
};
|
|
933
964
|
}
|
|
934
965
|
if (config.transformProviderContext) {
|
|
@@ -936,17 +967,15 @@ async function streamAssistantResponse(
|
|
|
936
967
|
}
|
|
937
968
|
|
|
938
969
|
// Owned tool calling: take tool calls away from the provider and run them
|
|
939
|
-
// through the selected in-band prompt
|
|
940
|
-
// force-enables GLM; `
|
|
941
|
-
const ownedSyntax: ToolCallSyntax | undefined =
|
|
942
|
-
config.toolCallSyntax ?? resolveOwnedToolSyntaxFromEnv(Bun.env.PI_OWNED_TOOLS);
|
|
970
|
+
// through the selected in-band prompt dialect. `PI_DIALECT=1` still
|
|
971
|
+
// force-enables GLM; `PI_DIALECT=<dialect>` force-enables that dialect.
|
|
943
972
|
let promptToolWireTools: Context["tools"];
|
|
944
|
-
if (
|
|
973
|
+
if (ownedDialect && llmContext.tools && llmContext.tools.length > 0) {
|
|
945
974
|
promptToolWireTools = llmContext.tools;
|
|
946
975
|
llmContext = {
|
|
947
976
|
...llmContext,
|
|
948
|
-
systemPrompt: [...(llmContext.systemPrompt ?? []), renderInbandToolPrompt(promptToolWireTools,
|
|
949
|
-
messages: encodeInbandToolHistory(llmContext.messages,
|
|
977
|
+
systemPrompt: [...(llmContext.systemPrompt ?? []), renderInbandToolPrompt(promptToolWireTools, ownedDialect)],
|
|
978
|
+
messages: encodeInbandToolHistory(llmContext.messages, ownedDialect, promptToolWireTools),
|
|
950
979
|
tools: undefined,
|
|
951
980
|
};
|
|
952
981
|
}
|
|
@@ -980,7 +1009,7 @@ async function streamAssistantResponse(
|
|
|
980
1009
|
// the hallucinated turn. Merged into the provider signal ONLY (not
|
|
981
1010
|
// `requestSignal`), so it cancels the request without tripping the loop's
|
|
982
1011
|
// external-abort handling (`abortRacePromise` / `requestSignal.aborted`).
|
|
983
|
-
const promptToolAbortController =
|
|
1012
|
+
const promptToolAbortController = ownedDialect ? new AbortController() : undefined;
|
|
984
1013
|
const providerAbortSignals: AbortSignal[] = [];
|
|
985
1014
|
if (requestSignal) providerAbortSignals.push(requestSignal);
|
|
986
1015
|
providerAbortSignals.push(repetitionAbortController.signal);
|
|
@@ -990,7 +1019,7 @@ async function streamAssistantResponse(
|
|
|
990
1019
|
const effectiveTemperature =
|
|
991
1020
|
harmonyRetryAttempt > 0 && config.temperature !== undefined ? config.temperature + 0.05 : config.temperature;
|
|
992
1021
|
// Owned tool calling sends no native tools, so any tool_choice would error.
|
|
993
|
-
const effectiveToolChoice =
|
|
1022
|
+
const effectiveToolChoice = ownedDialect ? undefined : (dynamicToolChoice ?? config.toolChoice);
|
|
994
1023
|
const effectiveReasoning = dynamicReasoning ?? config.reasoning;
|
|
995
1024
|
const effectiveDisableReasoning = dynamicDisableReasoning ?? config.disableReasoning;
|
|
996
1025
|
|
|
@@ -1058,7 +1087,7 @@ async function streamAssistantResponse(
|
|
|
1058
1087
|
signal: finalRequestSignal,
|
|
1059
1088
|
onResponse: captureOnResponse,
|
|
1060
1089
|
});
|
|
1061
|
-
if (promptToolWireTools &&
|
|
1090
|
+
if (promptToolWireTools && ownedDialect) {
|
|
1062
1091
|
// Re-materialize in-band tool-call text as native toolCall content blocks
|
|
1063
1092
|
// so the rest of the loop executes them unchanged. When the model starts
|
|
1064
1093
|
// fabricating tool results, the abort callback cancels the provider — unless
|
|
@@ -1067,7 +1096,7 @@ async function streamAssistantResponse(
|
|
|
1067
1096
|
response = wrapInbandToolStream(
|
|
1068
1097
|
response,
|
|
1069
1098
|
promptToolWireTools,
|
|
1070
|
-
|
|
1099
|
+
ownedDialect,
|
|
1071
1100
|
() => promptToolAbortController?.abort(),
|
|
1072
1101
|
config.abortOnFabricatedToolResult ?? true,
|
|
1073
1102
|
);
|
|
@@ -1795,7 +1824,24 @@ async function executeToolCalls(
|
|
|
1795
1824
|
}
|
|
1796
1825
|
}
|
|
1797
1826
|
|
|
1798
|
-
|
|
1827
|
+
// While an interruptible tool is in flight (e.g. a `job` poll blocking on
|
|
1828
|
+
// background work), a queued steer would otherwise wait out the tool's own
|
|
1829
|
+
// window. Poll the steering queue and let checkSteering() abort the shared
|
|
1830
|
+
// tool signal so the wait returns early; the boundary dequeue below then
|
|
1831
|
+
// injects it. Gated on immediate-interrupt mode + an interruptible tool;
|
|
1832
|
+
// checkSteering is idempotent (no-op once triggered).
|
|
1833
|
+
const watchSteeringWhileRunning =
|
|
1834
|
+
shouldInterruptImmediately &&
|
|
1835
|
+
(hasSteeringMessages !== undefined || getSteeringMessages !== undefined) &&
|
|
1836
|
+
records.some(r => r.tool?.interruptible === true);
|
|
1837
|
+
const steeringWatchTimer = watchSteeringWhileRunning
|
|
1838
|
+
? setInterval(() => void checkSteering(), STEERING_INTERRUPT_POLL_MS)
|
|
1839
|
+
: undefined;
|
|
1840
|
+
try {
|
|
1841
|
+
await Promise.allSettled(tasks);
|
|
1842
|
+
} finally {
|
|
1843
|
+
if (steeringWatchTimer !== undefined) clearInterval(steeringWatchTimer);
|
|
1844
|
+
}
|
|
1799
1845
|
// Yield after batch tool execution to let GC and I/O catch up,
|
|
1800
1846
|
// especially when tool results are large (e.g. bash output).
|
|
1801
1847
|
await yieldIfDue();
|
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,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ApiKey, Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
9
10
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
10
11
|
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
11
12
|
import type { AgentMessage } from "../types";
|
|
@@ -290,7 +291,7 @@ export async function generateBranchSummary(
|
|
|
290
291
|
// Transform to LLM-compatible messages, then serialize to text
|
|
291
292
|
// Serialization prevents the model from treating it as a conversation to continue
|
|
292
293
|
const llmMessages = (options.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
293
|
-
const conversationText = serializeConversation(llmMessages);
|
|
294
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
294
295
|
|
|
295
296
|
// Build prompt
|
|
296
297
|
const instructions = customInstructions || BRANCH_SUMMARY_PROMPT;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type Usage,
|
|
19
19
|
withAuth,
|
|
20
20
|
} from "@oh-my-pi/pi-ai";
|
|
21
|
+
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
21
22
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
22
23
|
import { countTokens } from "@oh-my-pi/pi-natives";
|
|
23
24
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
@@ -642,7 +643,7 @@ export async function generateSummary(
|
|
|
642
643
|
// Serialize conversation to text so model doesn't try to continue it
|
|
643
644
|
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
|
644
645
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(currentMessages);
|
|
645
|
-
const conversationText = serializeConversation(llmMessages);
|
|
646
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
646
647
|
|
|
647
648
|
// Build the prompt with conversation wrapped in tags
|
|
648
649
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
@@ -790,7 +791,7 @@ async function generateShortSummary(
|
|
|
790
791
|
): Promise<string> {
|
|
791
792
|
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
|
792
793
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(recentMessages);
|
|
793
|
-
const conversationText = serializeConversation(llmMessages);
|
|
794
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
794
795
|
|
|
795
796
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
796
797
|
if (historySummary) {
|
|
@@ -1155,7 +1156,7 @@ async function generateTurnPrefixSummary(
|
|
|
1155
1156
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
1156
1157
|
|
|
1157
1158
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
1158
|
-
const conversationText = serializeConversation(llmMessages);
|
|
1159
|
+
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));
|
|
1159
1160
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
1160
1161
|
const summarizationMessages = [
|
|
1161
1162
|
{
|
|
@@ -81,6 +81,16 @@ function createPrunedNotice(tokens: number): string {
|
|
|
81
81
|
return `[Output truncated - ${tokens} tokens]`;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Generic age-based pruning floor. Below this, blanking a result to
|
|
86
|
+
* `[Output truncated - N tokens]` recovers nothing — the placeholder itself
|
|
87
|
+
* costs ~8 tokens, so a sub-floor result grows the context (and churns the
|
|
88
|
+
* prompt cache) instead of shrinking it. Superseded/useless results keep their
|
|
89
|
+
* own rules: useless already drops no-savings candidates, superseded prunes for
|
|
90
|
+
* correctness regardless of size.
|
|
91
|
+
*/
|
|
92
|
+
const MIN_PRUNE_TOKENS = 50;
|
|
93
|
+
|
|
84
94
|
function getToolResultMessage(entry: SessionEntry): ToolResultMessage | undefined {
|
|
85
95
|
if (entry.type !== "message") return undefined;
|
|
86
96
|
const message = entry.message as AgentMessage;
|
|
@@ -271,7 +281,8 @@ export function pruneToolOutputs(entries: SessionEntry[], config: PruneConfig =
|
|
|
271
281
|
// any age).
|
|
272
282
|
const superseded = supersededMessages?.has(message) ?? false;
|
|
273
283
|
const useless = uselessMessages?.has(message) ?? false;
|
|
274
|
-
|
|
284
|
+
const tooSmall = tokens < MIN_PRUNE_TOKENS;
|
|
285
|
+
if (!superseded && !useless && (accumulatedTokens < config.protectTokens || isProtected || tooSmall)) {
|
|
275
286
|
accumulatedTokens += tokens;
|
|
276
287
|
continue;
|
|
277
288
|
}
|
package/src/compaction/utils.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Shared utilities for compaction and branch summarization.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Message } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import type { Message, ToolCall } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { type Dialect, getDialectDefinition } from "@oh-my-pi/pi-ai/dialect";
|
|
6
7
|
import { formatGroupedPaths, prompt } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import type { AgentMessage } from "../types";
|
|
8
9
|
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
@@ -188,9 +189,7 @@ function truncateForSummary(text: string, maxChars: number): string {
|
|
|
188
189
|
* This prevents the model from treating it as a conversation to continue.
|
|
189
190
|
* Call convertToLlm() first to handle custom message types.
|
|
190
191
|
*/
|
|
191
|
-
export function serializeConversation(messages: Message[]): string {
|
|
192
|
-
const parts: string[] = [];
|
|
193
|
-
|
|
192
|
+
export function serializeConversation(messages: Message[], dialect?: Dialect): string {
|
|
194
193
|
// Tool results flagged contextually useless (and their paired calls) are
|
|
195
194
|
// dropped from the serialized text: the source region is discarded after
|
|
196
195
|
// summarization anyway, so excluding them costs nothing and keeps garbage
|
|
@@ -201,7 +200,33 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
201
200
|
uselessCallIds.add(msg.toolCallId);
|
|
202
201
|
}
|
|
203
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
|
+
}
|
|
204
228
|
|
|
229
|
+
const parts: string[] = [];
|
|
205
230
|
for (const msg of messages) {
|
|
206
231
|
if (msg.role === "user") {
|
|
207
232
|
const content =
|
|
@@ -215,7 +240,7 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
215
240
|
} else if (msg.role === "assistant") {
|
|
216
241
|
const textParts: string[] = [];
|
|
217
242
|
const thinkingParts: string[] = [];
|
|
218
|
-
const toolCalls:
|
|
243
|
+
const toolCalls: ToolCall[] = [];
|
|
219
244
|
|
|
220
245
|
for (const block of msg.content) {
|
|
221
246
|
if (block.type === "text") {
|
|
@@ -224,22 +249,18 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
224
249
|
thinkingParts.push(block.thinking);
|
|
225
250
|
} else if (block.type === "toolCall") {
|
|
226
251
|
if (uselessCallIds.has(block.id)) continue;
|
|
227
|
-
|
|
228
|
-
const argsStr = Object.entries(args)
|
|
229
|
-
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
230
|
-
.join(", ");
|
|
231
|
-
toolCalls.push(`${block.name}(${argsStr})`);
|
|
252
|
+
toolCalls.push(block);
|
|
232
253
|
}
|
|
233
254
|
}
|
|
234
255
|
|
|
235
256
|
if (thinkingParts.length > 0) {
|
|
236
|
-
parts.push(`[
|
|
257
|
+
parts.push(`[Think]: ${thinkingParts.join("\n")}`);
|
|
237
258
|
}
|
|
238
259
|
if (textParts.length > 0) {
|
|
239
260
|
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
240
261
|
}
|
|
241
262
|
if (toolCalls.length > 0) {
|
|
242
|
-
parts.push(`[
|
|
263
|
+
parts.push(`[Tool Call]: ${renderToolCalls(toolCalls)}`);
|
|
243
264
|
}
|
|
244
265
|
} else if (msg.role === "toolResult") {
|
|
245
266
|
if (uselessCallIds.has(msg.toolCallId)) continue;
|
|
@@ -248,7 +269,8 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
248
269
|
.map(c => c.text)
|
|
249
270
|
.join("");
|
|
250
271
|
if (content) {
|
|
251
|
-
|
|
272
|
+
const text = truncateForSummary(content, TOOL_RESULT_MAX_CHARS);
|
|
273
|
+
parts.push(`[Tool Result]: ${text}`);
|
|
252
274
|
}
|
|
253
275
|
}
|
|
254
276
|
}
|
|
@@ -256,6 +278,21 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
256
278
|
return parts.join("\n\n");
|
|
257
279
|
}
|
|
258
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Render an assistant turn's tool calls as a compact `name(args)` list for the
|
|
283
|
+
* legacy serializer.
|
|
284
|
+
*/
|
|
285
|
+
function renderToolCalls(calls: ToolCall[]): string {
|
|
286
|
+
return calls
|
|
287
|
+
.map(call => {
|
|
288
|
+
const argsStr = Object.entries(call.arguments as Record<string, unknown>)
|
|
289
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
290
|
+
.join(", ");
|
|
291
|
+
return `${call.name}(${argsStr})`;
|
|
292
|
+
})
|
|
293
|
+
.join("; ");
|
|
294
|
+
}
|
|
295
|
+
|
|
259
296
|
// ============================================================================
|
|
260
297
|
// Summarization System Prompt
|
|
261
298
|
// ============================================================================
|
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
|
|
@@ -503,6 +510,15 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
503
510
|
concurrency?: "shared" | "exclusive" | ((args: Partial<Static<TParameters>>) => "shared" | "exclusive");
|
|
504
511
|
/** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
|
|
505
512
|
lenientArgValidation?: boolean;
|
|
513
|
+
/**
|
|
514
|
+
* If true, the agent loop may abort this tool mid-execution to deliver a
|
|
515
|
+
* queued steering message (instead of waiting for the tool to finish on its
|
|
516
|
+
* own). Set only on tools that purely *wait* and observe their abort signal
|
|
517
|
+
* cleanly (e.g. the `job` poll), so the abort surfaces the tool's current
|
|
518
|
+
* snapshot rather than corrupting a side effect. Honored only when
|
|
519
|
+
* `interruptMode` is "immediate".
|
|
520
|
+
*/
|
|
521
|
+
interruptible?: boolean;
|
|
506
522
|
/**
|
|
507
523
|
* Controls how the INTENT_FIELD (`_i`) is handled for this tool.
|
|
508
524
|
* - `"require"` (default): `_i` is injected and required in the parameter schema.
|