@oh-my-pi/pi-agent-core 18.16.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 +7 -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/telemetry.d.ts +3 -1
- package/dist/types/types.d.ts +19 -0
- package/package.json +11 -11
- package/src/agent-loop.ts +73 -21
- package/src/agent.ts +83 -10
- package/src/append-only-context.ts +49 -18
- package/src/compaction/compaction-v2-streaming.ts +0 -1
- package/src/compaction/openai.ts +0 -1
- package/src/telemetry.ts +1 -1
- package/src/types.ts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
|
|
5
12
|
## [18.16.0] - 2026-11-02
|
|
6
13
|
|
|
7
14
|
### Changed
|
|
@@ -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;
|
|
@@ -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
|
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
|
@@ -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;
|
|
@@ -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,
|
package/src/agent.ts
CHANGED
|
@@ -62,6 +62,7 @@ import type {
|
|
|
62
62
|
AsideBoundary,
|
|
63
63
|
AsideMessage,
|
|
64
64
|
PrepareQueuedMessages,
|
|
65
|
+
QueuedMessageGate,
|
|
65
66
|
QueuedMessageQueue,
|
|
66
67
|
SpeculativeToolExecutionConfig,
|
|
67
68
|
StreamFn,
|
|
@@ -279,6 +280,8 @@ export interface AgentOptions {
|
|
|
279
280
|
|
|
280
281
|
/** Enable intent tracing schema injection/stripping in the harness. */
|
|
281
282
|
intentTracing?: boolean;
|
|
283
|
+
/** Schema description of the injected intent field; `PI_INTENT_DESC` overrides it. Default `concise intent`. */
|
|
284
|
+
intentDescription?: string;
|
|
282
285
|
/**
|
|
283
286
|
* Strip tool descriptions from provider-bound tool specs (top-level + nested
|
|
284
287
|
* schema annotations). Use when the full catalog is rendered into the system
|
|
@@ -493,6 +496,7 @@ export class Agent {
|
|
|
493
496
|
#resolveFallbackTool?: (name: string, advertised: readonly AgentTool<any>[]) => AgentTool<any> | undefined;
|
|
494
497
|
#suggestFallbackToolNames?: () => Iterable<string>;
|
|
495
498
|
#intentTracing: boolean;
|
|
499
|
+
#intentDescription?: string;
|
|
496
500
|
#pruneToolDescriptions: boolean;
|
|
497
501
|
#dialect?: Dialect;
|
|
498
502
|
#dialectResolver?: (model: Model) => Dialect | undefined;
|
|
@@ -525,6 +529,12 @@ export class Agent {
|
|
|
525
529
|
getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
|
|
526
530
|
/** Prepare actual queue deliveries after dequeue gates; commit runs only while ownership remains valid. */
|
|
527
531
|
prepareQueuedMessages?: PrepareQueuedMessages;
|
|
532
|
+
/**
|
|
533
|
+
* Consulted synchronously whenever queued messages are removed for delivery and
|
|
534
|
+
* told about claimed messages that went back undelivered (see {@link QueuedMessageGate}).
|
|
535
|
+
* Unset, batches are taken as they are.
|
|
536
|
+
*/
|
|
537
|
+
queuedMessageGate?: QueuedMessageGate;
|
|
528
538
|
/**
|
|
529
539
|
* Hook invoked after tool arguments are validated and before execution.
|
|
530
540
|
* Reassign at any time to swap the implementation (e.g. on extension reload).
|
|
@@ -602,6 +612,7 @@ export class Agent {
|
|
|
602
612
|
this.#resolveFallbackTool = opts.resolveFallbackTool;
|
|
603
613
|
this.#suggestFallbackToolNames = opts.suggestFallbackToolNames;
|
|
604
614
|
this.#intentTracing = opts.intentTracing === true;
|
|
615
|
+
this.#intentDescription = opts.intentDescription;
|
|
605
616
|
this.#pruneToolDescriptions = opts.pruneToolDescriptions === true;
|
|
606
617
|
this.#dialect = opts.dialect;
|
|
607
618
|
this.#dialectResolver = opts.dialectResolver;
|
|
@@ -864,6 +875,11 @@ export class Agent {
|
|
|
864
875
|
this.#intentTracing = value;
|
|
865
876
|
}
|
|
866
877
|
|
|
878
|
+
/** The intent field's schema description this agent was built with (`PI_INTENT_DESC` still overrides it). */
|
|
879
|
+
get intentDescription(): string | undefined {
|
|
880
|
+
return this.#intentDescription;
|
|
881
|
+
}
|
|
882
|
+
|
|
867
883
|
/** Abort the provider request on a fabricated tool result; applies from the next prompt run. */
|
|
868
884
|
get abortOnFabricatedToolResult(): boolean | undefined {
|
|
869
885
|
return this.#abortOnFabricatedToolResult;
|
|
@@ -970,6 +986,7 @@ export class Agent {
|
|
|
970
986
|
? []
|
|
971
987
|
: (normalizeTools(this.#toolsForModel(model), {
|
|
972
988
|
injectIntent: this.#intentTracing,
|
|
989
|
+
intentDescription: this.#intentDescription,
|
|
973
990
|
pruneDescriptions: this.#pruneToolDescriptions,
|
|
974
991
|
}) ?? []);
|
|
975
992
|
let context: Context = { systemPrompt, messages, tools };
|
|
@@ -1042,12 +1059,14 @@ export class Agent {
|
|
|
1042
1059
|
const runController = this.#abortController;
|
|
1043
1060
|
if (!prepare) {
|
|
1044
1061
|
this.#queuedMessageDeliveries.add({ queue, controller: runController, messages, next: 0, additional: [] });
|
|
1062
|
+
this.#emitQueueChanged();
|
|
1045
1063
|
return messages;
|
|
1046
1064
|
}
|
|
1047
1065
|
|
|
1048
1066
|
const claim: QueuedMessageClaim = { messages, controller: new AbortController() };
|
|
1049
1067
|
this.#queuedMessageClaims[queue] = claim;
|
|
1050
1068
|
const preparationSignal = AbortSignal.any([signal, claim.controller.signal]);
|
|
1069
|
+
this.#emitQueueChanged();
|
|
1051
1070
|
try {
|
|
1052
1071
|
const preparation = await prepare(messages, preparationSignal, queue);
|
|
1053
1072
|
signal.throwIfAborted();
|
|
@@ -1067,6 +1086,7 @@ export class Agent {
|
|
|
1067
1086
|
next: 0,
|
|
1068
1087
|
additional: additional ?? [],
|
|
1069
1088
|
});
|
|
1089
|
+
this.#emitQueueChanged();
|
|
1070
1090
|
return additional?.length ? [...messages, ...additional] : messages;
|
|
1071
1091
|
} catch (error) {
|
|
1072
1092
|
if (signal.aborted) throw error;
|
|
@@ -1086,6 +1106,7 @@ export class Agent {
|
|
|
1086
1106
|
const claim = this.#queuedMessageClaims[queue];
|
|
1087
1107
|
if (!claim) return;
|
|
1088
1108
|
delete this.#queuedMessageClaims[queue];
|
|
1109
|
+
this.queuedMessageGate?.release(claim.messages);
|
|
1089
1110
|
if (restore) {
|
|
1090
1111
|
if (queue === "steering") {
|
|
1091
1112
|
this.#steeringQueue = [...claim.messages, ...this.#steeringQueue];
|
|
@@ -1104,6 +1125,7 @@ export class Agent {
|
|
|
1104
1125
|
// Live steering was taken before anything the run still holds undelivered.
|
|
1105
1126
|
this.#liveSteered = this.#liveSteered.filter(entry => {
|
|
1106
1127
|
if (entry.controller !== controller) return true;
|
|
1128
|
+
if (entry.message.role === "user") delete entry.message.liveSteered;
|
|
1107
1129
|
restored.steering.push(entry.message);
|
|
1108
1130
|
return false;
|
|
1109
1131
|
});
|
|
@@ -1114,12 +1136,14 @@ export class Agent {
|
|
|
1114
1136
|
restored[delivery.queue].push(delivery.messages[i]);
|
|
1115
1137
|
}
|
|
1116
1138
|
}
|
|
1139
|
+
if (restored.steering.length === 0 && restored.followUp.length === 0) return;
|
|
1140
|
+
this.queuedMessageGate?.release([...restored.steering, ...restored.followUp]);
|
|
1117
1141
|
if (restored.steering.length > 0) {
|
|
1118
1142
|
this.#steeringQueue = [...restored.steering, ...this.#steeringQueue];
|
|
1119
1143
|
this.#notifySteeringWaiters();
|
|
1120
1144
|
}
|
|
1121
1145
|
if (restored.followUp.length > 0) this.#followUpQueue = [...restored.followUp, ...this.#followUpQueue];
|
|
1122
|
-
|
|
1146
|
+
this.#emitQueueChanged();
|
|
1123
1147
|
}
|
|
1124
1148
|
|
|
1125
1149
|
/** Move steering live steering took out of the queue-delivery records into {@link #liveSteered}. */
|
|
@@ -1177,6 +1201,9 @@ export class Agent {
|
|
|
1177
1201
|
for (const message of [...pending, ...delivery.additional]) suppress(message, delivery.controller);
|
|
1178
1202
|
}
|
|
1179
1203
|
this.#queuedMessageDeliveries.clear();
|
|
1204
|
+
if (withdrawn.steering.length > 0 || withdrawn.followUp.length > 0) {
|
|
1205
|
+
this.queuedMessageGate?.release([...withdrawn.steering, ...withdrawn.followUp]);
|
|
1206
|
+
}
|
|
1180
1207
|
if (hadLiveSteered) this.#emitQueueChanged();
|
|
1181
1208
|
return withdrawn;
|
|
1182
1209
|
}
|
|
@@ -1336,11 +1363,9 @@ export class Agent {
|
|
|
1336
1363
|
this.#state.messages = ms.slice();
|
|
1337
1364
|
}
|
|
1338
1365
|
|
|
1339
|
-
/** Signal
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
* enqueue, dequeue-on-delivery, clear, and restore without emits scattered
|
|
1343
|
-
* across every call site that queues or dequeues a message. */
|
|
1366
|
+
/** Signal after a queue transition has settled its pending, claim, and
|
|
1367
|
+
* delivery state, so AgentSession's `queue_update` coalescing observes
|
|
1368
|
+
* a consistent snapshot across enqueue, preparation, delivery, and restore. */
|
|
1344
1369
|
#emitQueueChanged(): void {
|
|
1345
1370
|
for (const listener of this.#queueListeners) {
|
|
1346
1371
|
try {
|
|
@@ -1507,6 +1532,19 @@ export class Agent {
|
|
|
1507
1532
|
return claim ? [...claim.messages, ...this.#steeringQueue] : this.#steeringQueue;
|
|
1508
1533
|
}
|
|
1509
1534
|
|
|
1535
|
+
#isUndeliveredQueuedMessage(message: AgentMessage): boolean {
|
|
1536
|
+
for (const entry of this.#liveSteered) {
|
|
1537
|
+
if (entry.message === message || entry.additional?.includes(message)) return true;
|
|
1538
|
+
}
|
|
1539
|
+
for (const delivery of this.#queuedMessageDeliveries) {
|
|
1540
|
+
if (delivery.additional.includes(message)) return true;
|
|
1541
|
+
for (let i = delivery.next; i < delivery.messages.length; i++) {
|
|
1542
|
+
if (delivery.messages[i] === message) return true;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1510
1548
|
/** Dequeued messages not yet in the transcript, e.g. steering a provider
|
|
1511
1549
|
* took into its in-flight response via live steering. Aborting the run
|
|
1512
1550
|
* requeues them, so the session's empty-submit interrupt counts them as
|
|
@@ -1553,17 +1591,50 @@ export class Agent {
|
|
|
1553
1591
|
return queue.slice(0, count);
|
|
1554
1592
|
}
|
|
1555
1593
|
|
|
1594
|
+
/**
|
|
1595
|
+
* The next delivery batch of `queue`. With a {@link queuedMessageGate}, the
|
|
1596
|
+
* gate sees each candidate batch first; what it vetoes leaves the queue and
|
|
1597
|
+
* the batch is recomputed, and candidates it claimed that the final batch no
|
|
1598
|
+
* longer holds are released again.
|
|
1599
|
+
*/
|
|
1600
|
+
#nextDeliveryBatch(queue: QueuedMessageQueue): AgentMessage[] {
|
|
1601
|
+
const mode = queue === "steering" ? this.#steeringMode : this.#followUpMode;
|
|
1602
|
+
let batch = this.#dequeueMessages(queue === "steering" ? this.#steeringQueue : this.#followUpQueue, mode);
|
|
1603
|
+
const gate = this.queuedMessageGate;
|
|
1604
|
+
if (!gate || batch.length === 0) return batch;
|
|
1605
|
+
let claimedEarlier: AgentMessage[] | undefined;
|
|
1606
|
+
let dropped = false;
|
|
1607
|
+
while (batch.length > 0) {
|
|
1608
|
+
const veto = gate.claim(queue, batch);
|
|
1609
|
+
if (veto.length === 0) break;
|
|
1610
|
+
const drop = new Set(veto);
|
|
1611
|
+
const pending = queue === "steering" ? this.#steeringQueue : this.#followUpQueue;
|
|
1612
|
+
const kept = pending.filter(message => !drop.has(message));
|
|
1613
|
+
if (kept.length === pending.length) break;
|
|
1614
|
+
for (const message of batch) if (!drop.has(message)) (claimedEarlier ??= []).push(message);
|
|
1615
|
+
if (queue === "steering") this.#steeringQueue = kept;
|
|
1616
|
+
else this.#followUpQueue = kept;
|
|
1617
|
+
dropped = true;
|
|
1618
|
+
batch = this.#dequeueMessages(kept, mode);
|
|
1619
|
+
}
|
|
1620
|
+
if (claimedEarlier) {
|
|
1621
|
+
const final = new Set(batch);
|
|
1622
|
+
const stale = claimedEarlier.filter(message => !final.has(message));
|
|
1623
|
+
if (stale.length > 0) gate.release(stale);
|
|
1624
|
+
}
|
|
1625
|
+
if (dropped && batch.length === 0) this.#emitQueueChanged();
|
|
1626
|
+
return batch;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1556
1629
|
#dequeueSteeringMessages(): AgentMessage[] {
|
|
1557
|
-
const messages = this.#
|
|
1630
|
+
const messages = this.#nextDeliveryBatch("steering");
|
|
1558
1631
|
this.#steeringQueue = this.#steeringQueue.slice(messages.length);
|
|
1559
|
-
if (messages.length > 0) this.#emitQueueChanged();
|
|
1560
1632
|
return messages;
|
|
1561
1633
|
}
|
|
1562
1634
|
|
|
1563
1635
|
#dequeueFollowUpMessages(): AgentMessage[] {
|
|
1564
|
-
const messages = this.#
|
|
1636
|
+
const messages = this.#nextDeliveryBatch("followUp");
|
|
1565
1637
|
this.#followUpQueue = this.#followUpQueue.slice(messages.length);
|
|
1566
|
-
if (messages.length > 0) this.#emitQueueChanged();
|
|
1567
1638
|
return messages;
|
|
1568
1639
|
}
|
|
1569
1640
|
|
|
@@ -1991,6 +2062,7 @@ export class Agent {
|
|
|
1991
2062
|
resolveFallbackTool: this.#resolveFallbackTool,
|
|
1992
2063
|
suggestFallbackToolNames: this.#suggestFallbackToolNames,
|
|
1993
2064
|
intentTracing: this.#intentTracing,
|
|
2065
|
+
intentDescription: this.#intentDescription,
|
|
1994
2066
|
pruneToolDescriptions: this.#pruneToolDescriptions,
|
|
1995
2067
|
dialect: this.#dialect,
|
|
1996
2068
|
getDialect: this.#dialectResolver,
|
|
@@ -2024,6 +2096,7 @@ export class Agent {
|
|
|
2024
2096
|
}
|
|
2025
2097
|
return this.#dequeueSteeringMessagesAfterHooks(signal ?? loopSignal);
|
|
2026
2098
|
},
|
|
2099
|
+
isUndeliveredQueuedMessage: message => this.#isUndeliveredQueuedMessage(message),
|
|
2027
2100
|
hasSteeringMessages: () =>
|
|
2028
2101
|
steeringQueueState(
|
|
2029
2102
|
this.#steeringQueue,
|
|
@@ -82,6 +82,8 @@ export interface StablePrefixSnapshot {
|
|
|
82
82
|
export interface BuildOptions {
|
|
83
83
|
/** Inject the `i` intent field into tool schemas (must match agent-loop's normalizeTools). */
|
|
84
84
|
intentTracing: boolean;
|
|
85
|
+
/** Schema description of the injected `i` field (must match agent-loop's normalizeTools). */
|
|
86
|
+
intentDescription?: string;
|
|
85
87
|
/** Strip tool descriptions from the provider-bound specs (must match normalizeTools). */
|
|
86
88
|
pruneToolDescriptions?: boolean;
|
|
87
89
|
}
|
|
@@ -136,6 +138,7 @@ export class StablePrefix {
|
|
|
136
138
|
// but changes any of these must miss the fast path.
|
|
137
139
|
#lastToolKey: readonly string[] | undefined;
|
|
138
140
|
#lastIntentTracing: boolean | undefined;
|
|
141
|
+
#lastIntentDescription: string | undefined;
|
|
139
142
|
#lastPruneToolDescriptions: boolean | undefined;
|
|
140
143
|
|
|
141
144
|
build(context: AgentContext, options: BuildOptions): boolean {
|
|
@@ -166,6 +169,7 @@ export class StablePrefix {
|
|
|
166
169
|
#fastPathHit(context: AgentContext, options: BuildOptions, toolKeys: readonly string[]): boolean {
|
|
167
170
|
if (
|
|
168
171
|
this.#lastIntentTracing !== options.intentTracing ||
|
|
172
|
+
this.#lastIntentDescription !== options.intentDescription ||
|
|
169
173
|
this.#lastPruneToolDescriptions !== options.pruneToolDescriptions
|
|
170
174
|
) {
|
|
171
175
|
return false;
|
|
@@ -196,6 +200,7 @@ export class StablePrefix {
|
|
|
196
200
|
#recordFastPathKey(context: AgentContext, options: BuildOptions, renderedToolKeys: readonly string[]): void {
|
|
197
201
|
this.#lastPromptSegments = [...context.systemPrompt];
|
|
198
202
|
this.#lastIntentTracing = options.intentTracing;
|
|
203
|
+
this.#lastIntentDescription = options.intentDescription;
|
|
199
204
|
this.#lastPruneToolDescriptions = options.pruneToolDescriptions;
|
|
200
205
|
this.#lastToolKey = renderedToolKeys;
|
|
201
206
|
}
|
|
@@ -470,11 +475,7 @@ export class AppendOnlyContextManager {
|
|
|
470
475
|
|
|
471
476
|
function takeSnapshot(context: AgentContext, options: BuildOptions): StablePrefixSnapshot {
|
|
472
477
|
const systemPrompt = [...context.systemPrompt];
|
|
473
|
-
const tools =
|
|
474
|
-
normalizeTools(context.tools, {
|
|
475
|
-
injectIntent: options.intentTracing,
|
|
476
|
-
pruneDescriptions: options.pruneToolDescriptions,
|
|
477
|
-
}) ?? [];
|
|
478
|
+
const tools = normalizeWireTools(context.tools, options);
|
|
478
479
|
return {
|
|
479
480
|
systemPrompt,
|
|
480
481
|
tools,
|
|
@@ -482,20 +483,50 @@ function takeSnapshot(context: AgentContext, options: BuildOptions): StablePrefi
|
|
|
482
483
|
};
|
|
483
484
|
}
|
|
484
485
|
|
|
486
|
+
function normalizeWireTools(tools: AgentContext["tools"], options: BuildOptions): Tool[] {
|
|
487
|
+
return (
|
|
488
|
+
normalizeTools(tools, {
|
|
489
|
+
injectIntent: options.intentTracing,
|
|
490
|
+
intentDescription: options.intentDescription,
|
|
491
|
+
pruneDescriptions: options.pruneToolDescriptions,
|
|
492
|
+
}) ?? []
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The fields of a provider-bound tool spec the wire carries; `normalizeTools` has rendered `examples` into the description. */
|
|
497
|
+
function wireToolFields(t: Tool) {
|
|
498
|
+
return {
|
|
499
|
+
n: t.name,
|
|
500
|
+
d: t.description,
|
|
501
|
+
p: t.parameters,
|
|
502
|
+
s: t.strict,
|
|
503
|
+
cf: t.customFormat,
|
|
504
|
+
cw: t.customWireName,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Fingerprint of the tool specs a request built with `options` sends: the
|
|
510
|
+
* tools half of the prefix fingerprint, for a caller that compares tool sets
|
|
511
|
+
* across requests. A same-name tool whose description, schema, examples,
|
|
512
|
+
* `strict`, `customFormat` or `customWireName` changed makes another one.
|
|
513
|
+
*/
|
|
514
|
+
export function wireToolsFingerprint(tools: AgentContext["tools"], options: BuildOptions): string {
|
|
515
|
+
return hashPayload(JSON.stringify(normalizeWireTools(tools, options).map(wireToolFields)));
|
|
516
|
+
}
|
|
517
|
+
|
|
485
518
|
function computeFingerprint(systemPrompt: string[], tools: Tool[], options: BuildOptions): string {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
pd: options.pruneToolDescriptions,
|
|
498
|
-
});
|
|
519
|
+
return hashPayload(
|
|
520
|
+
JSON.stringify({
|
|
521
|
+
s: systemPrompt,
|
|
522
|
+
t: tools.map(wireToolFields),
|
|
523
|
+
i: options.intentTracing,
|
|
524
|
+
pd: options.pruneToolDescriptions,
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function hashPayload(payload: string): string {
|
|
499
530
|
let hash = 0;
|
|
500
531
|
for (let i = 0; i < payload.length; i++) {
|
|
501
532
|
hash = ((hash << 5) - hash + payload.charCodeAt(i)) | 0;
|
|
@@ -472,7 +472,6 @@ function buildCompactionV2Headers(
|
|
|
472
472
|
headers[OPENAI_HEADERS.SESSION_ID] = routingSessionId;
|
|
473
473
|
headers["x-client-request-id"] = routingSessionId;
|
|
474
474
|
}
|
|
475
|
-
headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
|
|
476
475
|
headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
|
|
477
476
|
headers[OPENAI_HEADERS.CODEX_BETA_FEATURES] = OPENAI_HEADER_VALUES.REMOTE_COMPACTION_V2;
|
|
478
477
|
headers[OPENAI_HEADERS.ROUTING_HINT] = codexRoutingHint(request.body.model, undefined);
|
package/src/compaction/openai.ts
CHANGED
|
@@ -965,7 +965,6 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
965
965
|
if (attestation) {
|
|
966
966
|
headers[OPENAI_HEADERS.ATTESTATION] = attestation;
|
|
967
967
|
}
|
|
968
|
-
headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
|
|
969
968
|
headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
|
|
970
969
|
// This compaction request sends no `service_tier`, so the hint is model-only.
|
|
971
970
|
headers[OPENAI_HEADERS.ROUTING_HINT] = codexRoutingHint(request.model, undefined);
|
package/src/telemetry.ts
CHANGED
|
@@ -1836,7 +1836,7 @@ export interface InstrumentedChatSpanOptions {
|
|
|
1836
1836
|
export async function instrumentedCompleteSimple<TApi extends Api>(
|
|
1837
1837
|
model: Model<TApi>,
|
|
1838
1838
|
ctx: Context,
|
|
1839
|
-
options: SimpleStreamOptions,
|
|
1839
|
+
options: SimpleStreamOptions & { onAttempt?: (message: AssistantMessage) => void },
|
|
1840
1840
|
span: InstrumentedChatSpanOptions,
|
|
1841
1841
|
): Promise<AssistantMessage> {
|
|
1842
1842
|
const { telemetry, parent, oneshotKind } = span;
|
package/src/types.ts
CHANGED
|
@@ -48,6 +48,22 @@ export type PrepareQueuedMessages = (
|
|
|
48
48
|
queue: QueuedMessageQueue,
|
|
49
49
|
) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Synchronous arbiter over queued messages leaving a queue for delivery, for a
|
|
53
|
+
* queue whose messages someone else may withdraw concurrently (a session homed
|
|
54
|
+
* on another thread whose view pops them).
|
|
55
|
+
*/
|
|
56
|
+
export interface QueuedMessageGate {
|
|
57
|
+
/**
|
|
58
|
+
* Called with the batch about to be removed from `queue` for delivery; returns
|
|
59
|
+
* the candidates that must be dropped from the queue instead. The others are
|
|
60
|
+
* claimed for delivery.
|
|
61
|
+
*/
|
|
62
|
+
claim(queue: QueuedMessageQueue, candidates: readonly AgentMessage[]): readonly AgentMessage[];
|
|
63
|
+
/** Called with claimed messages that left delivery undelivered (requeued, withdrawn, or cancelled). */
|
|
64
|
+
release(messages: readonly AgentMessage[]): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
/** Called once an aside has been inserted into the agent's live context. */
|
|
52
68
|
export const ASIDE_MESSAGE_COMMIT = Symbol("aside-message-commit");
|
|
53
69
|
/** Symbol-keyed handoff for one finalized, tool-owned stream speculation session. */
|
|
@@ -278,6 +294,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
278
294
|
*/
|
|
279
295
|
getSteeringMessages?: (signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
280
296
|
|
|
297
|
+
/** Whether input is still owned by a queue and must be restored if a model call is aborted before it starts. */
|
|
298
|
+
isUndeliveredQueuedMessage?: (message: AgentMessage) => boolean;
|
|
299
|
+
|
|
281
300
|
/**
|
|
282
301
|
* Peeks whether steering messages are queued, without consuming them.
|
|
283
302
|
*
|
|
@@ -457,6 +476,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
457
476
|
* then strips from arguments before executing tools.
|
|
458
477
|
*/
|
|
459
478
|
intentTracing?: boolean;
|
|
479
|
+
/** Schema description of the injected intent field; `PI_INTENT_DESC` overrides it. Default `concise intent`. */
|
|
480
|
+
intentDescription?: string;
|
|
460
481
|
/**
|
|
461
482
|
* Strip tool descriptions (top-level + nested schema annotations) from the
|
|
462
483
|
* provider-bound tool specs. Use when the full catalog is rendered into the
|