@gajae-code/agent-core 0.13.3 → 0.14.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 +21 -0
- package/dist/types/agent-loop.d.ts +2 -1
- package/dist/types/agent.d.ts +36 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/tool-dispatch-identity.d.ts +27 -0
- package/dist/types/types.d.ts +11 -0
- package/package.json +4 -4
- package/src/agent-loop.ts +805 -161
- package/src/agent.ts +116 -18
- package/src/index.ts +2 -0
- package/src/proxy.ts +3 -1
- package/src/tool-dispatch-identity.ts +87 -0
- package/src/types.ts +11 -0
package/src/agent.ts
CHANGED
|
@@ -301,6 +301,8 @@ export interface AgentOptions {
|
|
|
301
301
|
* message are emitted. See {@link AgentLoopConfig.afterToolCall} for full semantics.
|
|
302
302
|
*/
|
|
303
303
|
afterToolCall?: AgentLoopConfig["afterToolCall"];
|
|
304
|
+
/** Invoked with the follow-up messages dequeued for the next turn (reassignable). */
|
|
305
|
+
onFollowUpConsumed?: AgentLoopConfig["onFollowUpConsumed"];
|
|
304
306
|
|
|
305
307
|
/**
|
|
306
308
|
* Opt-in OpenTelemetry instrumentation. Passing `{}` enables the loop's
|
|
@@ -322,8 +324,7 @@ export interface AgentPromptOptions {
|
|
|
322
324
|
/** Continue a cooperative maintenance checkpoint under its existing logical run and cancellation domain. */
|
|
323
325
|
maintenanceContinuation?: boolean;
|
|
324
326
|
/** Called synchronously after this invocation claims the agent run, before asynchronous provider work. */
|
|
325
|
-
|
|
326
|
-
onRunAccepted?: (...args: any[]) => void;
|
|
327
|
+
onRunAccepted?: (handle: AttemptRunHandle, acceptance: { consumedQueuedMessages: readonly AgentMessage[] }) => void;
|
|
327
328
|
/** Called once immediately before every managed upstream request. */
|
|
328
329
|
nextFallbackAttempt?: AgentLoopConfig["nextFallbackAttempt"];
|
|
329
330
|
/** Called after a managed upstream request is accepted and committed. */
|
|
@@ -410,6 +411,7 @@ export class Agent {
|
|
|
410
411
|
#onResponse?: SimpleStreamOptions["onResponse"];
|
|
411
412
|
#onSseEvent?: SimpleStreamOptions["onSseEvent"];
|
|
412
413
|
#onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
414
|
+
#onProvisionalAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
413
415
|
#onToolChoiceIncapability?: AgentLoopConfig["onToolChoiceIncapability"];
|
|
414
416
|
#onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
|
|
415
417
|
#onBeforeYield?: () => Promise<void> | void;
|
|
@@ -467,6 +469,8 @@ export class Agent {
|
|
|
467
469
|
* message emission. Reassign at any time to swap the implementation.
|
|
468
470
|
*/
|
|
469
471
|
afterToolCall?: AgentLoopConfig["afterToolCall"];
|
|
472
|
+
/** Invoked with the follow-up messages dequeued for the next turn. Reassign at any time. */
|
|
473
|
+
onFollowUpConsumed?: AgentLoopConfig["onFollowUpConsumed"];
|
|
470
474
|
|
|
471
475
|
constructor(opts: AgentOptions = {}) {
|
|
472
476
|
this.#state = { ...this.#state, ...opts.initialState };
|
|
@@ -510,6 +514,7 @@ export class Agent {
|
|
|
510
514
|
this.#onHarmonyLeak = opts.onHarmonyLeak;
|
|
511
515
|
this.#shouldPause = opts.shouldPause;
|
|
512
516
|
this.beforeToolCall = opts.beforeToolCall;
|
|
517
|
+
this.onFollowUpConsumed = opts.onFollowUpConsumed;
|
|
513
518
|
this.afterToolCall = opts.afterToolCall;
|
|
514
519
|
this.#telemetry = opts.telemetry;
|
|
515
520
|
this.#appendOnlyContext = opts.appendOnlyContext;
|
|
@@ -780,6 +785,12 @@ export class Agent {
|
|
|
780
785
|
this.#onAssistantMessageEvent = fn;
|
|
781
786
|
}
|
|
782
787
|
|
|
788
|
+
setProvisionalAssistantMessageEventInterceptor(
|
|
789
|
+
fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined,
|
|
790
|
+
): void {
|
|
791
|
+
this.#onProvisionalAssistantMessageEvent = fn;
|
|
792
|
+
}
|
|
793
|
+
|
|
783
794
|
setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void {
|
|
784
795
|
this.#onBeforeYield = fn;
|
|
785
796
|
}
|
|
@@ -792,6 +803,18 @@ export class Agent {
|
|
|
792
803
|
this.#maintainContext = fn;
|
|
793
804
|
}
|
|
794
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Publish an event produced OUTSIDE the agent loop (a provider that executed the tool
|
|
808
|
+
* itself, a host bridge, a replay).
|
|
809
|
+
*
|
|
810
|
+
* Identity is the PRODUCER's to prove: whoever dispatched the call binds the tool object
|
|
811
|
+
* it actually ran (see `bindDispatchedToolIdentity`) before handing the event here, and
|
|
812
|
+
* that binding is never touched from this side. Re-resolving `event.toolName` against the
|
|
813
|
+
* mutable current tool list would let a mid-run `setTools`, MCP reload, or plain name
|
|
814
|
+
* collision overwrite a proven object with one that never ran — and would invent an
|
|
815
|
+
* identity for replays and host bridges that never executed an AgentTool at all. An
|
|
816
|
+
* unbound external event stays unbound; unproven provenance is `custom`.
|
|
817
|
+
*/
|
|
795
818
|
emitExternalEvent(event: AgentEvent) {
|
|
796
819
|
switch (event.type) {
|
|
797
820
|
case "message_start":
|
|
@@ -1191,16 +1214,39 @@ export class Agent {
|
|
|
1191
1214
|
return true;
|
|
1192
1215
|
}
|
|
1193
1216
|
|
|
1194
|
-
/**
|
|
1195
|
-
|
|
1217
|
+
/**
|
|
1218
|
+
* Remove ALL queued STEERING messages without touching the follow-up queue.
|
|
1219
|
+
* Used by the terminal-abort path to purge steering queued for the aborted
|
|
1220
|
+
* turn (the loop may exit on the abort signal without polling it); the
|
|
1221
|
+
* follow-up queue is preserved because it may carry owned-completion
|
|
1222
|
+
* resumes that must still deliver.
|
|
1223
|
+
*/
|
|
1224
|
+
clearSteeringMessages(): void {
|
|
1225
|
+
this.#steeringQueue = [];
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Remove queued steering/follow-up messages matching `predicate`, preserving
|
|
1230
|
+
* order of the rest. `scope` restricts the removal to one queue — the
|
|
1231
|
+
* terminal-abort steering purge must not wipe the follow-up queue, which
|
|
1232
|
+
* the owned-completion resume policy preserves.
|
|
1233
|
+
*/
|
|
1234
|
+
removeQueuedMessages(
|
|
1235
|
+
predicate: (message: AgentMessage) => boolean,
|
|
1236
|
+
scope: "both" | "steering" | "followUp" = "both",
|
|
1237
|
+
): {
|
|
1196
1238
|
steering: number;
|
|
1197
1239
|
followUp: number;
|
|
1198
1240
|
total: number;
|
|
1199
1241
|
} {
|
|
1200
1242
|
const beforeSteering = this.#steeringQueue.length;
|
|
1201
1243
|
const beforeFollowUp = this.#followUpQueue.length;
|
|
1202
|
-
|
|
1203
|
-
|
|
1244
|
+
if (scope !== "followUp") {
|
|
1245
|
+
this.#steeringQueue = this.#steeringQueue.filter(m => !predicate(m));
|
|
1246
|
+
}
|
|
1247
|
+
if (scope !== "steering") {
|
|
1248
|
+
this.#followUpQueue = this.#followUpQueue.filter(m => !predicate(m));
|
|
1249
|
+
}
|
|
1204
1250
|
const steering = beforeSteering - this.#steeringQueue.length;
|
|
1205
1251
|
const followUp = beforeFollowUp - this.#followUpQueue.length;
|
|
1206
1252
|
return { steering, followUp, total: steering + followUp };
|
|
@@ -1409,13 +1455,28 @@ export class Agent {
|
|
|
1409
1455
|
if (messages[messages.length - 1].role === "assistant") {
|
|
1410
1456
|
const queuedSteering = this.#dequeueSteeringMessages();
|
|
1411
1457
|
if (queuedSteering.length > 0) {
|
|
1412
|
-
await this.#runLoop(queuedSteering, {
|
|
1458
|
+
await this.#runLoop(queuedSteering, {
|
|
1459
|
+
...options,
|
|
1460
|
+
skipInitialSteeringPoll: true,
|
|
1461
|
+
consumedQueuedMessages: queuedSteering,
|
|
1462
|
+
});
|
|
1413
1463
|
return;
|
|
1414
1464
|
}
|
|
1415
1465
|
|
|
1416
1466
|
const queuedFollowUp = this.#dequeueFollowUpMessages();
|
|
1417
1467
|
if (queuedFollowUp.length > 0) {
|
|
1418
|
-
|
|
1468
|
+
// Route the DIRECT-dequeue batch through the same consumption hook
|
|
1469
|
+
// the in-loop getFollowUpMessages path uses: denied owned-completion
|
|
1470
|
+
// envelopes are filtered before they reach the loop, and delivered
|
|
1471
|
+
// envelopes settle their registrations — the direct path otherwise
|
|
1472
|
+
// bypasses onFollowUpConsumed entirely (review threads P1/P2).
|
|
1473
|
+
await this.onFollowUpConsumed?.(queuedFollowUp);
|
|
1474
|
+
// The hook can filter the WHOLE batch (every entry denied by a
|
|
1475
|
+
// scope:"owned" abort): starting an empty provider run would
|
|
1476
|
+
// violate the zero-final-call guarantee, so return without
|
|
1477
|
+
// running the loop (review thread P1).
|
|
1478
|
+
if (queuedFollowUp.length === 0) return;
|
|
1479
|
+
await this.#runLoop(queuedFollowUp, { ...options, consumedQueuedMessages: queuedFollowUp });
|
|
1419
1480
|
return;
|
|
1420
1481
|
}
|
|
1421
1482
|
|
|
@@ -1438,12 +1499,31 @@ export class Agent {
|
|
|
1438
1499
|
}
|
|
1439
1500
|
const queuedSteering = this.#dequeueSteeringMessages();
|
|
1440
1501
|
if (queuedSteering.length > 0) {
|
|
1441
|
-
await this.#runLoop(queuedSteering, {
|
|
1502
|
+
await this.#runLoop(queuedSteering, {
|
|
1503
|
+
...options,
|
|
1504
|
+
skipInitialSteeringPoll: true,
|
|
1505
|
+
consumedQueuedMessages: queuedSteering,
|
|
1506
|
+
});
|
|
1442
1507
|
return;
|
|
1443
1508
|
}
|
|
1444
1509
|
const queuedFollowUp = this.#dequeueFollowUpMessages();
|
|
1445
1510
|
if (queuedFollowUp.length > 0) {
|
|
1446
|
-
|
|
1511
|
+
// Route the queued-tail batch through the same consumption hook as the
|
|
1512
|
+
// in-loop getFollowUpMessages path and the assistant-tail continue()
|
|
1513
|
+
// path: denied owned-completion envelopes are filtered before they
|
|
1514
|
+
// reach the loop, and delivered envelopes settle their registrations.
|
|
1515
|
+
// A terminal abort that leaves a tool/result tail and rearms an
|
|
1516
|
+
// authorized owned-completion follow-up reaches this branch, so
|
|
1517
|
+
// bypassing the hook would leak every such job's ownership tuple and
|
|
1518
|
+
// eventually exhaust the bounded ownership registries (review thread
|
|
1519
|
+
// P2).
|
|
1520
|
+
await this.onFollowUpConsumed?.(queuedFollowUp);
|
|
1521
|
+
// The hook can filter the WHOLE batch (every entry denied by a
|
|
1522
|
+
// scope:"owned" abort): starting an empty provider run would violate
|
|
1523
|
+
// the zero-final-call guarantee, so return without running the loop
|
|
1524
|
+
// (review thread P2).
|
|
1525
|
+
if (queuedFollowUp.length === 0) return;
|
|
1526
|
+
await this.#runLoop(queuedFollowUp, { ...options, consumedQueuedMessages: queuedFollowUp });
|
|
1447
1527
|
return;
|
|
1448
1528
|
}
|
|
1449
1529
|
throw new Error("No queued messages to continue");
|
|
@@ -1454,7 +1534,13 @@ export class Agent {
|
|
|
1454
1534
|
* If messages are provided, starts a new conversation turn with those messages.
|
|
1455
1535
|
* Otherwise, continues from existing context.
|
|
1456
1536
|
*/
|
|
1457
|
-
async #runLoop(
|
|
1537
|
+
async #runLoop(
|
|
1538
|
+
messages?: AgentMessage[],
|
|
1539
|
+
options?: AgentPromptOptions & {
|
|
1540
|
+
skipInitialSteeringPoll?: boolean;
|
|
1541
|
+
consumedQueuedMessages?: readonly AgentMessage[];
|
|
1542
|
+
},
|
|
1543
|
+
) {
|
|
1458
1544
|
const model = this.#state.model;
|
|
1459
1545
|
if (!model) throw new Error("No model configured");
|
|
1460
1546
|
|
|
@@ -1503,7 +1589,9 @@ export class Agent {
|
|
|
1503
1589
|
this.#observeMainAttemptScope(scope);
|
|
1504
1590
|
const handle: AttemptRunHandle = { logicalRunId, scope };
|
|
1505
1591
|
this.#runHandles.set(logicalRunId, handle);
|
|
1506
|
-
options?.onRunAccepted?.(handle
|
|
1592
|
+
options?.onRunAccepted?.(handle, {
|
|
1593
|
+
consumedQueuedMessages: options.consumedQueuedMessages ?? [],
|
|
1594
|
+
});
|
|
1507
1595
|
if (startsManagedLogicalRun) {
|
|
1508
1596
|
this.#managedLogicalRunOwner = logicalRunId;
|
|
1509
1597
|
this.#emit({ type: "agent_start", scope });
|
|
@@ -1671,12 +1759,16 @@ export class Agent {
|
|
|
1671
1759
|
return result;
|
|
1672
1760
|
}
|
|
1673
1761
|
: undefined,
|
|
1674
|
-
onAssistantMessageEvent:
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1762
|
+
onAssistantMessageEvent: (message, event) => {
|
|
1763
|
+
if (this.#activeRunId !== runId) return;
|
|
1764
|
+
this.#onAssistantMessageEvent?.(message, event);
|
|
1765
|
+
},
|
|
1766
|
+
onProvisionalAssistantMessageEvent: (message, event) => {
|
|
1767
|
+
if (this.#activeRunId !== runId) return;
|
|
1768
|
+
this.#state.streamMessage = message;
|
|
1769
|
+
this.#onProvisionalAssistantMessageEvent?.(message, event);
|
|
1770
|
+
},
|
|
1771
|
+
hasProvisionalAssistantMessageEventConsumer: this.#onProvisionalAssistantMessageEvent !== undefined,
|
|
1680
1772
|
onToolChoiceIncapability: this.#onToolChoiceIncapability
|
|
1681
1773
|
? event => {
|
|
1682
1774
|
if (this.#activeRunId !== runId) return;
|
|
@@ -1710,6 +1802,9 @@ export class Agent {
|
|
|
1710
1802
|
this.#followUpQueue = [...queued, ...this.#followUpQueue];
|
|
1711
1803
|
return [];
|
|
1712
1804
|
}
|
|
1805
|
+
if (queued.length > 0) {
|
|
1806
|
+
await this.onFollowUpConsumed?.(queued);
|
|
1807
|
+
}
|
|
1713
1808
|
return queued;
|
|
1714
1809
|
},
|
|
1715
1810
|
getSyntheticRecoveryMessage: async () => {
|
|
@@ -1881,6 +1976,9 @@ export class Agent {
|
|
|
1881
1976
|
stopReason: abortController.signal.aborted ? "aborted" : "error",
|
|
1882
1977
|
errorMessage: err?.message || String(err),
|
|
1883
1978
|
errorStatus: extractHttpStatusFromError({ status: err?.errorStatus }) ?? extractHttpStatusFromError(err),
|
|
1979
|
+
...(err?.errorKind === "local_snapshot_failure" || err?.errorKind === "local_buffer_overflow"
|
|
1980
|
+
? { errorKind: err.errorKind as "local_snapshot_failure" | "local_buffer_overflow" }
|
|
1981
|
+
: {}),
|
|
1884
1982
|
timestamp: Date.now(),
|
|
1885
1983
|
} as AgentMessage;
|
|
1886
1984
|
|
package/src/index.ts
CHANGED
|
@@ -17,5 +17,7 @@ export * from "./run-resource-ledger";
|
|
|
17
17
|
export * from "./telemetry";
|
|
18
18
|
// Thinking selectors
|
|
19
19
|
export * from "./thinking";
|
|
20
|
+
// Dispatch-bound tool identity (non-serializable side channel keyed by event object)
|
|
21
|
+
export * from "./tool-dispatch-identity";
|
|
20
22
|
// Types
|
|
21
23
|
export * from "./types";
|
package/src/proxy.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type ToolCall,
|
|
14
14
|
} from "@gajae-code/ai";
|
|
15
15
|
import { calculateCost } from "@gajae-code/ai/models";
|
|
16
|
-
import { parseStreamingJson } from "@gajae-code/ai/utils/json-parse";
|
|
16
|
+
import { findUnnecessaryUnicodeEscape, parseStreamingJson } from "@gajae-code/ai/utils/json-parse";
|
|
17
17
|
import { readSseJson } from "@gajae-code/utils";
|
|
18
18
|
|
|
19
19
|
// Create stream class matching ProxyMessageEventStream
|
|
@@ -379,6 +379,8 @@ function processProxyEvent(
|
|
|
379
379
|
case "toolcall_end": {
|
|
380
380
|
const content = partial.content[proxyEvent.contentIndex];
|
|
381
381
|
if (content?.type === "toolCall") {
|
|
382
|
+
const raw = (content as { partialJson?: string }).partialJson;
|
|
383
|
+
if (raw && findUnnecessaryUnicodeEscape(raw)) content.escapedNonAsciiArguments = true;
|
|
382
384
|
delete (content as any).partialJson;
|
|
383
385
|
return {
|
|
384
386
|
type: "toolcall_end",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of the tool object a `tool_execution_start` event was produced for.
|
|
3
|
+
*
|
|
4
|
+
* The producer already knows exactly which tool object it dispatched to: the agent loop
|
|
5
|
+
* resolves it once, from the run's immutable tool snapshot, before it emits anything. Any
|
|
6
|
+
* consumer that instead re-resolves `event.toolName` later reads a MUTABLE registry — a
|
|
7
|
+
* mid-run `setTools`, MCP reload, or tool refresh can hand the same wire name to a
|
|
8
|
+
* completely different object — and would then attribute the call to a tool that never
|
|
9
|
+
* ran.
|
|
10
|
+
*
|
|
11
|
+
* The binding is deliberately a side channel rather than an event field:
|
|
12
|
+
*
|
|
13
|
+
* - a tool object exposes `execute`, closures over session state, and its own
|
|
14
|
+
* description/parameters; putting it on `AgentEvent` would make it enumerable, walkable,
|
|
15
|
+
* and reachable by every serializer, wire envelope, and log sink that copies events, and
|
|
16
|
+
* `JSON.stringify(event)` would start emitting tool metadata;
|
|
17
|
+
* - the map is weak on the event object, so the association disappears with the event it
|
|
18
|
+
* describes and pins nothing alive;
|
|
19
|
+
* - it carries no arguments, no results, no call ids, and no timing — only object identity,
|
|
20
|
+
* which is exactly what provenance checks (`WeakSet.has`) need and nothing more.
|
|
21
|
+
*/
|
|
22
|
+
const dispatchedToolByEvent = new WeakMap<object, object>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Record, at the producer boundary, the tool object this event was emitted for.
|
|
26
|
+
*
|
|
27
|
+
* A call with no resolved tool (an unknown name the loop is about to reject, or a call
|
|
28
|
+
* aborted before dispatch) binds nothing: there is no object, so there is nothing to prove.
|
|
29
|
+
*/
|
|
30
|
+
export function bindDispatchedToolIdentity(event: object, tool: object | undefined): void {
|
|
31
|
+
if (!tool) return;
|
|
32
|
+
dispatchedToolByEvent.set(event, tool);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The tool object this event was actually dispatched to, if its producer bound one. */
|
|
36
|
+
export function dispatchedToolIdentity(event: object): object | undefined {
|
|
37
|
+
return dispatchedToolByEvent.get(event);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tool events emitted only to keep the stream's start/end PAIRING intact, for calls that
|
|
42
|
+
* were never dispatched.
|
|
43
|
+
*
|
|
44
|
+
* A skipped or aborted call still has to produce a result, and every consumer downstream
|
|
45
|
+
* is built around results arriving in pairs, so the loop synthesizes the missing start.
|
|
46
|
+
* That is a stream-shape obligation and nothing more: no `execute` ran, no work began, and
|
|
47
|
+
* no time was spent in a tool.
|
|
48
|
+
*
|
|
49
|
+
* Consumers that merely relay or record events are unaffected and must stay unaffected.
|
|
50
|
+
* Consumers that publish a claim about what is RUNNING are not: between the synthetic
|
|
51
|
+
* start and its end, such a consumer would report a tool as active that was never entered.
|
|
52
|
+
*
|
|
53
|
+
* A side channel rather than an `AgentEvent` field, for the same reasons the dispatched
|
|
54
|
+
* identity is one — no wire/schema surface to serialize, copy, or persist — and a `WeakSet`
|
|
55
|
+
* because the mark is meaningful only for the exact event object it was applied to.
|
|
56
|
+
*/
|
|
57
|
+
const nonDispatchedToolEvents = new WeakSet<object>();
|
|
58
|
+
|
|
59
|
+
/** Mark an event as pairing-only: emitted for a call the loop never dispatched. */
|
|
60
|
+
export function markNonDispatchedToolEvent(event: object): void {
|
|
61
|
+
nonDispatchedToolEvents.add(event);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether this exact event was synthesized for a call that never ran. */
|
|
65
|
+
export function isNonDispatchedToolEvent(event: object): boolean {
|
|
66
|
+
return nonDispatchedToolEvents.has(event);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Active tool a call name dispatches to. Tools emitted via OpenAI's custom-tool path
|
|
71
|
+
* (e.g. `apply_patch` on GPT-5) come back under their wire-level name, which may differ
|
|
72
|
+
* from the harness-internal `name`. Match on either, preferring `name` for determinism if
|
|
73
|
+
* both somehow collide.
|
|
74
|
+
*
|
|
75
|
+
* This is the single dispatch-matching rule: execution, external-event identity binding,
|
|
76
|
+
* and every "is this tool callable" check must agree, or a label can describe a tool the
|
|
77
|
+
* call would never have reached.
|
|
78
|
+
*/
|
|
79
|
+
export function activeToolForCallName<T extends { name: string; customWireName?: string }>(
|
|
80
|
+
tools: ReadonlyArray<T> | undefined,
|
|
81
|
+
callName: string,
|
|
82
|
+
): T | undefined {
|
|
83
|
+
return (
|
|
84
|
+
tools?.find(tool => tool.name === callName) ??
|
|
85
|
+
tools?.find(tool => tool.customWireName !== undefined && tool.customWireName === callName)
|
|
86
|
+
);
|
|
87
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -331,6 +331,13 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
331
331
|
* continues with another turn.
|
|
332
332
|
*/
|
|
333
333
|
getFollowUpMessages?: () => Promise<AgentMessage[]>;
|
|
334
|
+
/**
|
|
335
|
+
* Invoked with the follow-up messages the loop dequeues for the next turn
|
|
336
|
+
* (right after {@link getFollowUpMessages}). The consumer may use this to
|
|
337
|
+
* attach per-turn state (e.g. a fresh owned-completion lineage) at actual
|
|
338
|
+
* resume admission rather than when the message was merely queued.
|
|
339
|
+
*/
|
|
340
|
+
onFollowUpConsumed?: (messages: AgentMessage[]) => void;
|
|
334
341
|
/**
|
|
335
342
|
* Supplies one bounded synthetic recovery instruction before the loop would
|
|
336
343
|
* otherwise yield. Unlike a follow-up, it is sent only to the provider and
|
|
@@ -423,6 +430,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
423
430
|
* Callers may abort synchronously to stop consuming buffered provider events.
|
|
424
431
|
*/
|
|
425
432
|
onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
433
|
+
/** Observe unmanaged provisional assistant deltas before public publication. */
|
|
434
|
+
onProvisionalAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
435
|
+
/** True when the host consumes provisional assistant events for live safety checks. */
|
|
436
|
+
hasProvisionalAssistantMessageEventConsumer?: boolean;
|
|
426
437
|
|
|
427
438
|
/** Called for non-content tool-choice incapability stream events. */
|
|
428
439
|
onToolChoiceIncapability?: (event: Extract<AssistantMessageEvent, { type: "toolChoiceIncapability" }>) => void;
|