@gajae-code/agent-core 0.15.4 → 0.15.6
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 +6 -1
- package/dist/types/agent.d.ts +15 -3
- package/dist/types/types.d.ts +15 -1
- package/package.json +4 -4
- package/src/agent-loop.ts +380 -74
- package/src/agent.ts +54 -9
- package/src/types.ts +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.15.6] - 2026-08-30
|
|
6
|
+
|
|
7
|
+
## [0.15.5] - 2026-08-29
|
|
8
|
+
|
|
5
9
|
## [0.15.4] - 2026-08-29
|
|
6
10
|
|
|
7
11
|
### Added
|
|
@@ -12,9 +16,10 @@
|
|
|
12
16
|
|
|
13
17
|
### Changed
|
|
14
18
|
|
|
15
|
-
-
|
|
19
|
+
- Current provider adapters no longer mark syntactically valid `\uXXXX` arguments as guarded: JSON parsing has already produced the canonical string, so valid escaped Hangul, emoji, ASCII, and other scalars execute like literal UTF-8 in every tool. The existing `displaySafeEscapedArgFields` path remains only for compatibility with legacy producers that still attach non-malformed positional evidence. Malformed evidence, unauthenticated managed evidence, incomplete/conflicting arguments, and unpaired surrogates remain fail-closed.
|
|
16
20
|
### Fixed
|
|
17
21
|
|
|
22
|
+
- Managed fallback snapshots now convert unauthenticated Unicode evidence into `incompleteArgumentsReason: "malformed"` instead of preserving a boolean-only guard that could lose the evidence needed by terminal validation.
|
|
18
23
|
- Compaction no longer crashes on a persisted tool call whose `arguments` payload is null. `serializeConversation` passed that value straight into `Object.entries`, which threw `TypeError: Object.entries requires that input parameter not be null or undefined`. Because this runs inside compaction — itself the recovery path for context overflow — the failure surfaced as `Context overflow recovery failed: Object.entries requires ...`, and the next request went out uncompacted until the provider rejected it with `prompt is too long`. Malformed argument payloads now serialize as an empty argument list instead of aborting the summary.
|
|
19
24
|
|
|
20
25
|
## [0.15.2] - 2026-08-25
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type AssistantMessage, type AssistantMessageEvent, type CursorExecHandl
|
|
|
5
5
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
6
6
|
import type { AttemptRunHandle, AttemptScope } from "./attempt-scope";
|
|
7
7
|
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
8
|
-
import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunCancellationDomainBridge, RunResourceLedger, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
|
|
8
|
+
import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentMetadataResolverContext, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunCancellationDomainBridge, RunResourceLedger, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
|
|
9
9
|
/**
|
|
10
10
|
* Whether persisted history ends at a point where a new model turn can resume.
|
|
11
11
|
* Assistant-ended histories require an in-memory queued message and are handled
|
|
@@ -271,7 +271,7 @@ export declare class Agent {
|
|
|
271
271
|
* only included for `"anthropic"` requests). Falls back to the static
|
|
272
272
|
* {@link metadata} value when no resolver is set.
|
|
273
273
|
*/
|
|
274
|
-
metadataForProvider(provider: string): Record<string, unknown> | undefined;
|
|
274
|
+
metadataForProvider(provider: string, model?: Model, transport?: AgentMetadataResolverContext["transport"]): Record<string, unknown> | undefined;
|
|
275
275
|
/**
|
|
276
276
|
* Install a function that resolves request metadata at call time. The
|
|
277
277
|
* resolver receives the target provider string and can gate provider-specific
|
|
@@ -280,7 +280,7 @@ export declare class Agent {
|
|
|
280
280
|
* credential. Pass `undefined` to clear and revert to the static
|
|
281
281
|
* {@link metadata} value.
|
|
282
282
|
*/
|
|
283
|
-
setMetadataResolver(resolver: ((
|
|
283
|
+
setMetadataResolver(resolver: ((context: AgentMetadataResolverContext) => Record<string, unknown> | undefined) | undefined): void;
|
|
284
284
|
/**
|
|
285
285
|
* Read the active OpenTelemetry configuration. Returns `undefined` when
|
|
286
286
|
* instrumentation is disabled. Callers spawning child runs (e.g. subagent
|
|
@@ -358,6 +358,18 @@ export declare class Agent {
|
|
|
358
358
|
setProvisionalAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
|
|
359
359
|
setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
|
|
360
360
|
setShouldPause(fn: AgentLoopConfig["shouldPause"] | undefined): void;
|
|
361
|
+
/** The currently installed cooperative pause checkpoint, if any. */
|
|
362
|
+
get shouldPause(): AgentLoopConfig["shouldPause"] | undefined;
|
|
363
|
+
/**
|
|
364
|
+
* Fence old-turn steering admission.
|
|
365
|
+
*
|
|
366
|
+
* The loop polls steering UPSTREAM of its pause checkpoint (and again on the
|
|
367
|
+
* immediate-interrupt path), so a cooperative stop alone cannot prevent one
|
|
368
|
+
* more old-turn model call once a steering message has already been dequeued.
|
|
369
|
+
* While the fence returns true the poll yields no messages AND does not
|
|
370
|
+
* dequeue, so the queue survives intact for the next turn.
|
|
371
|
+
*/
|
|
372
|
+
setSteeringAdmissionFence(fn: (() => boolean) | undefined): void;
|
|
361
373
|
setMaintainContext(fn: AgentLoopConfig["maintainContext"] | undefined): void;
|
|
362
374
|
/**
|
|
363
375
|
* Publish an event produced OUTSIDE the agent loop (a provider that executed the tool
|
package/dist/types/types.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
|
|
|
6
6
|
import type { AgentTelemetryConfig } from "./telemetry";
|
|
7
7
|
/** Stream function - can return sync or Promise for async config lookup */
|
|
8
8
|
export type StreamFn = (...args: Parameters<typeof streamSimple>) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
9
|
+
/**
|
|
10
|
+
* Request context supplied to provider-aware metadata resolvers.
|
|
11
|
+
*
|
|
12
|
+
* The model is the exact model selected for the concrete request (including
|
|
13
|
+
* fallback and ephemeral requests), while `transport` distinguishes the
|
|
14
|
+
* built-in stream path from a caller-supplied stream function. Metadata that
|
|
15
|
+
* carries provider identity must use both values to fail closed when routing
|
|
16
|
+
* is not the canonical provider path.
|
|
17
|
+
*/
|
|
18
|
+
export interface AgentMetadataResolverContext {
|
|
19
|
+
provider: string;
|
|
20
|
+
model?: Model;
|
|
21
|
+
transport?: "default" | "custom";
|
|
22
|
+
}
|
|
9
23
|
/** Stable identifier for a managed logical run, shared by all of its retry attempts. */
|
|
10
24
|
export type ManagedLogicalRunId = number;
|
|
11
25
|
/** A resource owned by a prompt run until its promise settles. */
|
|
@@ -233,7 +247,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
233
247
|
* current when `AgentLoopConfig` was first constructed). Overrides the static
|
|
234
248
|
* `metadata` field when present.
|
|
235
249
|
*/
|
|
236
|
-
metadataResolver?: (
|
|
250
|
+
metadataResolver?: (context: AgentMetadataResolverContext) => Record<string, unknown> | undefined;
|
|
237
251
|
/**
|
|
238
252
|
* Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.
|
|
239
253
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/agent-core",
|
|
4
|
-
"version": "0.15.
|
|
4
|
+
"version": "0.15.6",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"fmt": "biome format --write ."
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@gajae-code/ai": "0.15.
|
|
36
|
-
"@gajae-code/natives": "0.15.
|
|
37
|
-
"@gajae-code/utils": "0.15.
|
|
35
|
+
"@gajae-code/ai": "0.15.6",
|
|
36
|
+
"@gajae-code/natives": "0.15.6",
|
|
37
|
+
"@gajae-code/utils": "0.15.6",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
EventStream,
|
|
15
15
|
isProviderSafetyStopAuthenticated,
|
|
16
16
|
isZodSchema,
|
|
17
|
+
SERVER_OVERLOADED_PROVIDER_CODE,
|
|
17
18
|
streamSimple,
|
|
18
19
|
type ToolChoice,
|
|
19
20
|
type ToolResultMessage,
|
|
@@ -403,12 +404,57 @@ export const ESCAPED_NONASCII_RECOVERY_PROMPT = escapedNonAsciiRecoveryPrompt;
|
|
|
403
404
|
const MAX_ESCAPED_NONASCII_RESAMPLES = 2;
|
|
404
405
|
|
|
405
406
|
/** Whether any tool call in the turn carried `\uXXXX`-escaped arguments. */
|
|
407
|
+
interface EscapedToolCallMetadata {
|
|
408
|
+
guarded: boolean;
|
|
409
|
+
malformed: boolean;
|
|
410
|
+
evidence: UnicodeEscapeEvidence | undefined;
|
|
411
|
+
incompleteArguments: boolean;
|
|
412
|
+
incompleteArgumentsReason: unknown;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const acceptedToolCallMetadata = new WeakMap<object, EscapedToolCallMetadata>();
|
|
416
|
+
|
|
417
|
+
function escapedToolCallMetadata(
|
|
418
|
+
block: Extract<AssistantMessage["content"][number], { type: "toolCall" }>,
|
|
419
|
+
): EscapedToolCallMetadata {
|
|
420
|
+
const guardRead = managedOwnPropertyRead(block, "escapedNonAsciiArguments");
|
|
421
|
+
const evidenceRead = managedOwnPropertyRead(block, "escapedUnicodeArgumentEvidence");
|
|
422
|
+
const incompleteRead = managedPropertyRead(block, "incompleteArguments");
|
|
423
|
+
const incompleteReasonRead = managedPropertyRead(block, "incompleteArgumentsReason");
|
|
424
|
+
const inheritedGuard = managedInheritedProperty(block, "escapedNonAsciiArguments");
|
|
425
|
+
const inheritedEvidence = managedInheritedProperty(block, "escapedUnicodeArgumentEvidence");
|
|
426
|
+
let evidence: UnicodeEscapeEvidence | undefined;
|
|
427
|
+
try {
|
|
428
|
+
evidence = managedUnicodeEscapeEvidence(evidenceRead.value);
|
|
429
|
+
} catch {
|
|
430
|
+
evidence = undefined;
|
|
431
|
+
}
|
|
432
|
+
const malformedMetadata =
|
|
433
|
+
!guardRead.ok ||
|
|
434
|
+
!evidenceRead.ok ||
|
|
435
|
+
!incompleteRead.ok ||
|
|
436
|
+
!incompleteReasonRead.ok ||
|
|
437
|
+
inheritedGuard ||
|
|
438
|
+
inheritedEvidence ||
|
|
439
|
+
(typeof incompleteRead.value === "string" && SANITIZER_SENTINELS.has(incompleteRead.value)) ||
|
|
440
|
+
(typeof incompleteReasonRead.value === "string" && SANITIZER_SENTINELS.has(incompleteReasonRead.value)) ||
|
|
441
|
+
(evidenceRead.present && (evidenceRead.value === undefined || evidence === undefined || evidence.malformed));
|
|
442
|
+
const incomplete = incompleteRead.ok && incompleteRead.value === true;
|
|
443
|
+
return {
|
|
444
|
+
guarded:
|
|
445
|
+
malformedMetadata ||
|
|
446
|
+
(guardRead.present && guardRead.value === true) ||
|
|
447
|
+
(evidenceRead.present && evidenceRead.value !== undefined),
|
|
448
|
+
malformed: malformedMetadata || incomplete,
|
|
449
|
+
evidence,
|
|
450
|
+
incompleteArguments: incomplete,
|
|
451
|
+
incompleteArgumentsReason:
|
|
452
|
+
!incompleteReasonRead.ok || malformedMetadata ? "malformed" : incompleteReasonRead.value,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
406
456
|
function hasEscapedNonAsciiToolCall(message: AssistantMessage): boolean {
|
|
407
|
-
return message.content.some(
|
|
408
|
-
block =>
|
|
409
|
-
block.type === "toolCall" &&
|
|
410
|
-
(block.escapedNonAsciiArguments === true || block.escapedUnicodeArgumentEvidence !== undefined),
|
|
411
|
-
);
|
|
457
|
+
return message.content.some(block => block.type === "toolCall" && escapedToolCallMetadata(block).guarded);
|
|
412
458
|
}
|
|
413
459
|
|
|
414
460
|
const ESCAPED_NONASCII_DIAGNOSTIC_TOOL_CALL_COUNT_MAX = 8;
|
|
@@ -419,9 +465,7 @@ function escapedNonAsciiToolCallShape(message: AssistantMessage): {
|
|
|
419
465
|
escapedToolCallCountCapped: boolean;
|
|
420
466
|
} {
|
|
421
467
|
const count = message.content.filter(
|
|
422
|
-
block =>
|
|
423
|
-
block.type === "toolCall" &&
|
|
424
|
-
(block.escapedNonAsciiArguments === true || block.escapedUnicodeArgumentEvidence !== undefined),
|
|
468
|
+
block => block.type === "toolCall" && escapedToolCallMetadata(block).guarded,
|
|
425
469
|
).length;
|
|
426
470
|
return {
|
|
427
471
|
escapedToolCallCount: Math.min(count, ESCAPED_NONASCII_DIAGNOSTIC_TOOL_CALL_COUNT_MAX),
|
|
@@ -430,9 +474,53 @@ function escapedNonAsciiToolCallShape(message: AssistantMessage): {
|
|
|
430
474
|
}
|
|
431
475
|
|
|
432
476
|
/** Remove transient raw-evidence metadata before any message can become durable. */
|
|
477
|
+
function stripToolCallEvidence<T extends { escapedUnicodeArgumentEvidence?: unknown }>(toolCall: T): T {
|
|
478
|
+
try {
|
|
479
|
+
if (nodeUtilTypes.isProxy(toolCall)) {
|
|
480
|
+
const sanitized = Object.create(null) as T;
|
|
481
|
+
for (const key of [
|
|
482
|
+
"type",
|
|
483
|
+
"id",
|
|
484
|
+
"name",
|
|
485
|
+
"arguments",
|
|
486
|
+
"thoughtSignature",
|
|
487
|
+
"intent",
|
|
488
|
+
"customWireName",
|
|
489
|
+
"escapedNonAsciiArguments",
|
|
490
|
+
"incompleteArguments",
|
|
491
|
+
"incompleteArgumentsReason",
|
|
492
|
+
]) {
|
|
493
|
+
const read = managedPropertyRead(toolCall, key);
|
|
494
|
+
if (read.ok && read.value !== undefined)
|
|
495
|
+
Object.defineProperty(sanitized, key, { value: read.value, enumerable: true });
|
|
496
|
+
}
|
|
497
|
+
return sanitized;
|
|
498
|
+
}
|
|
499
|
+
const descriptor = Object.getOwnPropertyDescriptor(toolCall, "escapedUnicodeArgumentEvidence");
|
|
500
|
+
if (!descriptor) return toolCall;
|
|
501
|
+
if (descriptor.configurable && Reflect.deleteProperty(toolCall, "escapedUnicodeArgumentEvidence"))
|
|
502
|
+
return toolCall;
|
|
503
|
+
const sanitized = Object.create(null) as T;
|
|
504
|
+
for (const key of Reflect.ownKeys(toolCall)) {
|
|
505
|
+
if (key === "escapedUnicodeArgumentEvidence") continue;
|
|
506
|
+
const own = Object.getOwnPropertyDescriptor(toolCall, key);
|
|
507
|
+
if (!own?.enumerable || !("value" in own)) continue;
|
|
508
|
+
try {
|
|
509
|
+
Object.defineProperty(sanitized, key, own);
|
|
510
|
+
} catch {
|
|
511
|
+
// Skip hostile descriptors; required fields are already captured in the record.
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return sanitized;
|
|
515
|
+
} catch {
|
|
516
|
+
return Object.create(null) as T;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
433
520
|
function stripUnicodeEscapeEvidence(message: AssistantMessage): void {
|
|
434
|
-
for (
|
|
435
|
-
|
|
521
|
+
for (let index = 0; index < message.content.length; index += 1) {
|
|
522
|
+
const block = message.content[index];
|
|
523
|
+
if (block?.type === "toolCall") message.content[index] = stripToolCallEvidence(block);
|
|
436
524
|
}
|
|
437
525
|
}
|
|
438
526
|
|
|
@@ -606,14 +694,13 @@ function allEscapedToolCallsDisplaySafe(
|
|
|
606
694
|
let sawEscapedCall = false;
|
|
607
695
|
for (const block of message.content) {
|
|
608
696
|
if (block.type !== "toolCall") continue;
|
|
609
|
-
|
|
697
|
+
const metadata = escapedToolCallMetadata(block);
|
|
698
|
+
if (!metadata.guarded) continue;
|
|
699
|
+
if (metadata.malformed) return false;
|
|
610
700
|
sawEscapedCall = true;
|
|
611
701
|
const tool = tools?.find(candidate => candidate.name === block.name);
|
|
612
702
|
const args = block.arguments as Record<string, unknown>;
|
|
613
|
-
if (
|
|
614
|
-
!isDisplaySafeEscapedArguments(tool, args) ||
|
|
615
|
-
!isDisplaySafeRawEscapeEvidence(tool, args, block.escapedUnicodeArgumentEvidence)
|
|
616
|
-
)
|
|
703
|
+
if (!isDisplaySafeEscapedArguments(tool, args) || !isDisplaySafeRawEscapeEvidence(tool, args, metadata.evidence))
|
|
617
704
|
return false;
|
|
618
705
|
}
|
|
619
706
|
return sawEscapedCall;
|
|
@@ -656,6 +743,25 @@ function managedProperty(value: unknown, key: string): unknown {
|
|
|
656
743
|
return managedPropertyRead(value, key).value;
|
|
657
744
|
}
|
|
658
745
|
|
|
746
|
+
function managedOwnPropertyRead(value: unknown, key: string): { present: boolean; ok: boolean; value: unknown } {
|
|
747
|
+
if (!value || typeof value !== "object") return { present: false, ok: true, value: undefined };
|
|
748
|
+
try {
|
|
749
|
+
if (!Object.hasOwn(value, key)) return { present: false, ok: true, value: undefined };
|
|
750
|
+
return { present: true, ok: true, value: Reflect.get(value, key) };
|
|
751
|
+
} catch {
|
|
752
|
+
return { present: true, ok: false, value: undefined };
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function managedInheritedProperty(value: unknown, key: string): boolean {
|
|
757
|
+
if (!value || typeof value !== "object") return false;
|
|
758
|
+
try {
|
|
759
|
+
return !Object.hasOwn(value, key) && key in value;
|
|
760
|
+
} catch {
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
659
765
|
function managedTransportFailure(failure: unknown) {
|
|
660
766
|
const facts = managedProperty(failure, "transportFailure");
|
|
661
767
|
return facts && typeof facts === "object" ? transportFailureFacts(facts) : undefined;
|
|
@@ -678,6 +784,19 @@ function isManagedProviderSafetyStopAuthenticated(value: unknown): boolean {
|
|
|
678
784
|
function managedRetryableFailure(failure: unknown): boolean {
|
|
679
785
|
const facts = managedTransportFailure(failure);
|
|
680
786
|
if (!facts) return false;
|
|
787
|
+
// OpenAI's typed statusless capacity-overload code (issue #5018) never
|
|
788
|
+
// becomes managed transaction authority. Before the code survived as
|
|
789
|
+
// transport facts this failure produced none, so the staged attempt was
|
|
790
|
+
// always committed; the shared Responses parser and Codex events now carry
|
|
791
|
+
// it, and this check preserves that committed-failure behavior instead of
|
|
792
|
+
// discarding the transaction. It reads only typed facts, never error text.
|
|
793
|
+
if (
|
|
794
|
+
facts.status === undefined &&
|
|
795
|
+
facts.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
|
|
796
|
+
(facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE)
|
|
797
|
+
) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
681
800
|
// A typed provider safety stop is terminal evidence ahead of any transport
|
|
682
801
|
// class, but only with adapter-minted provenance: unauthenticated labels
|
|
683
802
|
// are stripped at the stream exit (`sanitizeProviderSafetyStopProvenance`)
|
|
@@ -748,6 +867,12 @@ function sanitizeProviderSafetyStopProvenance(
|
|
|
748
867
|
if (isManagedPlainRecord(detached)) {
|
|
749
868
|
const rebuilt = { ...detached } as AssistantMessage;
|
|
750
869
|
delete rebuilt.errorKind;
|
|
870
|
+
if (!Array.isArray(rebuilt.content)) {
|
|
871
|
+
const repaired = managedAssistantShell(message, model);
|
|
872
|
+
delete repaired.errorKind;
|
|
873
|
+
return repaired;
|
|
874
|
+
}
|
|
875
|
+
restoreTransientUnicodeEscapeEvidence(rebuilt.content, message);
|
|
751
876
|
return rebuilt;
|
|
752
877
|
}
|
|
753
878
|
const rebuilt = managedAssistantShell(message, model);
|
|
@@ -1947,7 +2072,11 @@ function managedAssistantShell(
|
|
|
1947
2072
|
// `message.role === "assistant"` check passes while the detached
|
|
1948
2073
|
// snapshot retains none of the message identity.
|
|
1949
2074
|
const source =
|
|
1950
|
-
snapshotRecord !== undefined &&
|
|
2075
|
+
snapshotRecord !== undefined &&
|
|
2076
|
+
managedProperty(snapshotRecord, "role") === "assistant" &&
|
|
2077
|
+
(managedProperty(snapshotRecord, "content") !== undefined || managedProperty(value, "content") === undefined)
|
|
2078
|
+
? snapshotRecord
|
|
2079
|
+
: value;
|
|
1951
2080
|
if (managedProperty(source, "role") !== "assistant") throw new ManagedAttemptSnapshotError("shell.role");
|
|
1952
2081
|
const rawContent = managedAttemptSnapshot(managedProperty(source, "content"));
|
|
1953
2082
|
// Providers may deliver `content` as a string, missing value, or a primitive
|
|
@@ -2048,11 +2177,18 @@ function managedContentBlock(block: unknown): AssistantMessage["content"] {
|
|
|
2048
2177
|
|
|
2049
2178
|
function managedUnicodeEscapeEvidence(value: unknown): UnicodeEscapeEvidence | undefined {
|
|
2050
2179
|
if (!isManagedPlainRecord(value)) return undefined;
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2180
|
+
const envelopeReads = Object.fromEntries(
|
|
2181
|
+
["positions", "totalPositions", "truncated", "malformed", "integrity"].map(key => [
|
|
2182
|
+
key,
|
|
2183
|
+
managedOwnPropertyRead(value, key),
|
|
2184
|
+
]),
|
|
2185
|
+
) as Record<string, { present: boolean; ok: boolean; value: unknown }>;
|
|
2186
|
+
if (Object.values(envelopeReads).some(read => !read.present || !read.ok)) return undefined;
|
|
2187
|
+
const positionsValue = envelopeReads.positions!.value;
|
|
2188
|
+
const totalPositions = envelopeReads.totalPositions!.value;
|
|
2189
|
+
const truncated = envelopeReads.truncated!.value;
|
|
2190
|
+
const malformed = envelopeReads.malformed!.value;
|
|
2191
|
+
const integrity = envelopeReads.integrity!.value;
|
|
2056
2192
|
if (!Array.isArray(positionsValue) || positionsValue.length > 32) return undefined;
|
|
2057
2193
|
if (
|
|
2058
2194
|
typeof totalPositions !== "number" ||
|
|
@@ -2067,12 +2203,19 @@ function managedUnicodeEscapeEvidence(value: unknown): UnicodeEscapeEvidence | u
|
|
|
2067
2203
|
const positions: UnicodeEscapeEvidence["positions"][number][] = [];
|
|
2068
2204
|
for (const positionValue of positionsValue) {
|
|
2069
2205
|
if (!isManagedPlainRecord(positionValue)) return undefined;
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2206
|
+
const positionReads = Object.fromEntries(
|
|
2207
|
+
["offset", "scalarTag", "pathTag", "location", "valueOrdinal", "valueOffset"].map(key => [
|
|
2208
|
+
key,
|
|
2209
|
+
managedOwnPropertyRead(positionValue, key),
|
|
2210
|
+
]),
|
|
2211
|
+
) as Record<string, { present: boolean; ok: boolean; value: unknown }>;
|
|
2212
|
+
if (Object.values(positionReads).some(read => !read.present || !read.ok)) return undefined;
|
|
2213
|
+
const offset = positionReads.offset!.value;
|
|
2214
|
+
const scalarTag = positionReads.scalarTag!.value;
|
|
2215
|
+
const pathTag = positionReads.pathTag!.value;
|
|
2216
|
+
const location = positionReads.location!.value;
|
|
2217
|
+
const valueOrdinal = positionReads.valueOrdinal!.value;
|
|
2218
|
+
const valueOffset = positionReads.valueOffset!.value;
|
|
2076
2219
|
if (
|
|
2077
2220
|
typeof offset !== "number" ||
|
|
2078
2221
|
!Number.isSafeInteger(offset) ||
|
|
@@ -2093,14 +2236,15 @@ function managedUnicodeEscapeEvidence(value: unknown): UnicodeEscapeEvidence | u
|
|
|
2093
2236
|
}
|
|
2094
2237
|
positions.push({ offset, scalarTag, pathTag, location, valueOrdinal, valueOffset });
|
|
2095
2238
|
}
|
|
2096
|
-
|
|
2239
|
+
const evidence = { positions, totalPositions, truncated, malformed, integrity };
|
|
2240
|
+
return verifyUnicodeEscapeEvidence(evidence) ? evidence : undefined;
|
|
2097
2241
|
}
|
|
2098
2242
|
|
|
2099
2243
|
function restoreTransientUnicodeEscapeEvidence(content: AssistantMessage["content"], liveMessage: unknown): void {
|
|
2100
2244
|
const liveContent = managedProperty(liveMessage, "content");
|
|
2101
2245
|
if (!Array.isArray(liveContent)) return;
|
|
2102
2246
|
for (const destination of content) {
|
|
2103
|
-
if (destination.type !== "toolCall") continue;
|
|
2247
|
+
if (!destination || typeof destination !== "object" || destination.type !== "toolCall") continue;
|
|
2104
2248
|
const matches = liveContent.filter(
|
|
2105
2249
|
candidate =>
|
|
2106
2250
|
isManagedPlainRecord(candidate) &&
|
|
@@ -2108,12 +2252,52 @@ function restoreTransientUnicodeEscapeEvidence(content: AssistantMessage["conten
|
|
|
2108
2252
|
managedProperty(candidate, "id") === destination.id &&
|
|
2109
2253
|
managedProperty(candidate, "name") === destination.name,
|
|
2110
2254
|
);
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2255
|
+
const evidenceReads = matches.map(candidate =>
|
|
2256
|
+
managedOwnPropertyRead(candidate, "escapedUnicodeArgumentEvidence"),
|
|
2257
|
+
);
|
|
2258
|
+
const inheritedEvidence = matches.map(candidate =>
|
|
2259
|
+
managedInheritedProperty(candidate, "escapedUnicodeArgumentEvidence"),
|
|
2260
|
+
);
|
|
2261
|
+
const guardReads = matches.map(candidate => managedOwnPropertyRead(candidate, "escapedNonAsciiArguments"));
|
|
2262
|
+
const inheritedGuards = matches.map(candidate => managedInheritedProperty(candidate, "escapedNonAsciiArguments"));
|
|
2263
|
+
if (matches.length !== 1) {
|
|
2264
|
+
if (
|
|
2265
|
+
evidenceReads.some(read => read.present) ||
|
|
2266
|
+
inheritedEvidence.some(Boolean) ||
|
|
2267
|
+
guardReads.some(read => read.present) ||
|
|
2268
|
+
inheritedGuards.some(Boolean)
|
|
2269
|
+
) {
|
|
2270
|
+
destination.incompleteArguments = true;
|
|
2271
|
+
destination.incompleteArgumentsReason = "malformed";
|
|
2272
|
+
}
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
2275
|
+
const guardRead = guardReads[0]!;
|
|
2276
|
+
if (!guardRead.ok || inheritedGuards[0]) {
|
|
2277
|
+
destination.incompleteArguments = true;
|
|
2278
|
+
destination.incompleteArgumentsReason = "malformed";
|
|
2279
|
+
}
|
|
2280
|
+
if (guardRead.ok && guardRead.present && guardRead.value === true) destination.escapedNonAsciiArguments = true;
|
|
2281
|
+
const evidenceRead = evidenceReads[0]!;
|
|
2282
|
+
if (!evidenceRead.present || !evidenceRead.ok || evidenceRead.value === undefined || inheritedEvidence[0]) {
|
|
2283
|
+
if (evidenceRead.present || inheritedEvidence[0]) {
|
|
2284
|
+
destination.incompleteArguments = true;
|
|
2285
|
+
destination.incompleteArgumentsReason = "malformed";
|
|
2286
|
+
}
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
const rawEvidence = evidenceRead.value;
|
|
2290
|
+
let evidence: UnicodeEscapeEvidence | undefined;
|
|
2291
|
+
try {
|
|
2292
|
+
evidence = managedUnicodeEscapeEvidence(rawEvidence);
|
|
2293
|
+
} catch {
|
|
2294
|
+
evidence = undefined;
|
|
2295
|
+
}
|
|
2296
|
+
if (evidence && !evidence.malformed) attachUnicodeEscapeEvidence(destination, evidence);
|
|
2297
|
+
else {
|
|
2298
|
+
destination.incompleteArguments = true;
|
|
2299
|
+
destination.incompleteArgumentsReason = "malformed";
|
|
2300
|
+
}
|
|
2117
2301
|
}
|
|
2118
2302
|
}
|
|
2119
2303
|
|
|
@@ -2140,12 +2324,47 @@ function managedAssistantContent(value: unknown): AssistantMessage["content"][nu
|
|
|
2140
2324
|
const thoughtSignature = managedProperty(value, "thoughtSignature");
|
|
2141
2325
|
const intent = managedProperty(value, "intent");
|
|
2142
2326
|
const customWireName = managedProperty(value, "customWireName");
|
|
2143
|
-
const
|
|
2144
|
-
const
|
|
2145
|
-
const
|
|
2146
|
-
const
|
|
2147
|
-
const
|
|
2148
|
-
const
|
|
2327
|
+
const incompleteArgumentsRead = managedPropertyRead(value, "incompleteArguments");
|
|
2328
|
+
const incompleteArgumentsReasonRead = managedPropertyRead(value, "incompleteArgumentsReason");
|
|
2329
|
+
const escapedGuardRead = managedOwnPropertyRead(value, "escapedNonAsciiArguments");
|
|
2330
|
+
const evidenceRead = managedOwnPropertyRead(value, "escapedUnicodeArgumentEvidence");
|
|
2331
|
+
const inheritedGuard = managedInheritedProperty(value, "escapedNonAsciiArguments");
|
|
2332
|
+
const inheritedEvidence = managedInheritedProperty(value, "escapedUnicodeArgumentEvidence");
|
|
2333
|
+
const inheritedIncompleteArguments = managedInheritedProperty(value, "incompleteArguments");
|
|
2334
|
+
const inheritedIncompleteArgumentsReason = managedInheritedProperty(value, "incompleteArgumentsReason");
|
|
2335
|
+
const incompleteArguments = incompleteArgumentsRead.value;
|
|
2336
|
+
const incompleteArgumentsReason = incompleteArgumentsReasonRead.value;
|
|
2337
|
+
const incompleteMetadataSentinel =
|
|
2338
|
+
(typeof incompleteArguments === "string" && SANITIZER_SENTINELS.has(incompleteArguments)) ||
|
|
2339
|
+
(typeof incompleteArgumentsReason === "string" && SANITIZER_SENTINELS.has(incompleteArgumentsReason));
|
|
2340
|
+
const incompleteMetadataMalformed =
|
|
2341
|
+
!incompleteArgumentsRead.ok ||
|
|
2342
|
+
!incompleteArgumentsReasonRead.ok ||
|
|
2343
|
+
inheritedIncompleteArguments ||
|
|
2344
|
+
inheritedIncompleteArgumentsReason ||
|
|
2345
|
+
incompleteMetadataSentinel;
|
|
2346
|
+
const escapedNonAsciiArguments = escapedGuardRead.value;
|
|
2347
|
+
const rawEscapedUnicodeArgumentEvidence = evidenceRead.value;
|
|
2348
|
+
let escapedUnicodeArgumentEvidence: UnicodeEscapeEvidence | undefined;
|
|
2349
|
+
try {
|
|
2350
|
+
escapedUnicodeArgumentEvidence = managedUnicodeEscapeEvidence(rawEscapedUnicodeArgumentEvidence);
|
|
2351
|
+
} catch {
|
|
2352
|
+
escapedUnicodeArgumentEvidence = undefined;
|
|
2353
|
+
}
|
|
2354
|
+
const invalidEscapedUnicodeEvidence =
|
|
2355
|
+
(evidenceRead.present &&
|
|
2356
|
+
(!evidenceRead.ok ||
|
|
2357
|
+
rawEscapedUnicodeArgumentEvidence === undefined ||
|
|
2358
|
+
escapedUnicodeArgumentEvidence === undefined ||
|
|
2359
|
+
escapedUnicodeArgumentEvidence.malformed)) ||
|
|
2360
|
+
!escapedGuardRead.ok ||
|
|
2361
|
+
inheritedGuard ||
|
|
2362
|
+
inheritedEvidence ||
|
|
2363
|
+
incompleteMetadataMalformed;
|
|
2364
|
+
const escapedArgumentsGuarded =
|
|
2365
|
+
invalidEscapedUnicodeEvidence ||
|
|
2366
|
+
(escapedGuardRead.present && escapedGuardRead.ok && escapedNonAsciiArguments === true) ||
|
|
2367
|
+
(evidenceRead.present && evidenceRead.ok && escapedUnicodeArgumentEvidence !== undefined);
|
|
2149
2368
|
return {
|
|
2150
2369
|
type,
|
|
2151
2370
|
id,
|
|
@@ -2154,16 +2373,22 @@ function managedAssistantContent(value: unknown): AssistantMessage["content"][nu
|
|
|
2154
2373
|
...(typeof thoughtSignature === "string" ? { thoughtSignature } : {}),
|
|
2155
2374
|
...(typeof intent === "string" ? { intent } : {}),
|
|
2156
2375
|
...(typeof customWireName === "string" ? { customWireName } : {}),
|
|
2157
|
-
...(
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2376
|
+
...(invalidEscapedUnicodeEvidence
|
|
2377
|
+
? { incompleteArguments: true }
|
|
2378
|
+
: typeof incompleteArguments === "boolean"
|
|
2379
|
+
? { incompleteArguments }
|
|
2380
|
+
: {}),
|
|
2381
|
+
...(invalidEscapedUnicodeEvidence
|
|
2382
|
+
? { incompleteArgumentsReason: "malformed" as const }
|
|
2383
|
+
: typeof incompleteArgumentsReason === "string"
|
|
2384
|
+
? {
|
|
2385
|
+
incompleteArgumentsReason: incompleteArgumentsReason as
|
|
2386
|
+
| "truncated"
|
|
2387
|
+
| "malformed"
|
|
2388
|
+
| "conflicting"
|
|
2389
|
+
| "ambiguous",
|
|
2390
|
+
}
|
|
2391
|
+
: {}),
|
|
2167
2392
|
...(escapedArgumentsGuarded
|
|
2168
2393
|
? { escapedNonAsciiArguments: true }
|
|
2169
2394
|
: typeof escapedNonAsciiArguments === "boolean"
|
|
@@ -2582,7 +2807,57 @@ class ManagedAttemptTransaction {
|
|
|
2582
2807
|
}
|
|
2583
2808
|
|
|
2584
2809
|
acceptedAssistantSnapshot(message: AssistantMessage): AssistantMessage {
|
|
2585
|
-
|
|
2810
|
+
const sourceContent = Array.isArray(message.content) ? message.content : [];
|
|
2811
|
+
const sourceMetadata: Array<EscapedToolCallMetadata | undefined> = [];
|
|
2812
|
+
for (let index = 0; index < sourceContent.length; index += 1) {
|
|
2813
|
+
const block = sourceContent[index];
|
|
2814
|
+
sourceMetadata[index] = block?.type === "toolCall" ? escapedToolCallMetadata(block) : undefined;
|
|
2815
|
+
}
|
|
2816
|
+
let snapshot = this.#assistantSnapshot(message);
|
|
2817
|
+
if (snapshot.role !== "assistant") return snapshot;
|
|
2818
|
+
if (!Array.isArray(snapshot.content)) {
|
|
2819
|
+
snapshot = managedAssistantShell(message, this.model, this.#degradedFieldDiagnostics, true);
|
|
2820
|
+
}
|
|
2821
|
+
for (let index = 0; index < sourceContent.length; index += 1) {
|
|
2822
|
+
const sourceBlock = sourceContent[index];
|
|
2823
|
+
if (sourceBlock?.type !== "toolCall") continue;
|
|
2824
|
+
const metadata = sourceMetadata[index];
|
|
2825
|
+
let snapshotBlock = snapshot.content[index];
|
|
2826
|
+
if (snapshotBlock?.type !== "toolCall") {
|
|
2827
|
+
const normalized = managedAssistantContent(sourceBlock);
|
|
2828
|
+
if (normalized?.type !== "toolCall") continue;
|
|
2829
|
+
snapshotBlock = normalized;
|
|
2830
|
+
snapshot.content[index] = snapshotBlock;
|
|
2831
|
+
}
|
|
2832
|
+
if (metadata) {
|
|
2833
|
+
const detachedMetadata = escapedToolCallMetadata(snapshotBlock);
|
|
2834
|
+
const evidencePresenceChanged = Boolean(metadata.evidence) !== Boolean(detachedMetadata.evidence);
|
|
2835
|
+
const metadataChanged =
|
|
2836
|
+
metadata.guarded !== detachedMetadata.guarded ||
|
|
2837
|
+
metadata.incompleteArguments !== detachedMetadata.incompleteArguments ||
|
|
2838
|
+
evidencePresenceChanged;
|
|
2839
|
+
const combinedMetadata: EscapedToolCallMetadata = {
|
|
2840
|
+
guarded: metadata.guarded || detachedMetadata.guarded || metadataChanged,
|
|
2841
|
+
malformed: metadata.malformed || detachedMetadata.malformed || metadataChanged,
|
|
2842
|
+
evidence: metadata.evidence ?? detachedMetadata.evidence,
|
|
2843
|
+
incompleteArguments: metadata.incompleteArguments || detachedMetadata.incompleteArguments,
|
|
2844
|
+
incompleteArgumentsReason:
|
|
2845
|
+
metadata.incompleteArgumentsReason ?? detachedMetadata.incompleteArgumentsReason,
|
|
2846
|
+
};
|
|
2847
|
+
if (combinedMetadata.malformed) {
|
|
2848
|
+
const marked = {
|
|
2849
|
+
...snapshotBlock,
|
|
2850
|
+
incompleteArguments: true,
|
|
2851
|
+
incompleteArgumentsReason: "malformed" as const,
|
|
2852
|
+
};
|
|
2853
|
+
snapshot.content[index] = marked;
|
|
2854
|
+
acceptedToolCallMetadata.set(marked, combinedMetadata);
|
|
2855
|
+
} else {
|
|
2856
|
+
acceptedToolCallMetadata.set(snapshotBlock, combinedMetadata);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return snapshot;
|
|
2586
2861
|
}
|
|
2587
2862
|
|
|
2588
2863
|
discard(): void {
|
|
@@ -4248,7 +4523,13 @@ async function streamAssistantResponse(
|
|
|
4248
4523
|
// reflects the credential actually used, not the snapshot from AgentLoopConfig construction.
|
|
4249
4524
|
const authCredentialType = config.getAuthCredentialType?.(config.model.provider);
|
|
4250
4525
|
|
|
4251
|
-
const resolvedMetadata = config.metadataResolver
|
|
4526
|
+
const resolvedMetadata = config.metadataResolver
|
|
4527
|
+
? config.metadataResolver({
|
|
4528
|
+
provider: config.model.provider,
|
|
4529
|
+
model: config.model,
|
|
4530
|
+
transport: streamFunction === streamSimple ? "default" : "custom",
|
|
4531
|
+
})
|
|
4532
|
+
: config.metadata;
|
|
4252
4533
|
|
|
4253
4534
|
// Synthetic recovery requests choose their tool mode explicitly below and
|
|
4254
4535
|
// must never consume a queued dynamic choice intended for an ordinary turn.
|
|
@@ -4751,20 +5032,23 @@ async function executeToolCalls(
|
|
|
4751
5032
|
let steeringMessages: AgentMessage[] | undefined;
|
|
4752
5033
|
let steeringCheck: Promise<void> | null = null;
|
|
4753
5034
|
|
|
4754
|
-
const records = toolCalls.map(toolCall =>
|
|
4755
|
-
toolCall
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
5035
|
+
const records = toolCalls.map(toolCall => {
|
|
5036
|
+
const metadata = acceptedToolCallMetadata.get(toolCall) ?? escapedToolCallMetadata(toolCall);
|
|
5037
|
+
return {
|
|
5038
|
+
toolCall: stripToolCallEvidence(toolCall),
|
|
5039
|
+
metadata,
|
|
5040
|
+
tool: findActiveTool(tools, toolCall.name),
|
|
5041
|
+
args: toolCall.arguments as Record<string, unknown>,
|
|
5042
|
+
eventFields: undefined as { toolCallId: string; toolName: string; intent: string | undefined } | undefined,
|
|
5043
|
+
started: false,
|
|
5044
|
+
result: undefined as AgentToolResult<any> | undefined,
|
|
5045
|
+
isError: false,
|
|
5046
|
+
skipped: false,
|
|
5047
|
+
toolResultMessage: undefined as ToolResultMessage | undefined,
|
|
5048
|
+
resultEmitted: false,
|
|
5049
|
+
argumentValidationFailed: false,
|
|
5050
|
+
};
|
|
5051
|
+
});
|
|
4768
5052
|
const checkSteering = async (): Promise<void> => {
|
|
4769
5053
|
// Never consume steering once the run's own signal is aborted: an aborted
|
|
4770
5054
|
// run cannot deliver it (the loop hands drained steering back and ends), and
|
|
@@ -4792,6 +5076,7 @@ async function executeToolCalls(
|
|
|
4792
5076
|
|
|
4793
5077
|
const emitToolResult = (record: (typeof records)[number], result: AgentToolResult<any>, isError: boolean): void => {
|
|
4794
5078
|
if (record.resultEmitted) return;
|
|
5079
|
+
record.toolCall = stripToolCallEvidence(record.toolCall);
|
|
4795
5080
|
const { toolCall } = record;
|
|
4796
5081
|
const eventFields =
|
|
4797
5082
|
record.eventFields ?? ({ toolCallId: toolCall.id, toolName: toolCall.name, intent: toolCall.intent } as const);
|
|
@@ -4845,6 +5130,24 @@ async function executeToolCalls(
|
|
|
4845
5130
|
stream.push({ type: "message_start", message: toolResultMessage, scope });
|
|
4846
5131
|
stream.push({ type: "message_end", message: toolResultMessage, scope });
|
|
4847
5132
|
};
|
|
5133
|
+
const isInvalidEscapedRecord = (record: (typeof records)[number]): boolean => {
|
|
5134
|
+
const metadata = record.metadata;
|
|
5135
|
+
if (metadata.malformed) return true;
|
|
5136
|
+
if (!metadata.guarded) return false;
|
|
5137
|
+
return !(
|
|
5138
|
+
isDisplaySafeEscapedArguments(record.tool, record.args) &&
|
|
5139
|
+
isDisplaySafeRawEscapeEvidence(record.tool, record.args, metadata.evidence)
|
|
5140
|
+
);
|
|
5141
|
+
};
|
|
5142
|
+
const hasInvalidEscapedCall = records.some(isInvalidEscapedRecord);
|
|
5143
|
+
if (hasInvalidEscapedCall) {
|
|
5144
|
+
for (const record of records) {
|
|
5145
|
+
if (isInvalidEscapedRecord(record)) continue;
|
|
5146
|
+
record.skipped = true;
|
|
5147
|
+
record.toolCall = stripToolCallEvidence(record.toolCall);
|
|
5148
|
+
emitToolResult(record, createSkippedToolResult(), true);
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
4848
5151
|
|
|
4849
5152
|
/**
|
|
4850
5153
|
* Prepare every value needed to publish and invoke one dispatch before claiming that it
|
|
@@ -4903,7 +5206,7 @@ async function executeToolCalls(
|
|
|
4903
5206
|
};
|
|
4904
5207
|
|
|
4905
5208
|
const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
|
|
4906
|
-
if (interruptState.triggered) {
|
|
5209
|
+
if (record.skipped || interruptState.triggered) {
|
|
4907
5210
|
// Skip both span emission and the collector orphan record here. The
|
|
4908
5211
|
// scheduler-task finalizer emits the skipped result and collector record;
|
|
4909
5212
|
// the tail sweep below remains a defensive fallback for unexpected throws.
|
|
@@ -4911,6 +5214,7 @@ async function executeToolCalls(
|
|
|
4911
5214
|
return;
|
|
4912
5215
|
}
|
|
4913
5216
|
|
|
5217
|
+
record.toolCall = stripToolCallEvidence(record.toolCall);
|
|
4914
5218
|
const { toolCall, tool } = record;
|
|
4915
5219
|
let argsForExecution = toolCall.arguments as Record<string, unknown>;
|
|
4916
5220
|
if (intentTracing) {
|
|
@@ -4948,16 +5252,17 @@ async function executeToolCalls(
|
|
|
4948
5252
|
|
|
4949
5253
|
await runInActiveSpan(toolSpan, async () => {
|
|
4950
5254
|
try {
|
|
4951
|
-
const
|
|
4952
|
-
const
|
|
4953
|
-
|
|
4954
|
-
if (escapedArgumentsGuarded)
|
|
4955
|
-
|
|
5255
|
+
const metadata = record.metadata;
|
|
5256
|
+
const escapedUnicodeArgumentEvidence = metadata.evidence;
|
|
5257
|
+
const escapedArgumentsGuarded = metadata.guarded;
|
|
5258
|
+
if (escapedArgumentsGuarded) record.toolCall = stripToolCallEvidence(record.toolCall);
|
|
5259
|
+
const incompleteArguments = metadata.malformed || metadata.incompleteArguments;
|
|
5260
|
+
if (incompleteArguments) {
|
|
4956
5261
|
record.argumentValidationFailed = true;
|
|
4957
5262
|
// The provider flagged this call's arguments as unsafe to execute.
|
|
4958
5263
|
// The typed reason selects accurate recovery guidance; callers that
|
|
4959
5264
|
// only read the boolean still get a safe, actionable rejection.
|
|
4960
|
-
const reason =
|
|
5265
|
+
const reason = metadata.incompleteArgumentsReason;
|
|
4961
5266
|
const detail =
|
|
4962
5267
|
reason === "malformed"
|
|
4963
5268
|
? `The terminal arguments for tool call "${toolCall.name}" did not decode to a valid JSON object. The arguments cannot be executed. Re-issue the call with valid, complete arguments.`
|
|
@@ -5265,6 +5570,7 @@ function createAbortedToolResult(
|
|
|
5265
5570
|
reason: "aborted" | "error",
|
|
5266
5571
|
errorMessage?: string,
|
|
5267
5572
|
): ToolResultMessage {
|
|
5573
|
+
toolCall = stripToolCallEvidence(toolCall);
|
|
5268
5574
|
const message = reason === "aborted" ? "Tool execution was aborted" : "Tool execution failed due to an error";
|
|
5269
5575
|
const result: AgentToolResult<any> = {
|
|
5270
5576
|
content: [{ type: "text", text: errorMessage ? `${message}: ${errorMessage}` : `${message}.` }],
|
package/src/agent.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
AgentEvent,
|
|
39
39
|
AgentLoopConfig,
|
|
40
40
|
AgentMessage,
|
|
41
|
+
AgentMetadataResolverContext,
|
|
41
42
|
AgentState,
|
|
42
43
|
AgentTool,
|
|
43
44
|
AgentToolContext,
|
|
@@ -67,6 +68,9 @@ const RUNTIME_FAILURE_CODES = new Set([
|
|
|
67
68
|
"local_snapshot_failure",
|
|
68
69
|
"provider_down",
|
|
69
70
|
"provider_unavailable",
|
|
71
|
+
"provider_rejected",
|
|
72
|
+
"provider_http_402",
|
|
73
|
+
"provider_http_429",
|
|
70
74
|
"upstream_stream_interrupted",
|
|
71
75
|
"argument_validation",
|
|
72
76
|
"execution",
|
|
@@ -120,13 +124,21 @@ function safeErrorStatus(error: unknown): number | undefined {
|
|
|
120
124
|
try {
|
|
121
125
|
return (
|
|
122
126
|
extractHttpStatusFromError({ status: (error as { errorStatus?: unknown } | undefined)?.errorStatus }) ??
|
|
123
|
-
extractHttpStatusFromError(error)
|
|
127
|
+
extractHttpStatusFromError(error) ??
|
|
128
|
+
extractHttpStatusFromError((error as { transportFailure?: unknown } | undefined)?.transportFailure)
|
|
124
129
|
);
|
|
125
130
|
} catch {
|
|
126
131
|
return undefined;
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
function providerFailureCode(error: unknown): string | undefined {
|
|
136
|
+
const status = safeErrorStatus(error);
|
|
137
|
+
if (status === undefined) return undefined;
|
|
138
|
+
if (status === 402 || status === 429) return `provider_http_${status}`;
|
|
139
|
+
return "provider_rejected";
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
function assertUserImagePlaceholdersHavePayload(messages: readonly AgentMessage[]): void {
|
|
131
143
|
for (const message of messages) {
|
|
132
144
|
if (!("role" in message) || message.role !== "user") continue;
|
|
@@ -455,7 +467,7 @@ export class Agent {
|
|
|
455
467
|
#sessionId?: string;
|
|
456
468
|
#providerSessionId?: string;
|
|
457
469
|
#metadata?: Record<string, unknown>;
|
|
458
|
-
#metadataResolver?: (
|
|
470
|
+
#metadataResolver?: (context: AgentMetadataResolverContext) => Record<string, unknown> | undefined;
|
|
459
471
|
#providerSessionState?: Map<string, ProviderSessionState>;
|
|
460
472
|
#thinkingBudgets?: ThinkingBudgets;
|
|
461
473
|
#temperature?: number;
|
|
@@ -494,6 +506,8 @@ export class Agent {
|
|
|
494
506
|
#onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
|
|
495
507
|
#onBeforeYield?: () => Promise<void> | void;
|
|
496
508
|
#shouldPause?: AgentLoopConfig["shouldPause"];
|
|
509
|
+
/** While set and returning true, steering is neither admitted nor dequeued. */
|
|
510
|
+
#steeringAdmissionFence?: () => boolean;
|
|
497
511
|
#maintainContext?: AgentLoopConfig["maintainContext"];
|
|
498
512
|
#telemetry?: AgentLoopConfig["telemetry"];
|
|
499
513
|
#appendOnlyContext?: AppendOnlyContextManager;
|
|
@@ -658,8 +672,12 @@ export class Agent {
|
|
|
658
672
|
* only included for `"anthropic"` requests). Falls back to the static
|
|
659
673
|
* {@link metadata} value when no resolver is set.
|
|
660
674
|
*/
|
|
661
|
-
metadataForProvider(
|
|
662
|
-
|
|
675
|
+
metadataForProvider(
|
|
676
|
+
provider: string,
|
|
677
|
+
model?: Model,
|
|
678
|
+
transport?: AgentMetadataResolverContext["transport"],
|
|
679
|
+
): Record<string, unknown> | undefined {
|
|
680
|
+
if (this.#metadataResolver) return this.#metadataResolver({ provider, model, transport });
|
|
663
681
|
return this.#metadata;
|
|
664
682
|
}
|
|
665
683
|
|
|
@@ -671,7 +689,9 @@ export class Agent {
|
|
|
671
689
|
* credential. Pass `undefined` to clear and revert to the static
|
|
672
690
|
* {@link metadata} value.
|
|
673
691
|
*/
|
|
674
|
-
setMetadataResolver(
|
|
692
|
+
setMetadataResolver(
|
|
693
|
+
resolver: ((context: AgentMetadataResolverContext) => Record<string, unknown> | undefined) | undefined,
|
|
694
|
+
): void {
|
|
675
695
|
this.#metadataResolver = resolver;
|
|
676
696
|
}
|
|
677
697
|
|
|
@@ -880,6 +900,24 @@ export class Agent {
|
|
|
880
900
|
this.#shouldPause = fn;
|
|
881
901
|
}
|
|
882
902
|
|
|
903
|
+
/** The currently installed cooperative pause checkpoint, if any. */
|
|
904
|
+
get shouldPause(): AgentLoopConfig["shouldPause"] | undefined {
|
|
905
|
+
return this.#shouldPause;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Fence old-turn steering admission.
|
|
910
|
+
*
|
|
911
|
+
* The loop polls steering UPSTREAM of its pause checkpoint (and again on the
|
|
912
|
+
* immediate-interrupt path), so a cooperative stop alone cannot prevent one
|
|
913
|
+
* more old-turn model call once a steering message has already been dequeued.
|
|
914
|
+
* While the fence returns true the poll yields no messages AND does not
|
|
915
|
+
* dequeue, so the queue survives intact for the next turn.
|
|
916
|
+
*/
|
|
917
|
+
setSteeringAdmissionFence(fn: (() => boolean) | undefined): void {
|
|
918
|
+
this.#steeringAdmissionFence = fn;
|
|
919
|
+
}
|
|
920
|
+
|
|
883
921
|
setMaintainContext(fn: AgentLoopConfig["maintainContext"] | undefined): void {
|
|
884
922
|
this.#maintainContext = fn;
|
|
885
923
|
}
|
|
@@ -1900,6 +1938,12 @@ export class Agent {
|
|
|
1900
1938
|
skipInitialSteeringPoll = false;
|
|
1901
1939
|
return [];
|
|
1902
1940
|
}
|
|
1941
|
+
// Fenced: yield nothing and dequeue nothing, so a steer submitted while a
|
|
1942
|
+
// fold is being claimed is neither consumed by the run being wound down
|
|
1943
|
+
// nor lost.
|
|
1944
|
+
if (this.#steeringAdmissionFence?.() === true) {
|
|
1945
|
+
return [];
|
|
1946
|
+
}
|
|
1903
1947
|
const queued = this.#dequeueSteeringMessages();
|
|
1904
1948
|
if (this.#activeRunId !== runId) {
|
|
1905
1949
|
this.#steeringQueue = [...queued, ...this.#steeringQueue];
|
|
@@ -2080,6 +2124,10 @@ export class Agent {
|
|
|
2080
2124
|
if (this.#activeRunId !== runId) {
|
|
2081
2125
|
return;
|
|
2082
2126
|
}
|
|
2127
|
+
const providerCode = providerFailureCode(err);
|
|
2128
|
+
const runtimeFailureCode = abortController.signal.aborted
|
|
2129
|
+
? "aborted"
|
|
2130
|
+
: (managedLocalErrorDiagnostic(err)?.errorKind ?? providerCode);
|
|
2083
2131
|
|
|
2084
2132
|
const errorMsg: AgentMessage = {
|
|
2085
2133
|
role: "assistant",
|
|
@@ -2116,10 +2164,7 @@ export class Agent {
|
|
|
2116
2164
|
// signal, and a local staging failure comes from the identity-
|
|
2117
2165
|
// checked managedLocalErrorDiagnostic — a foreign error that
|
|
2118
2166
|
// self-declares a local kind still maps to agent_failed.
|
|
2119
|
-
error: sanitizeAgentFailure(
|
|
2120
|
-
err,
|
|
2121
|
-
abortController.signal.aborted ? "aborted" : managedLocalErrorDiagnostic(err)?.errorKind,
|
|
2122
|
-
),
|
|
2167
|
+
error: sanitizeAgentFailure(err, runtimeFailureCode),
|
|
2123
2168
|
scope: handle.scope,
|
|
2124
2169
|
});
|
|
2125
2170
|
this.requestRunTerminal(managedLogicalRunOwner ?? runId, {
|
package/src/types.ts
CHANGED
|
@@ -28,6 +28,21 @@ export type StreamFn = (
|
|
|
28
28
|
...args: Parameters<typeof streamSimple>
|
|
29
29
|
) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Request context supplied to provider-aware metadata resolvers.
|
|
33
|
+
*
|
|
34
|
+
* The model is the exact model selected for the concrete request (including
|
|
35
|
+
* fallback and ephemeral requests), while `transport` distinguishes the
|
|
36
|
+
* built-in stream path from a caller-supplied stream function. Metadata that
|
|
37
|
+
* carries provider identity must use both values to fail closed when routing
|
|
38
|
+
* is not the canonical provider path.
|
|
39
|
+
*/
|
|
40
|
+
export interface AgentMetadataResolverContext {
|
|
41
|
+
provider: string;
|
|
42
|
+
model?: Model;
|
|
43
|
+
transport?: "default" | "custom";
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/** Stable identifier for a managed logical run, shared by all of its retry attempts. */
|
|
32
47
|
export type ManagedLogicalRunId = number;
|
|
33
48
|
/** A resource owned by a prompt run until its promise settles. */
|
|
@@ -257,7 +272,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
257
272
|
* current when `AgentLoopConfig` was first constructed). Overrides the static
|
|
258
273
|
* `metadata` field when present.
|
|
259
274
|
*/
|
|
260
|
-
metadataResolver?: (
|
|
275
|
+
metadataResolver?: (context: AgentMetadataResolverContext) => Record<string, unknown> | undefined;
|
|
261
276
|
|
|
262
277
|
/**
|
|
263
278
|
* Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.
|