@oh-my-pi/pi-agent-core 18.15.0 → 18.17.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 +22 -0
- package/dist/types/agent-loop.d.ts +2 -0
- package/dist/types/agent.d.ts +11 -1
- package/dist/types/append-only-context.d.ts +9 -0
- package/dist/types/compaction/branch-summarization.d.ts +2 -3
- package/dist/types/compaction/compaction.d.ts +2 -2
- package/dist/types/compaction/utils.d.ts +6 -22
- package/dist/types/telemetry.d.ts +3 -1
- package/dist/types/tokenizer.d.ts +9 -9
- package/dist/types/types.d.ts +19 -0
- package/package.json +11 -11
- package/src/agent-loop.ts +75 -23
- package/src/agent.ts +83 -10
- package/src/append-only-context.ts +49 -18
- package/src/compaction/branch-summarization.ts +4 -10
- package/src/compaction/compaction-v2-streaming.ts +2 -3
- package/src/compaction/compaction.ts +6 -10
- package/src/compaction/openai.ts +0 -1
- package/src/compaction/utils.ts +19 -98
- package/src/telemetry.ts +1 -1
- package/src/tokenizer.ts +53 -24
- package/src/types.ts +21 -0
- package/src/compaction/prompts/file-operations.md +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.17.0] - 2026-11-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `Agent.queuedMessageGate` to filter queued messages before delivery and observe claimed messages returned undelivered.
|
|
10
|
+
- Added `wireToolsFingerprint()`, returning a hash of the tool specs sent with a request.
|
|
11
|
+
|
|
12
|
+
## [18.16.0] - 2026-11-02
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Deprecated `createFileOps`, `computeFileLists`, `formatFileOperations`, `upsertFileOperations`, `isUrlSchemePath` and `FileOperations` in `@oh-my-pi/pi-agent-core/compaction`; they now re-export `@oh-my-pi/snapcompact`, which callers should import directly
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Context estimates for a model without a known tokenizer family (Gemini, Grok and the like), and token counts made without a model (compress metrics, the legacy `estimateTokens` export), count text in o200k_base instead of guessing 4 bytes per token, which undercounted what those providers bill by 11–48% on code-heavy sessions and let compaction fire late after a switch to them ([#17607](https://github.com/can1357/oh-my-pi/pull/17607) by [@will-bogusz](https://github.com/will-bogusz))
|
|
21
|
+
- Context estimates no longer charge a reader another model's thinking signatures or redacted thinking, which the request strips: a Claude session switched to Grok or Gemini, including Gemini through Factory, counted them as text ([#17607](https://github.com/can1357/oh-my-pi/pull/17607) by [@will-bogusz](https://github.com/will-bogusz))
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- Removed `PI_TOKENIZER_ACCURATE`: every model is now counted with a native tokenizer ([#17607](https://github.com/can1357/oh-my-pi/pull/17607) by [@will-bogusz](https://github.com/will-bogusz))
|
|
26
|
+
|
|
5
27
|
## [18.15.0] - 2026-11-01
|
|
6
28
|
|
|
7
29
|
### Fixed
|
|
@@ -102,6 +102,8 @@ export declare function normalizeMessagesForProvider(messages: Context["messages
|
|
|
102
102
|
export interface NormalizeToolsOptions {
|
|
103
103
|
/** Inject the `i` intent field into tool schemas (subject to `PI_NO_INTENT`). */
|
|
104
104
|
injectIntent: boolean;
|
|
105
|
+
/** Schema description of the injected `i` field; `PI_INTENT_DESC` overrides it. Default `concise intent`. */
|
|
106
|
+
intentDescription?: string;
|
|
105
107
|
/** Strip descriptions from the wire specs when the catalog rides in the system prompt. */
|
|
106
108
|
pruneDescriptions?: boolean;
|
|
107
109
|
}
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
|
4
4
|
import type { AppendOnlyContextManager } from "./append-only-context.js";
|
|
5
5
|
import { SentToolDefinitions } from "./sent-tool-definitions.js";
|
|
6
6
|
import { Tokenizer } from "./tokenizer.js";
|
|
7
|
-
import type { AgentBeforeModelCall, AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideBoundary, AsideMessage, PrepareQueuedMessages, SpeculativeToolExecutionConfig, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
|
|
7
|
+
import type { AgentBeforeModelCall, AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideBoundary, AsideMessage, PrepareQueuedMessages, QueuedMessageGate, SpeculativeToolExecutionConfig, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
|
|
8
8
|
export declare class AgentBusyError extends Error {
|
|
9
9
|
constructor(message?: string);
|
|
10
10
|
}
|
|
@@ -157,6 +157,8 @@ export interface AgentOptions {
|
|
|
157
157
|
suggestFallbackToolNames?: () => Iterable<string>;
|
|
158
158
|
/** Enable intent tracing schema injection/stripping in the harness. */
|
|
159
159
|
intentTracing?: boolean;
|
|
160
|
+
/** Schema description of the injected intent field; `PI_INTENT_DESC` overrides it. Default `concise intent`. */
|
|
161
|
+
intentDescription?: string;
|
|
160
162
|
/**
|
|
161
163
|
* Strip tool descriptions from provider-bound tool specs (top-level + nested
|
|
162
164
|
* schema annotations). Use when the full catalog is rendered into the system
|
|
@@ -252,6 +254,12 @@ export declare class Agent {
|
|
|
252
254
|
getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
|
|
253
255
|
/** Prepare actual queue deliveries after dequeue gates; commit runs only while ownership remains valid. */
|
|
254
256
|
prepareQueuedMessages?: PrepareQueuedMessages;
|
|
257
|
+
/**
|
|
258
|
+
* Consulted synchronously whenever queued messages are removed for delivery and
|
|
259
|
+
* told about claimed messages that went back undelivered (see {@link QueuedMessageGate}).
|
|
260
|
+
* Unset, batches are taken as they are.
|
|
261
|
+
*/
|
|
262
|
+
queuedMessageGate?: QueuedMessageGate;
|
|
255
263
|
/**
|
|
256
264
|
* Hook invoked after tool arguments are validated and before execution.
|
|
257
265
|
* Reassign at any time to swap the implementation (e.g. on extension reload).
|
|
@@ -405,6 +413,8 @@ export declare class Agent {
|
|
|
405
413
|
/** Inject/strip the intent field on tool calls; applies from the next prompt run. */
|
|
406
414
|
get intentTracing(): boolean;
|
|
407
415
|
set intentTracing(value: boolean);
|
|
416
|
+
/** The intent field's schema description this agent was built with (`PI_INTENT_DESC` still overrides it). */
|
|
417
|
+
get intentDescription(): string | undefined;
|
|
408
418
|
/** Abort the provider request on a fabricated tool result; applies from the next prompt run. */
|
|
409
419
|
get abortOnFabricatedToolResult(): boolean | undefined;
|
|
410
420
|
set abortOnFabricatedToolResult(value: boolean | undefined);
|
|
@@ -25,6 +25,8 @@ export interface StablePrefixSnapshot {
|
|
|
25
25
|
export interface BuildOptions {
|
|
26
26
|
/** Inject the `i` intent field into tool schemas (must match agent-loop's normalizeTools). */
|
|
27
27
|
intentTracing: boolean;
|
|
28
|
+
/** Schema description of the injected `i` field (must match agent-loop's normalizeTools). */
|
|
29
|
+
intentDescription?: string;
|
|
28
30
|
/** Strip tool descriptions from the provider-bound specs (must match normalizeTools). */
|
|
29
31
|
pruneToolDescriptions?: boolean;
|
|
30
32
|
}
|
|
@@ -125,3 +127,10 @@ export declare class AppendOnlyContextManager {
|
|
|
125
127
|
invalidate(): void;
|
|
126
128
|
reset(context: AgentContext, options: BuildOptions): void;
|
|
127
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Fingerprint of the tool specs a request built with `options` sends: the
|
|
132
|
+
* tools half of the prefix fingerprint, for a caller that compares tool sets
|
|
133
|
+
* across requests. A same-name tool whose description, schema, examples,
|
|
134
|
+
* `strict`, `customFormat` or `customWireName` changed makes another one.
|
|
135
|
+
*/
|
|
136
|
+
export declare function wireToolsFingerprint(tools: AgentContext["tools"], options: BuildOptions): string;
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* a summary of the branch being left so context isn't lost.
|
|
6
6
|
*/
|
|
7
7
|
import type { Api, ApiKey, AssistantMessage, Context, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
8
9
|
import { type AgentTelemetry } from "../telemetry.js";
|
|
9
10
|
import { Tokenizer } from "../tokenizer.js";
|
|
10
11
|
import type { AgentMessage } from "../types.js";
|
|
11
12
|
import type { ReadonlySessionManager, SessionEntry } from "./entries.js";
|
|
12
13
|
import { type ConvertToLlm } from "./messages.js";
|
|
13
|
-
import { type FileOperations } from "./utils.js";
|
|
14
14
|
export interface BranchSummaryResult {
|
|
15
15
|
summary?: string;
|
|
16
16
|
readFiles?: string[];
|
|
@@ -23,12 +23,11 @@ export interface BranchSummaryDetails {
|
|
|
23
23
|
readFiles: string[];
|
|
24
24
|
modifiedFiles: string[];
|
|
25
25
|
}
|
|
26
|
-
export type { FileOperations } from "./utils.js";
|
|
27
26
|
export interface BranchPreparation {
|
|
28
27
|
/** Messages extracted for summarization, in chronological order */
|
|
29
28
|
messages: AgentMessage[];
|
|
30
29
|
/** File operations extracted from tool calls */
|
|
31
|
-
fileOps: FileOperations;
|
|
30
|
+
fileOps: snapcompact.FileOperations;
|
|
32
31
|
/** Total estimated tokens in messages */
|
|
33
32
|
totalTokens: number;
|
|
34
33
|
}
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* and after compaction the session is reloaded.
|
|
6
6
|
*/
|
|
7
7
|
import { type Api, type ApiKey, type AssistantMessage, type CodexCompactionContext, type Context, type FetchImpl, type MessageAttribution, type Model, type OneshotRetryOptions, type ProviderSessionState, type SimpleStreamOptions, type Tool, type Usage } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
8
9
|
import { type AgentTelemetry } from "../telemetry.js";
|
|
9
10
|
import { ThinkingLevel } from "../thinking.js";
|
|
10
11
|
import { Tokenizer } from "../tokenizer.js";
|
|
11
12
|
import type { AgentMessage } from "../types.js";
|
|
12
13
|
import type { SessionEntry } from "./entries.js";
|
|
13
14
|
import { type ConvertToLlm } from "./messages.js";
|
|
14
|
-
import { type FileOperations } from "./utils.js";
|
|
15
15
|
/** Details stored in CompactionEntry.details for file tracking */
|
|
16
16
|
export interface CompactionDetails {
|
|
17
17
|
readFiles: string[];
|
|
@@ -368,7 +368,7 @@ export interface CompactionPreparation {
|
|
|
368
368
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
369
369
|
previousPreserveData?: Record<string, unknown>;
|
|
370
370
|
/** File operations extracted from messagesToSummarize */
|
|
371
|
-
fileOps: FileOperations;
|
|
371
|
+
fileOps: snapcompact.FileOperations;
|
|
372
372
|
/** Compaction settions from settings.jsonl */
|
|
373
373
|
settings: CompactionSettings;
|
|
374
374
|
}
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { Message } from "@oh-my-pi/pi-ai";
|
|
5
5
|
import { type Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
6
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
6
7
|
import type { AgentMessage } from "../types.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
export declare function createFileOps(): FileOperations;
|
|
8
|
+
/** @deprecated Import these from `@oh-my-pi/snapcompact`. */
|
|
9
|
+
export { computeFileLists, createFileOps, type FileOperations, isUrlSchemePath, upsertFileOperations, } from "@oh-my-pi/snapcompact";
|
|
10
|
+
/** @deprecated Use `upsertFileOperations("", readFiles, modifiedFiles, readSet)` from `@oh-my-pi/snapcompact`. */
|
|
11
|
+
export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
|
|
13
12
|
/** Error text for a summarization call that hit its output limit (`stopReason: "length"`): partial text is never used. */
|
|
14
13
|
export declare function incompleteOutputError(prefix: string): string;
|
|
15
14
|
/**
|
|
@@ -33,25 +32,10 @@ export declare function stripReadSelector(path: string): string;
|
|
|
33
32
|
* than its plain lines: verbatim `raw` bytes or the merge `conflicts` view.
|
|
34
33
|
*/
|
|
35
34
|
export declare function isAlternateFormReadSelector(sel: string): boolean;
|
|
36
|
-
/**
|
|
37
|
-
* Whether `path` references a `scheme://` URL (internal URI or web URL) rather
|
|
38
|
-
* than a filesystem path that belongs in the compaction `<files>` summary.
|
|
39
|
-
*/
|
|
40
|
-
export declare function isUrlSchemePath(path: string): boolean;
|
|
41
35
|
/**
|
|
42
36
|
* Extract file operations from tool calls in an assistant message.
|
|
43
37
|
*/
|
|
44
|
-
export declare function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void;
|
|
45
|
-
/**
|
|
46
|
-
* Compute final file lists from file operations.
|
|
47
|
-
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
|
48
|
-
*/
|
|
49
|
-
export declare function computeFileLists(fileOps: FileOperations): {
|
|
50
|
-
readFiles: string[];
|
|
51
|
-
modifiedFiles: string[];
|
|
52
|
-
};
|
|
53
|
-
export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
|
|
54
|
-
export declare function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
|
|
38
|
+
export declare function extractFileOpsFromMessage(message: AgentMessage, fileOps: snapcompact.FileOperations): void;
|
|
55
39
|
/**
|
|
56
40
|
* Truncate tool results to the same representation used in summarization prompts.
|
|
57
41
|
*/
|
|
@@ -558,7 +558,9 @@ export interface InstrumentedChatSpanOptions {
|
|
|
558
558
|
* Short-circuits when `telemetry` is `undefined` so cost / overhead stays at
|
|
559
559
|
* zero for installations without an OTEL SDK.
|
|
560
560
|
*/
|
|
561
|
-
export declare function instrumentedCompleteSimple<TApi extends Api>(model: Model<TApi>, ctx: Context, options: SimpleStreamOptions
|
|
561
|
+
export declare function instrumentedCompleteSimple<TApi extends Api>(model: Model<TApi>, ctx: Context, options: SimpleStreamOptions & {
|
|
562
|
+
onAttempt?: (message: AssistantMessage) => void;
|
|
563
|
+
}, span: InstrumentedChatSpanOptions): Promise<AssistantMessage>;
|
|
562
564
|
/**
|
|
563
565
|
* Start an `execute_tool` span representing one tool invocation. Parented
|
|
564
566
|
* under the supplied `invoke_agent` span by default — pass `parent` to
|
|
@@ -10,6 +10,7 @@ type TokenizerModel = Pick<Model, "tokenizer"> & snapcompact.ShapeTarget & {
|
|
|
10
10
|
compat?: Model["compat"];
|
|
11
11
|
thinking?: Model["thinking"];
|
|
12
12
|
reasoning?: boolean;
|
|
13
|
+
requestModelId?: string;
|
|
13
14
|
};
|
|
14
15
|
/** Identity of the reader whose replayed reasoning a {@link Tokenizer} counts; equal keys count it alike. */
|
|
15
16
|
export declare function reasoningReplayKey(model: TokenizerModel | null | undefined): string;
|
|
@@ -24,13 +25,12 @@ export interface TokenizerOptions {
|
|
|
24
25
|
nativeHistoryReplay?: boolean;
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
* when known, o200k_base otherwise
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* undercounts).
|
|
28
|
+
* Every mode counts with the native tokenizer: the catalog-resolved family
|
|
29
|
+
* when known, o200k_base otherwise. They differ only in the fallback when the
|
|
30
|
+
* loaded addon does not recognize that encoding: `strict` and `upperbound`
|
|
31
|
+
* the raw byte length (never undercounts), `approximate` a bytes/4 guess.
|
|
32
|
+
* Tests (`NODE_ENV=test`) skip the native count outside `strict` and use that
|
|
33
|
+
* fallback.
|
|
34
34
|
*/
|
|
35
35
|
export type TokenCountMode = "strict" | "approximate" | "upperbound";
|
|
36
36
|
/** Options for {@link Tokenizer.countMessage} / {@link Tokenizer.countMessages}. */
|
|
@@ -69,8 +69,8 @@ export interface TokenBudgetCheck {
|
|
|
69
69
|
* when the model's encoding, image or thinking-replay pricing, replay
|
|
70
70
|
* identity or native-history replay state changes); one-shot flows construct
|
|
71
71
|
* their own for the model that will be billed. Known tokenizer families use
|
|
72
|
-
* exact native counts;
|
|
73
|
-
*
|
|
72
|
+
* exact native counts; other models are counted in o200k_base, which bills
|
|
73
|
+
* unknown readers closer than a bytes/4 guess.
|
|
74
74
|
*/
|
|
75
75
|
export declare class Tokenizer {
|
|
76
76
|
#private;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -16,6 +16,21 @@ export interface QueuedMessagePreparation {
|
|
|
16
16
|
export type QueuedMessageQueue = "steering" | "followUp";
|
|
17
17
|
/** Prepare an exclusively claimed batch. Undefined delivers unchanged; the signal also aborts when the claim is cancelled. */
|
|
18
18
|
export type PrepareQueuedMessages = (messages: readonly AgentMessage[], signal: AbortSignal, queue: QueuedMessageQueue) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Synchronous arbiter over queued messages leaving a queue for delivery, for a
|
|
21
|
+
* queue whose messages someone else may withdraw concurrently (a session homed
|
|
22
|
+
* on another thread whose view pops them).
|
|
23
|
+
*/
|
|
24
|
+
export interface QueuedMessageGate {
|
|
25
|
+
/**
|
|
26
|
+
* Called with the batch about to be removed from `queue` for delivery; returns
|
|
27
|
+
* the candidates that must be dropped from the queue instead. The others are
|
|
28
|
+
* claimed for delivery.
|
|
29
|
+
*/
|
|
30
|
+
claim(queue: QueuedMessageQueue, candidates: readonly AgentMessage[]): readonly AgentMessage[];
|
|
31
|
+
/** Called with claimed messages that left delivery undelivered (requeued, withdrawn, or cancelled). */
|
|
32
|
+
release(messages: readonly AgentMessage[]): void;
|
|
33
|
+
}
|
|
19
34
|
/** Called once an aside has been inserted into the agent's live context. */
|
|
20
35
|
export declare const ASIDE_MESSAGE_COMMIT: unique symbol;
|
|
21
36
|
/** Symbol-keyed handoff for one finalized, tool-owned stream speculation session. */
|
|
@@ -218,6 +233,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
218
233
|
* never consumes the queue.
|
|
219
234
|
*/
|
|
220
235
|
getSteeringMessages?: (signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
236
|
+
/** Whether input is still owned by a queue and must be restored if a model call is aborted before it starts. */
|
|
237
|
+
isUndeliveredQueuedMessage?: (message: AgentMessage) => boolean;
|
|
221
238
|
/**
|
|
222
239
|
* Peeks whether steering messages are queued, without consuming them.
|
|
223
240
|
*
|
|
@@ -390,6 +407,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
390
407
|
* then strips from arguments before executing tools.
|
|
391
408
|
*/
|
|
392
409
|
intentTracing?: boolean;
|
|
410
|
+
/** Schema description of the injected intent field; `PI_INTENT_DESC` overrides it. Default `concise intent`. */
|
|
411
|
+
intentDescription?: string;
|
|
393
412
|
/**
|
|
394
413
|
* Strip tool descriptions (top-level + nested schema annotations) from the
|
|
395
414
|
* provider-bound tool specs. Use when the full catalog is rendered into the
|
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": "18.
|
|
4
|
+
"version": "18.17.0",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -31,25 +31,25 @@
|
|
|
31
31
|
"types": "./dist/types/index.d.ts",
|
|
32
32
|
"scripts": {
|
|
33
33
|
"check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
|
|
34
|
-
"check:types": "
|
|
34
|
+
"check:types": "bun check --no-script -p tsconfig.json",
|
|
35
35
|
"lint": "oxlint .",
|
|
36
36
|
"test": "bun test --parallel",
|
|
37
37
|
"fix": "oxlint --fix --fix-suggestions . && bun run fmt",
|
|
38
38
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@oh-my-pi/pi-ai": "18.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.17.0",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.17.0",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.17.0",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.17.0",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.17.0",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.17.0",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.
|
|
51
|
-
"@opentelemetry/context-async-hooks": "
|
|
52
|
-
"@opentelemetry/sdk-trace-base": "
|
|
50
|
+
"@oh-my-pi/omptype": "18.17.0",
|
|
51
|
+
"@opentelemetry/context-async-hooks": "2.9.0",
|
|
52
|
+
"@opentelemetry/sdk-trace-base": "2.9.0",
|
|
53
53
|
"@types/bun": "^1.3.14"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
signalListLabel,
|
|
54
54
|
} from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
55
55
|
import { isDsmlLeakRecoveryTarget, removeDsmlToolMarkupLeak } from "@oh-my-pi/pi-ai/utils/dsml-leak";
|
|
56
|
-
import { cloneJsonTree, logger, sanitizeText, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
56
|
+
import { cloneJsonTree, logger, sanitizeText, structuredCloneJSON, toError } from "@oh-my-pi/pi-utils";
|
|
57
57
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
58
58
|
import { LiveSteeringChannel } from "./live-steering";
|
|
59
59
|
import { agentPauseGate } from "./pause";
|
|
@@ -943,7 +943,7 @@ export function normalizeMessagesForProvider(
|
|
|
943
943
|
});
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
-
/** Schema description of the injected `i` field
|
|
946
|
+
/** Schema description of the injected `i` field when neither `PI_INTENT_DESC` nor the caller sets one. */
|
|
947
947
|
const DEFAULT_INTENT_FIELD_DESCRIPTION = "concise intent";
|
|
948
948
|
const INTENT_SCHEMA_UNION_KEYS = ["anyOf", "oneOf"] as const;
|
|
949
949
|
|
|
@@ -954,26 +954,30 @@ const INTENT_SCHEMA_UNION_KEYS = ["anyOf", "oneOf"] as const;
|
|
|
954
954
|
// the whole catalog per request). The injected object is shared across
|
|
955
955
|
// requests — the same profile as the intent-off path, where parameters IS the
|
|
956
956
|
// shared memoized wire schema (see schema-immutability.test.ts). One stamp
|
|
957
|
-
// key per (mode,
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
optional: [Symbol("intent:optional"), Symbol("intent:optional:described")],
|
|
961
|
-
} as const;
|
|
957
|
+
// key per (mode, description) variant, "" keying the bare one: agents in one
|
|
958
|
+
// process may describe `i` differently over the same shared tool schemas.
|
|
959
|
+
const INTENT_STAMPS = { require: new Map<string, symbol>(), optional: new Map<string, symbol>() } as const;
|
|
962
960
|
|
|
963
961
|
function memoizedInjectIntentIntoSchema(
|
|
964
962
|
schema: Record<string, unknown>,
|
|
965
963
|
mode: "require" | "optional",
|
|
966
|
-
|
|
964
|
+
description: string | undefined,
|
|
967
965
|
): unknown {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
);
|
|
966
|
+
const stamps = INTENT_STAMPS[mode];
|
|
967
|
+
const variant = description ?? "";
|
|
968
|
+
let key = stamps.get(variant);
|
|
969
|
+
if (!key) {
|
|
970
|
+
key = Symbol(`intent:${mode}`);
|
|
971
|
+
stamps.set(variant, key);
|
|
972
|
+
}
|
|
973
|
+
return stamp(schema, key, host => injectIntentIntoSchema(host, mode, description));
|
|
971
974
|
}
|
|
972
975
|
|
|
976
|
+
/** `schema` with the `i` field first (and required in `require` mode); `description` unset injects it bare. */
|
|
973
977
|
function injectIntentIntoSchema(
|
|
974
978
|
schema: unknown,
|
|
975
|
-
mode: "require" | "optional"
|
|
976
|
-
|
|
979
|
+
mode: "require" | "optional",
|
|
980
|
+
description: string | undefined,
|
|
977
981
|
): unknown {
|
|
978
982
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return schema;
|
|
979
983
|
const schemaRecord = schema as Record<string, unknown>;
|
|
@@ -996,7 +1000,7 @@ function injectIntentIntoSchema(
|
|
|
996
1000
|
if (!Array.isArray(variants)) continue;
|
|
997
1001
|
return {
|
|
998
1002
|
...schemaRecord,
|
|
999
|
-
[key]: variants.map(variant => injectIntentIntoSchema(variant, mode,
|
|
1003
|
+
[key]: variants.map(variant => injectIntentIntoSchema(variant, mode, description)),
|
|
1000
1004
|
};
|
|
1001
1005
|
}
|
|
1002
1006
|
}
|
|
@@ -1020,9 +1024,7 @@ function injectIntentIntoSchema(
|
|
|
1020
1024
|
return {
|
|
1021
1025
|
...schemaRecord,
|
|
1022
1026
|
properties: {
|
|
1023
|
-
[INTENT_FIELD]:
|
|
1024
|
-
? { type: "string", description: Bun.env.PI_INTENT_DESC || DEFAULT_INTENT_FIELD_DESCRIPTION }
|
|
1025
|
-
: { type: "string" },
|
|
1027
|
+
[INTENT_FIELD]: description === undefined ? { type: "string" } : { type: "string", description },
|
|
1026
1028
|
...properties,
|
|
1027
1029
|
},
|
|
1028
1030
|
...(mode === "require" ? { required: [...required, INTENT_FIELD] } : {}),
|
|
@@ -1032,6 +1034,8 @@ function injectIntentIntoSchema(
|
|
|
1032
1034
|
export interface NormalizeToolsOptions {
|
|
1033
1035
|
/** Inject the `i` intent field into tool schemas (subject to `PI_NO_INTENT`). */
|
|
1034
1036
|
injectIntent: boolean;
|
|
1037
|
+
/** Schema description of the injected `i` field; `PI_INTENT_DESC` overrides it. Default `concise intent`. */
|
|
1038
|
+
intentDescription?: string;
|
|
1035
1039
|
/** Strip descriptions from the wire specs when the catalog rides in the system prompt. */
|
|
1036
1040
|
pruneDescriptions?: boolean;
|
|
1037
1041
|
}
|
|
@@ -1039,6 +1043,7 @@ export interface NormalizeToolsOptions {
|
|
|
1039
1043
|
export function normalizeTools(tools: AgentContext["tools"], options: NormalizeToolsOptions): Context["tools"] {
|
|
1040
1044
|
const pruneDescriptions = options.pruneDescriptions === true;
|
|
1041
1045
|
const injectIntent = options.injectIntent && Bun.env.PI_NO_INTENT !== "1";
|
|
1046
|
+
const intentDescription = Bun.env.PI_INTENT_DESC || options.intentDescription || DEFAULT_INTENT_FIELD_DESCRIPTION;
|
|
1042
1047
|
return tools?.map(t => {
|
|
1043
1048
|
const intentMode = resolveIntentMode(t.intent);
|
|
1044
1049
|
const doInjectIntent =
|
|
@@ -1047,17 +1052,19 @@ export function normalizeTools(tools: AgentContext["tools"], options: NormalizeT
|
|
|
1047
1052
|
// specs without their descriptions (top-level + nested schema annotations)
|
|
1048
1053
|
// so they are not duplicated on the wire. Strip the STABLE wire schema (the
|
|
1049
1054
|
// memoized `stripSchemaDescriptions` result is reused across requests), then
|
|
1050
|
-
// re-inject `i` (without its hint, which
|
|
1055
|
+
// re-inject `i` (without its hint, which an unset description omits) so
|
|
1051
1056
|
// intent tracing keeps the field while no descriptions ride the wire.
|
|
1052
1057
|
if (pruneDescriptions) {
|
|
1053
1058
|
const stripped = stripSchemaDescriptions(toolWireSchema(t));
|
|
1054
1059
|
const parameters = (
|
|
1055
|
-
doInjectIntent ? memoizedInjectIntentIntoSchema(stripped, intentMode,
|
|
1060
|
+
doInjectIntent ? memoizedInjectIntentIntoSchema(stripped, intentMode, undefined) : stripped
|
|
1056
1061
|
) as TSchema;
|
|
1057
1062
|
return { ...t, parameters, description: "" };
|
|
1058
1063
|
}
|
|
1059
1064
|
const wire = toolWireSchema(t);
|
|
1060
|
-
const parameters = (
|
|
1065
|
+
const parameters = (
|
|
1066
|
+
doInjectIntent ? memoizedInjectIntentIntoSchema(wire, intentMode, intentDescription) : wire
|
|
1067
|
+
) as TSchema;
|
|
1061
1068
|
const description = t.description ?? "";
|
|
1062
1069
|
const examplesBlock = renderToolExamples({ ...t, parameters }, doInjectIntent ? INTENT_FIELD : undefined);
|
|
1063
1070
|
const finalDescription = examplesBlock ? `${description}\n\n${examplesBlock}` : description;
|
|
@@ -1188,7 +1195,7 @@ function resolveAsides(entries: AsideMessage[] | undefined): AgentMessage[] {
|
|
|
1188
1195
|
if (message) out.push(message);
|
|
1189
1196
|
}
|
|
1190
1197
|
} catch (error) {
|
|
1191
|
-
discardAsides(out,
|
|
1198
|
+
discardAsides(out, toError(error));
|
|
1192
1199
|
throw error;
|
|
1193
1200
|
}
|
|
1194
1201
|
return out;
|
|
@@ -1240,6 +1247,25 @@ async function runLoopBody(
|
|
|
1240
1247
|
// `liveAccepted` reached the model inside it, `liveDeferred` did not.
|
|
1241
1248
|
let liveAccepted: AgentMessage[] = [];
|
|
1242
1249
|
let liveDeferred: AgentMessage[] = [];
|
|
1250
|
+
const endAbortedBeforeModel = async (messages: AgentMessage[], turnOpen: boolean): Promise<boolean> => {
|
|
1251
|
+
if (!signal?.aborted || !config.isUndeliveredQueuedMessage) return false;
|
|
1252
|
+
const undelivered = new Set(messages.filter(message => config.isUndeliveredQueuedMessage?.(message)));
|
|
1253
|
+
for (const list of [currentContext.messages, newMessages]) {
|
|
1254
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
1255
|
+
if (undelivered.has(list[i])) list.splice(i, 1);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (!turnOpen) stream.push({ type: "turn_start" });
|
|
1259
|
+
emitInputMessages(
|
|
1260
|
+
stream,
|
|
1261
|
+
messages.filter(message => !undelivered.has(message)),
|
|
1262
|
+
);
|
|
1263
|
+
const aborted = emitAbortedAssistantMessage(null, false, new Set(), currentContext, config, stream, signal);
|
|
1264
|
+
newMessages.push(aborted);
|
|
1265
|
+
await emitTurnEnd(stream, currentContext, aborted, [], config, signal, { willContinue: false });
|
|
1266
|
+
endAgentStream(stream, newMessages, telemetry, stepCounter.count);
|
|
1267
|
+
return true;
|
|
1268
|
+
};
|
|
1243
1269
|
try {
|
|
1244
1270
|
let messagesToEmit = [...initialMessages];
|
|
1245
1271
|
if (isDeadlineExceeded(config.deadline)) {
|
|
@@ -1253,6 +1279,7 @@ async function runLoopBody(
|
|
|
1253
1279
|
try {
|
|
1254
1280
|
pendingMessages = signal?.aborted ? [] : (await config.getSteeringMessages?.(signal)) || [];
|
|
1255
1281
|
} catch (error) {
|
|
1282
|
+
if (signal?.aborted && (await endAbortedBeforeModel(messagesToEmit, false))) return;
|
|
1256
1283
|
stream.push({ type: "turn_start" });
|
|
1257
1284
|
emitInputMessages(stream, messagesToEmit);
|
|
1258
1285
|
throw error;
|
|
@@ -1351,10 +1378,19 @@ async function runLoopBody(
|
|
|
1351
1378
|
|
|
1352
1379
|
let preparedProviderCall: PreparedProviderCall;
|
|
1353
1380
|
let gateResult: AgentPreModelCallResult;
|
|
1381
|
+
const emitTurnInputs = () => {
|
|
1382
|
+
emitInputMessages(
|
|
1383
|
+
stream,
|
|
1384
|
+
signal?.aborted
|
|
1385
|
+
? turnMessages.filter(message => !config.isUndeliveredQueuedMessage?.(message))
|
|
1386
|
+
: turnMessages,
|
|
1387
|
+
);
|
|
1388
|
+
};
|
|
1354
1389
|
try {
|
|
1355
1390
|
if (config.syncContextBeforeModelCall) {
|
|
1356
1391
|
await config.syncContextBeforeModelCall(currentContext, signal);
|
|
1357
1392
|
}
|
|
1393
|
+
if (signal?.aborted && (await endAbortedBeforeModel(turnMessages, turnOpen))) return;
|
|
1358
1394
|
|
|
1359
1395
|
if (!directiveResolvedForTurn) {
|
|
1360
1396
|
const directive = signal?.aborted ? undefined : config.getToolChoice?.();
|
|
@@ -1383,16 +1419,19 @@ async function runLoopBody(
|
|
|
1383
1419
|
}
|
|
1384
1420
|
|
|
1385
1421
|
preparedProviderCall = await prepareProviderCall(currentContext, config, signal);
|
|
1422
|
+
if (signal?.aborted && (await endAbortedBeforeModel(turnMessages, turnOpen))) return;
|
|
1386
1423
|
preparedProviderCall.liveSteering = openLiveSteering(config, signal, preparedProviderCall);
|
|
1387
1424
|
gateResult = (await config.beforeModelCall?.(preparedProviderCall.context, signal)) || undefined;
|
|
1388
1425
|
} catch (error) {
|
|
1426
|
+
if (signal?.aborted && (await endAbortedBeforeModel(turnMessages, turnOpen))) return;
|
|
1389
1427
|
if (!turnOpen) {
|
|
1390
1428
|
stream.push({ type: "turn_start" });
|
|
1391
|
-
|
|
1429
|
+
emitTurnInputs();
|
|
1392
1430
|
turnOpen = true;
|
|
1393
1431
|
}
|
|
1394
1432
|
throw error;
|
|
1395
1433
|
}
|
|
1434
|
+
if (signal?.aborted && (await endAbortedBeforeModel(turnMessages, turnOpen))) return;
|
|
1396
1435
|
if (config.beforeModelCall && signal?.aborted) {
|
|
1397
1436
|
gateResult = { stop: true };
|
|
1398
1437
|
}
|
|
@@ -1436,13 +1475,13 @@ async function runLoopBody(
|
|
|
1436
1475
|
|
|
1437
1476
|
if (!turnOpen) {
|
|
1438
1477
|
stream.push({ type: "turn_start" });
|
|
1439
|
-
emitInputMessages(stream, turnMessages);
|
|
1440
1478
|
turnOpen = true;
|
|
1441
1479
|
}
|
|
1442
1480
|
|
|
1443
1481
|
// Stream assistant response
|
|
1444
1482
|
let recovered: HarmonyRecoveredToolCall | undefined;
|
|
1445
1483
|
let message: AssistantMessage;
|
|
1484
|
+
let inputsEmitted = false;
|
|
1446
1485
|
try {
|
|
1447
1486
|
if (agentPauseGate.paused) await waitForProcessPause(config, signal);
|
|
1448
1487
|
message = await streamAssistantResponse(
|
|
@@ -1458,6 +1497,11 @@ async function runLoopBody(
|
|
|
1458
1497
|
hostToolChoice,
|
|
1459
1498
|
softRequirementState.forcedToolChoice,
|
|
1460
1499
|
preparedProviderCall,
|
|
1500
|
+
() => {
|
|
1501
|
+
if (config.isUndeliveredQueuedMessage) signal?.throwIfAborted();
|
|
1502
|
+
emitTurnInputs();
|
|
1503
|
+
inputsEmitted = true;
|
|
1504
|
+
},
|
|
1461
1505
|
message => {
|
|
1462
1506
|
const finalToolCalls = message.content.filter(
|
|
1463
1507
|
(content): content is Extract<AssistantMessage["content"][number], { type: "toolCall" }> =>
|
|
@@ -1484,6 +1528,10 @@ async function runLoopBody(
|
|
|
1484
1528
|
harmonyRetryAttempt = 0;
|
|
1485
1529
|
harmonyTruncateResumeCount = 0;
|
|
1486
1530
|
} catch (err) {
|
|
1531
|
+
if (!inputsEmitted) {
|
|
1532
|
+
if (signal?.aborted && (await endAbortedBeforeModel(turnMessages, turnOpen))) return;
|
|
1533
|
+
emitTurnInputs();
|
|
1534
|
+
}
|
|
1487
1535
|
if (!(err instanceof HarmonyLeakInterruption)) throw err;
|
|
1488
1536
|
if (err.recovered) {
|
|
1489
1537
|
if (harmonyTruncateResumeCount >= 2) {
|
|
@@ -1958,6 +2006,7 @@ async function prepareProviderCall(
|
|
|
1958
2006
|
config.appendOnlyContext.syncMessages(normalizedMessages);
|
|
1959
2007
|
llmContext = config.appendOnlyContext.build(context, {
|
|
1960
2008
|
intentTracing: !!config.intentTracing,
|
|
2009
|
+
intentDescription: config.intentDescription,
|
|
1961
2010
|
pruneToolDescriptions,
|
|
1962
2011
|
});
|
|
1963
2012
|
} else {
|
|
@@ -1966,6 +2015,7 @@ async function prepareProviderCall(
|
|
|
1966
2015
|
messages: normalizedMessages,
|
|
1967
2016
|
tools: normalizeTools(context.tools, {
|
|
1968
2017
|
injectIntent: !!config.intentTracing,
|
|
2018
|
+
intentDescription: config.intentDescription,
|
|
1969
2019
|
pruneDescriptions: pruneToolDescriptions,
|
|
1970
2020
|
}),
|
|
1971
2021
|
};
|
|
@@ -2012,6 +2062,7 @@ async function streamAssistantResponse(
|
|
|
2012
2062
|
hostToolChoice?: ToolChoice,
|
|
2013
2063
|
forcedToolChoice?: ToolChoice,
|
|
2014
2064
|
prepared?: PreparedProviderCall,
|
|
2065
|
+
onProviderStart?: () => void,
|
|
2015
2066
|
canDispatchFinalToolCalls?: (message: AssistantMessage) => boolean,
|
|
2016
2067
|
): Promise<AssistantMessage> {
|
|
2017
2068
|
const providerCall = prepared ?? (await prepareProviderCall(context, config, signal));
|
|
@@ -2156,6 +2207,7 @@ async function streamAssistantResponse(
|
|
|
2156
2207
|
|
|
2157
2208
|
try {
|
|
2158
2209
|
return await runInActiveSpan(chatSpan, async () => {
|
|
2210
|
+
onProviderStart?.();
|
|
2159
2211
|
let response = await streamFunction(model, llmContext, {
|
|
2160
2212
|
...config,
|
|
2161
2213
|
apiKey,
|