@oh-my-pi/pi-agent-core 15.13.0 → 15.13.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/CHANGELOG.md +219 -71
- package/dist/types/agent-loop.d.ts +2 -1
- package/dist/types/agent.d.ts +12 -2
- package/dist/types/append-only-context.d.ts +2 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/types.d.ts +23 -1
- package/package.json +6 -6
- package/src/agent-loop.ts +91 -11
- package/src/agent.ts +19 -3
- package/src/append-only-context.ts +4 -1
- package/src/index.ts +0 -1
- package/src/types.ts +23 -1
- package/dist/types/harmony-leak.d.ts +0 -118
- package/src/harmony-leak.ts +0 -456
package/src/agent-loop.ts
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
validateToolArguments,
|
|
16
16
|
zodToWireSchema,
|
|
17
17
|
} from "@oh-my-pi/pi-ai";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
encodeInbandToolHistory,
|
|
20
|
+
renderInbandToolPrompt,
|
|
21
|
+
renderToolExamples,
|
|
22
|
+
type ToolCallSyntax,
|
|
23
|
+
wrapInbandToolStream,
|
|
24
|
+
} from "@oh-my-pi/pi-ai/grammar";
|
|
19
25
|
import {
|
|
20
26
|
createHarmonyAuditEvent,
|
|
21
27
|
detectHarmonyLeakInAssistantMessage,
|
|
@@ -25,7 +31,9 @@ import {
|
|
|
25
31
|
isHarmonyLeakMitigationTarget,
|
|
26
32
|
recoverHarmonyToolCall,
|
|
27
33
|
signalListLabel,
|
|
28
|
-
} from "
|
|
34
|
+
} from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
35
|
+
import { preferredToolSyntax } from "@oh-my-pi/pi-catalog/identity";
|
|
36
|
+
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
29
37
|
import { type AgentRunCoverage, type AgentRunSummary, ToolCallBlockedError } from "./run-collector";
|
|
30
38
|
import {
|
|
31
39
|
type AgentTelemetry,
|
|
@@ -76,6 +84,25 @@ class HarmonyLeakInterruption extends Error {
|
|
|
76
84
|
this.name = "HarmonyLeakInterruption";
|
|
77
85
|
}
|
|
78
86
|
}
|
|
87
|
+
function resolveOwnedToolSyntaxFromEnv(value: string | undefined): ToolCallSyntax | undefined {
|
|
88
|
+
switch (value) {
|
|
89
|
+
case "1":
|
|
90
|
+
case "true":
|
|
91
|
+
return "glm";
|
|
92
|
+
case "glm":
|
|
93
|
+
case "hermes":
|
|
94
|
+
case "kimi":
|
|
95
|
+
case "xml":
|
|
96
|
+
case "anthropic":
|
|
97
|
+
case "deepseek":
|
|
98
|
+
case "harmony":
|
|
99
|
+
case "pi":
|
|
100
|
+
case "qwen3":
|
|
101
|
+
return value;
|
|
102
|
+
default:
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
79
106
|
|
|
80
107
|
type AssistantContentBlock = AssistantMessage["content"][number];
|
|
81
108
|
type AssistantToolCallBlock = Extract<AssistantContentBlock, { type: "toolCall" }>;
|
|
@@ -483,6 +510,7 @@ function injectIntentIntoSchema(schema: unknown, mode: "require" | "optional" =
|
|
|
483
510
|
properties: {
|
|
484
511
|
[INTENT_FIELD]: {
|
|
485
512
|
type: "string",
|
|
513
|
+
description: "Concise intent in present participle form (2-6 words) strictly on a single line, no newlines",
|
|
486
514
|
},
|
|
487
515
|
...properties,
|
|
488
516
|
},
|
|
@@ -490,7 +518,11 @@ function injectIntentIntoSchema(schema: unknown, mode: "require" | "optional" =
|
|
|
490
518
|
};
|
|
491
519
|
}
|
|
492
520
|
|
|
493
|
-
export function normalizeTools(
|
|
521
|
+
export function normalizeTools(
|
|
522
|
+
tools: AgentContext["tools"],
|
|
523
|
+
injectIntent: boolean,
|
|
524
|
+
exampleSyntax?: ToolCallSyntax,
|
|
525
|
+
): Context["tools"] {
|
|
494
526
|
injectIntent = injectIntent && Bun.env.PI_NO_INTENT !== "1";
|
|
495
527
|
return tools?.map(t => {
|
|
496
528
|
const intentMode = resolveIntentMode(t.intent);
|
|
@@ -504,7 +536,12 @@ export function normalizeTools(tools: AgentContext["tools"], injectIntent: boole
|
|
|
504
536
|
}
|
|
505
537
|
}
|
|
506
538
|
const description = t.description ?? "";
|
|
507
|
-
|
|
539
|
+
const injectExampleIntent = injectIntent && intentMode !== "omit";
|
|
540
|
+
const examplesBlock = exampleSyntax
|
|
541
|
+
? renderToolExamples({ ...t, parameters }, exampleSyntax, injectExampleIntent ? INTENT_FIELD : undefined)
|
|
542
|
+
: "";
|
|
543
|
+
const finalDescription = examplesBlock ? `${description}\n\n${examplesBlock}` : description;
|
|
544
|
+
return { ...t, parameters, description: finalDescription };
|
|
508
545
|
});
|
|
509
546
|
}
|
|
510
547
|
|
|
@@ -883,18 +920,37 @@ async function streamAssistantResponse(
|
|
|
883
920
|
let llmContext: Context;
|
|
884
921
|
if (config.appendOnlyContext) {
|
|
885
922
|
config.appendOnlyContext.syncMessages(normalizedMessages);
|
|
886
|
-
llmContext = config.appendOnlyContext.build(context, {
|
|
923
|
+
llmContext = config.appendOnlyContext.build(context, {
|
|
924
|
+
intentTracing: !!config.intentTracing,
|
|
925
|
+
exampleSyntax: preferredToolSyntax(config.model.id),
|
|
926
|
+
});
|
|
887
927
|
} else {
|
|
888
928
|
llmContext = {
|
|
889
929
|
systemPrompt: context.systemPrompt,
|
|
890
930
|
messages: normalizedMessages,
|
|
891
|
-
tools: normalizeTools(context.tools, !!config.intentTracing),
|
|
931
|
+
tools: normalizeTools(context.tools, !!config.intentTracing, preferredToolSyntax(config.model.id)),
|
|
892
932
|
};
|
|
893
933
|
}
|
|
894
934
|
if (config.transformProviderContext) {
|
|
895
935
|
llmContext = config.transformProviderContext(llmContext, config.model);
|
|
896
936
|
}
|
|
897
937
|
|
|
938
|
+
// Owned tool calling: take tool calls away from the provider and run them
|
|
939
|
+
// through the selected in-band prompt syntax. `PI_OWNED_TOOLS=1` still
|
|
940
|
+
// force-enables GLM; `PI_OWNED_TOOLS=<syntax>` force-enables that syntax.
|
|
941
|
+
const ownedSyntax: ToolCallSyntax | undefined =
|
|
942
|
+
config.toolCallSyntax ?? resolveOwnedToolSyntaxFromEnv(Bun.env.PI_OWNED_TOOLS);
|
|
943
|
+
let promptToolWireTools: Context["tools"];
|
|
944
|
+
if (ownedSyntax && llmContext.tools && llmContext.tools.length > 0) {
|
|
945
|
+
promptToolWireTools = llmContext.tools;
|
|
946
|
+
llmContext = {
|
|
947
|
+
...llmContext,
|
|
948
|
+
systemPrompt: [...(llmContext.systemPrompt ?? []), renderInbandToolPrompt(promptToolWireTools, ownedSyntax)],
|
|
949
|
+
messages: encodeInbandToolHistory(llmContext.messages, ownedSyntax, promptToolWireTools),
|
|
950
|
+
tools: undefined,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
898
954
|
const streamFunction = streamFn || streamSimple;
|
|
899
955
|
|
|
900
956
|
// Resolve API key (important for expiring tokens) — do this before resolving
|
|
@@ -919,12 +975,22 @@ async function streamAssistantResponse(
|
|
|
919
975
|
: harmonyAbortController.signal
|
|
920
976
|
: signal;
|
|
921
977
|
const repetitionAbortController = new AbortController();
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
978
|
+
// Owned tool calling: aborted by the stream wrapper when the model starts
|
|
979
|
+
// fabricating a `<tool_response>`, so the provider stops generating the rest of
|
|
980
|
+
// the hallucinated turn. Merged into the provider signal ONLY (not
|
|
981
|
+
// `requestSignal`), so it cancels the request without tripping the loop's
|
|
982
|
+
// external-abort handling (`abortRacePromise` / `requestSignal.aborted`).
|
|
983
|
+
const promptToolAbortController = ownedSyntax ? new AbortController() : undefined;
|
|
984
|
+
const providerAbortSignals: AbortSignal[] = [];
|
|
985
|
+
if (requestSignal) providerAbortSignals.push(requestSignal);
|
|
986
|
+
providerAbortSignals.push(repetitionAbortController.signal);
|
|
987
|
+
if (promptToolAbortController) providerAbortSignals.push(promptToolAbortController.signal);
|
|
988
|
+
const finalRequestSignal =
|
|
989
|
+
providerAbortSignals.length === 1 ? providerAbortSignals[0]! : AbortSignal.any(providerAbortSignals);
|
|
925
990
|
const effectiveTemperature =
|
|
926
991
|
harmonyRetryAttempt > 0 && config.temperature !== undefined ? config.temperature + 0.05 : config.temperature;
|
|
927
|
-
|
|
992
|
+
// Owned tool calling sends no native tools, so any tool_choice would error.
|
|
993
|
+
const effectiveToolChoice = ownedSyntax ? undefined : (dynamicToolChoice ?? config.toolChoice);
|
|
928
994
|
const effectiveReasoning = dynamicReasoning ?? config.reasoning;
|
|
929
995
|
const effectiveDisableReasoning = dynamicDisableReasoning ?? config.disableReasoning;
|
|
930
996
|
|
|
@@ -969,7 +1035,7 @@ async function streamAssistantResponse(
|
|
|
969
1035
|
|
|
970
1036
|
try {
|
|
971
1037
|
return await runInActiveSpan(chatSpan, async () => {
|
|
972
|
-
|
|
1038
|
+
let response = await streamFunction(config.model, llmContext, {
|
|
973
1039
|
...config,
|
|
974
1040
|
// Hand streamSimple a resolver so its central auth-retry policy can
|
|
975
1041
|
// re-resolve on 401 / usage-limit: the initial step reuses the key
|
|
@@ -992,6 +1058,20 @@ async function streamAssistantResponse(
|
|
|
992
1058
|
signal: finalRequestSignal,
|
|
993
1059
|
onResponse: captureOnResponse,
|
|
994
1060
|
});
|
|
1061
|
+
if (promptToolWireTools && ownedSyntax) {
|
|
1062
|
+
// Re-materialize in-band tool-call text as native toolCall content blocks
|
|
1063
|
+
// so the rest of the loop executes them unchanged. When the model starts
|
|
1064
|
+
// fabricating tool results, the abort callback cancels the provider — unless
|
|
1065
|
+
// `abortOnFabricatedToolResult` is false, in which case the stream drains and
|
|
1066
|
+
// the fabricated continuation is discarded without aborting.
|
|
1067
|
+
response = wrapInbandToolStream(
|
|
1068
|
+
response,
|
|
1069
|
+
promptToolWireTools,
|
|
1070
|
+
ownedSyntax,
|
|
1071
|
+
() => promptToolAbortController?.abort(),
|
|
1072
|
+
config.abortOnFabricatedToolResult ?? true,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
995
1075
|
|
|
996
1076
|
let partialMessage: AssistantMessage | null = null;
|
|
997
1077
|
let addedPartial = false;
|
package/src/agent.ts
CHANGED
|
@@ -22,11 +22,12 @@ import {
|
|
|
22
22
|
type ToolChoice,
|
|
23
23
|
type ToolResultMessage,
|
|
24
24
|
} from "@oh-my-pi/pi-ai";
|
|
25
|
+
import type { ToolCallSyntax } from "@oh-my-pi/pi-ai/grammar";
|
|
26
|
+
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
25
27
|
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
26
28
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
27
29
|
import { abortReasonText, agentLoop, agentLoopContinue } from "./agent-loop";
|
|
28
30
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
29
|
-
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
30
31
|
import type {
|
|
31
32
|
AgentContext,
|
|
32
33
|
AgentEvent,
|
|
@@ -220,6 +221,15 @@ export interface AgentOptions {
|
|
|
220
221
|
|
|
221
222
|
/** Enable intent tracing schema injection/stripping in the harness. */
|
|
222
223
|
intentTracing?: boolean;
|
|
224
|
+
/** Owned tool-calling syntax. Undefined keeps provider-native tool calling. */
|
|
225
|
+
toolCallSyntax?: ToolCallSyntax;
|
|
226
|
+
/**
|
|
227
|
+
* When owned tool calling is active and the model fabricates a tool result
|
|
228
|
+
* mid-turn: `true` (default) aborts the provider request immediately; `false`
|
|
229
|
+
* drains the request and discards the fabricated continuation. Forwarded to
|
|
230
|
+
* the loop's {@link AgentLoopConfig.abortOnFabricatedToolResult}.
|
|
231
|
+
*/
|
|
232
|
+
abortOnFabricatedToolResult?: boolean;
|
|
223
233
|
/** Dynamic tool choice override, resolved per LLM call. */
|
|
224
234
|
getToolChoice?: () => ToolChoice | undefined;
|
|
225
235
|
|
|
@@ -316,6 +326,8 @@ export class Agent {
|
|
|
316
326
|
#preferWebsockets?: boolean;
|
|
317
327
|
#transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
|
318
328
|
#intentTracing: boolean;
|
|
329
|
+
#toolCallSyntax?: ToolCallSyntax;
|
|
330
|
+
#abortOnFabricatedToolResult?: boolean;
|
|
319
331
|
#getToolChoice?: () => ToolChoice | undefined;
|
|
320
332
|
#onPayload?: SimpleStreamOptions["onPayload"];
|
|
321
333
|
#onResponse?: SimpleStreamOptions["onResponse"];
|
|
@@ -378,6 +390,8 @@ export class Agent {
|
|
|
378
390
|
this.#preferWebsockets = opts.preferWebsockets;
|
|
379
391
|
this.#transformToolCallArguments = opts.transformToolCallArguments;
|
|
380
392
|
this.#intentTracing = opts.intentTracing === true;
|
|
393
|
+
this.#toolCallSyntax = opts.toolCallSyntax;
|
|
394
|
+
this.#abortOnFabricatedToolResult = opts.abortOnFabricatedToolResult;
|
|
381
395
|
this.#getToolChoice = opts.getToolChoice;
|
|
382
396
|
this.#onAssistantMessageEvent = opts.onAssistantMessageEvent;
|
|
383
397
|
this.#onHarmonyLeak = opts.onHarmonyLeak;
|
|
@@ -657,8 +671,8 @@ export class Agent {
|
|
|
657
671
|
}
|
|
658
672
|
|
|
659
673
|
// State mutators
|
|
660
|
-
setSystemPrompt(v: string[]) {
|
|
661
|
-
this.#state.systemPrompt = v;
|
|
674
|
+
setSystemPrompt(v: string[] | string) {
|
|
675
|
+
this.#state.systemPrompt = typeof v === "string" ? [v] : v;
|
|
662
676
|
}
|
|
663
677
|
|
|
664
678
|
setModel(m: Model) {
|
|
@@ -1023,6 +1037,8 @@ export class Agent {
|
|
|
1023
1037
|
cursorOnToolResult,
|
|
1024
1038
|
transformToolCallArguments: this.#transformToolCallArguments,
|
|
1025
1039
|
intentTracing: this.#intentTracing,
|
|
1040
|
+
toolCallSyntax: this.#toolCallSyntax,
|
|
1041
|
+
abortOnFabricatedToolResult: this.#abortOnFabricatedToolResult,
|
|
1026
1042
|
appendOnlyContext: this.#appendOnlyContext,
|
|
1027
1043
|
beforeToolCall: this.beforeToolCall ? (ctx, signal) => this.beforeToolCall?.(ctx, signal) : undefined,
|
|
1028
1044
|
afterToolCall: this.afterToolCall ? (ctx, signal) => this.afterToolCall?.(ctx, signal) : undefined,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
|
|
18
|
+
import type { ToolCallSyntax } from "@oh-my-pi/pi-ai/grammar";
|
|
18
19
|
import { normalizeTools } from "./agent-loop";
|
|
19
20
|
import type { AgentContext } from "./types";
|
|
20
21
|
|
|
@@ -33,6 +34,7 @@ export interface StablePrefixSnapshot {
|
|
|
33
34
|
export interface BuildOptions {
|
|
34
35
|
/** Inject the `_i` intent field into tool schemas (must match agent-loop's normalizeTools). */
|
|
35
36
|
intentTracing: boolean;
|
|
37
|
+
exampleSyntax?: ToolCallSyntax;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -268,7 +270,7 @@ export class AppendOnlyContextManager {
|
|
|
268
270
|
|
|
269
271
|
function takeSnapshot(context: AgentContext, options: BuildOptions): StablePrefixSnapshot {
|
|
270
272
|
const systemPrompt = [...context.systemPrompt];
|
|
271
|
-
const tools = normalizeTools(context.tools, options.intentTracing) ?? [];
|
|
273
|
+
const tools = normalizeTools(context.tools, options.intentTracing, options.exampleSyntax) ?? [];
|
|
272
274
|
return {
|
|
273
275
|
systemPrompt,
|
|
274
276
|
tools,
|
|
@@ -288,6 +290,7 @@ function computeFingerprint(systemPrompt: string[], tools: Tool[], options: Buil
|
|
|
288
290
|
cw: t.customWireName,
|
|
289
291
|
})),
|
|
290
292
|
i: options.intentTracing,
|
|
293
|
+
ex: options.exampleSyntax,
|
|
291
294
|
});
|
|
292
295
|
let hash = 0;
|
|
293
296
|
for (let i = 0; i < payload.length; i++) {
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -17,8 +17,9 @@ import type {
|
|
|
17
17
|
ToolResultMessage,
|
|
18
18
|
TSchema,
|
|
19
19
|
} from "@oh-my-pi/pi-ai";
|
|
20
|
+
import type { ToolCallSyntax } from "@oh-my-pi/pi-ai/grammar";
|
|
21
|
+
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
20
22
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
21
|
-
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
22
23
|
import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
|
|
23
24
|
import type { AgentTelemetryConfig } from "./telemetry";
|
|
24
25
|
|
|
@@ -199,6 +200,27 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
199
200
|
* then strips from arguments before executing tools.
|
|
200
201
|
*/
|
|
201
202
|
intentTracing?: boolean;
|
|
203
|
+
/**
|
|
204
|
+
* Owned tool calling syntax.
|
|
205
|
+
*
|
|
206
|
+
* Undefined keeps provider-native tool calling. A syntax value sends no
|
|
207
|
+
* native `tools`, forces `toolChoice` off, appends that syntax's tool catalog
|
|
208
|
+
* instructions, re-encodes prior tool calls/results as text, and parses the
|
|
209
|
+
* model's text output back into canonical `toolCall` blocks.
|
|
210
|
+
*/
|
|
211
|
+
toolCallSyntax?: ToolCallSyntax;
|
|
212
|
+
/**
|
|
213
|
+
* When owned (in-band) tool calling is active and the model starts
|
|
214
|
+
* fabricating a tool result inside its own turn, control how the loop reacts:
|
|
215
|
+
* - `true` (default): abort the provider request immediately so it stops
|
|
216
|
+
* generating the hallucinated continuation (cheaper, lower latency).
|
|
217
|
+
* - `false`: let the request finish and silently discard everything past the
|
|
218
|
+
* fabrication boundary (keeps the connection alive but pays for the tokens
|
|
219
|
+
* the model spends on the discarded tail).
|
|
220
|
+
* Only meaningful when {@link toolCallSyntax} (or `PI_OWNED_TOOLS`) selects an
|
|
221
|
+
* owned syntax; native tool calling never fabricates results in text.
|
|
222
|
+
*/
|
|
223
|
+
abortOnFabricatedToolResult?: boolean;
|
|
202
224
|
/**
|
|
203
225
|
* Append-only context mode — stabilizes system prompt + tool spec bytes
|
|
204
226
|
* across turns so provider prefix caches hit at maximum rate.
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GPT-5 Harmony-header leakage detection and recovery.
|
|
3
|
-
*
|
|
4
|
-
* Background and policy: see `docs/ERRATA-GPT5-HARMONY.md`. This module
|
|
5
|
-
* implements §3 of that document: detection by signal fusion, plus a
|
|
6
|
-
* truncate-and-resume primitive for the `edit` tool when its input is in
|
|
7
|
-
* hashline DSL form. Other tools and surfaces fall through to
|
|
8
|
-
* abort-and-retry handled by the agent loop.
|
|
9
|
-
*/
|
|
10
|
-
import type { AssistantMessage, Model, ToolCall } from "@oh-my-pi/pi-ai";
|
|
11
|
-
declare const SIGNAL_ORDER: readonly ["M", "C", "G", "S", "B", "R", "T"];
|
|
12
|
-
export type HarmonySignalClass = "H" | (typeof SIGNAL_ORDER)[number];
|
|
13
|
-
export type HarmonySurface = "assistant_text" | "assistant_thinking" | "tool_arg";
|
|
14
|
-
export interface HarmonySignal {
|
|
15
|
-
classes: HarmonySignalClass[];
|
|
16
|
-
start: number;
|
|
17
|
-
end: number;
|
|
18
|
-
text: string;
|
|
19
|
-
}
|
|
20
|
-
export interface HarmonyDetection {
|
|
21
|
-
surface: HarmonySurface;
|
|
22
|
-
contentIndex?: number;
|
|
23
|
-
toolName?: string;
|
|
24
|
-
toolCallId?: string;
|
|
25
|
-
signals: HarmonySignal[];
|
|
26
|
-
}
|
|
27
|
-
export interface HarmonyAuditEvent {
|
|
28
|
-
action: "truncate_resume" | "abort_retry" | "escalated";
|
|
29
|
-
surface: HarmonySurface;
|
|
30
|
-
signal: string;
|
|
31
|
-
retryN: number;
|
|
32
|
-
model: string;
|
|
33
|
-
provider: string;
|
|
34
|
-
toolName?: string;
|
|
35
|
-
removedLen: number;
|
|
36
|
-
removedSha8: string;
|
|
37
|
-
removedPreview: string;
|
|
38
|
-
removedBlob?: string;
|
|
39
|
-
}
|
|
40
|
-
export interface HarmonyRecoveredToolCall {
|
|
41
|
-
message: AssistantMessage;
|
|
42
|
-
removed: string;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Whether to run leak detection on responses from this model. We default-on
|
|
46
|
-
* for every openai-codex model rather than enumerating ids, so a future
|
|
47
|
-
* gpt-5.6 (or whatever) doesn't silently bypass the mitigation. Detection
|
|
48
|
-
* itself is cheap; the cost of missing a leak on a new model is not.
|
|
49
|
-
*/
|
|
50
|
-
export declare function isHarmonyLeakMitigationTarget(model: Model): boolean;
|
|
51
|
-
export declare function signalListLabel(signals: readonly HarmonySignal[]): string;
|
|
52
|
-
/**
|
|
53
|
-
* Detect harmony-protocol leakage in `text`. Returns undefined if clean.
|
|
54
|
-
*
|
|
55
|
-
* Trip rule: `H` alone, or `M` paired with at least one co-signal
|
|
56
|
-
* (`C`/`G`/`S`/`B`/`R`/`T`). Bare `M` does not trip — this document, its
|
|
57
|
-
* tests, and bug reports legitimately carry the marker.
|
|
58
|
-
*
|
|
59
|
-
* The `tool_arg` surface is held to a stricter rule. A tool argument is
|
|
60
|
-
* arbitrary file/data content that can legitimately carry the marker, a
|
|
61
|
-
* channel word, harmony control tokens, or a non-Latin script run (editing
|
|
62
|
-
* these very fixtures does exactly that). The only robust leak signal there
|
|
63
|
-
* is content trailing the structurally-valid parse, so a `tool_arg` detection
|
|
64
|
-
* additionally requires the `T` co-signal. Absent a `parsedEnd` boundary `T`
|
|
65
|
-
* is never set, so `tool_arg` scanning stays inert and a legitimate codex tool
|
|
66
|
-
* call is never hard-aborted. `assistant_text`/`assistant_thinking` keep the
|
|
67
|
-
* base rule.
|
|
68
|
-
*
|
|
69
|
-
* `parsedEnd`, when supplied, marks the byte at which a structurally valid
|
|
70
|
-
* tool-argument parse ends; markers at or past it set the `T` co-signal.
|
|
71
|
-
* `contentIndex`/`toolName`/`toolCallId` flow through to the returned
|
|
72
|
-
* detection for downstream auditing.
|
|
73
|
-
*/
|
|
74
|
-
export declare function detectHarmonyLeak(text: string, surface: HarmonySurface, options?: {
|
|
75
|
-
parsedEnd?: number;
|
|
76
|
-
contentIndex?: number;
|
|
77
|
-
toolName?: string;
|
|
78
|
-
toolCallId?: string;
|
|
79
|
-
}): HarmonyDetection | undefined;
|
|
80
|
-
/**
|
|
81
|
-
* Scan an assistant message's content blocks; return the first detection.
|
|
82
|
-
*
|
|
83
|
-
* `toolArgParseEnd`, when supplied, resolves the byte offset at which a tool
|
|
84
|
-
* call's structurally-valid argument parse ends (the `T` co-signal in
|
|
85
|
-
* {@link detectHarmonyLeak}). Callers that can parse a tool's argument DSL pass
|
|
86
|
-
* it to enable `tool_arg` leak detection; omitting it keeps that surface inert
|
|
87
|
-
* — the safe default the agent loop relies on, since it cannot bound a streamed
|
|
88
|
-
* tool DSL and must never hard-abort a legitimate tool call.
|
|
89
|
-
*/
|
|
90
|
-
export declare function detectHarmonyLeakInAssistantMessage(message: AssistantMessage, toolArgParseEnd?: (toolCall: ToolCall) => number | undefined): HarmonyDetection | undefined;
|
|
91
|
-
/**
|
|
92
|
-
* Truncate a contaminated tool call at the start of the contaminated line and
|
|
93
|
-
* append the tool's recovery sentinel. Returns a recovered AssistantMessage
|
|
94
|
-
* (containing only the cleaned tool call), a synthetic continuation user
|
|
95
|
-
* message asking the model to re-issue the rest, and the removed substring
|
|
96
|
-
* for auditing. Returns undefined when the tool is not recovery-eligible or
|
|
97
|
-
* the truncation would leave nothing meaningful to dispatch.
|
|
98
|
-
*
|
|
99
|
-
* `providerPayload` is dropped from the recovered message: for Codex the
|
|
100
|
-
* encrypted reasoning blob is opaque/signed and we cannot validate that it is
|
|
101
|
-
* uncontaminated. The model re-reasons on the next turn.
|
|
102
|
-
*/
|
|
103
|
-
export declare function recoverHarmonyToolCall(message: AssistantMessage, detection: HarmonyDetection): HarmonyRecoveredToolCall | undefined;
|
|
104
|
-
/**
|
|
105
|
-
* Return the contaminated substring from `message` for audit purposes when
|
|
106
|
-
* recovery is not applicable (abort path). Walks from the first detected
|
|
107
|
-
* signal to end-of-content within the relevant block. Returns "" if the
|
|
108
|
-
* detection cannot be resolved against the message.
|
|
109
|
-
*/
|
|
110
|
-
export declare function extractHarmonyRemoved(message: AssistantMessage, detection: HarmonyDetection): string;
|
|
111
|
-
export declare function createHarmonyAuditEvent(params: {
|
|
112
|
-
action: HarmonyAuditEvent["action"];
|
|
113
|
-
detection: HarmonyDetection;
|
|
114
|
-
model: Model;
|
|
115
|
-
retryN: number;
|
|
116
|
-
removed: string;
|
|
117
|
-
}): HarmonyAuditEvent;
|
|
118
|
-
export {};
|