@gajae-code/agent-core 0.16.7 → 0.17.1
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 +15 -0
- package/dist/types/agent.d.ts +1 -1
- package/dist/types/compaction/branch-summarization.d.ts +6 -0
- package/dist/types/compaction/compaction.d.ts +12 -0
- package/dist/types/types.d.ts +2 -2
- package/package.json +4 -4
- package/src/agent-loop.ts +89 -20
- package/src/agent.ts +89 -22
- package/src/compaction/branch-summarization.ts +7 -0
- package/src/compaction/compaction.ts +16 -0
- package/src/types.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.17.1] - 2026-09-17
|
|
6
|
+
|
|
7
|
+
## [0.17.0] - 2026-09-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Maintenance LLM calls (compaction, handoff, branch summaries) forward a `maintenanceCall: true` marker to the provider. Agent-level providers such as Devin over ACP use it to refuse work they cannot serve instead of forwarding a summarization prompt to a billed upstream agent.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Lossless, managed, abort-path, and safety-stop assistant-message rebuilds now carry the in-process provider-resolved tool-call marker across the boundary. The marker is a symbol, so a deep snapshot silently dropped it, and the loop would then dispatch a provider-executed tool call to a local tool of the same display name — or, on an aborted turn, fabricate a failed tool result for it. This affected Cursor's exec-owned calls and the new Devin ACP provider alike. The marker is registered with `Symbol.for` and restoration is fail-closed on ambiguous identity.
|
|
16
|
+
|
|
17
|
+
- Publish reasoning summaries, thinking, and tool-call updates during ordinary unmanaged streaming instead of holding them until text, response completion, or interruption. Tool execution still waits for terminal validation; already-published legacy guarded calls receive explicit rejection instead of silent resampling. Managed fallback attempts remain atomic.
|
|
18
|
+
- Provisional streaming consumers can reject an escaped-non-ASCII tool-call turn after earlier updates without publishing its terminal message, allowing AgentSession to resample the turn without persisting defective provider metadata.
|
|
19
|
+
|
|
5
20
|
## [0.16.7] - 2026-09-13
|
|
6
21
|
|
|
7
22
|
## [0.16.6] - 2026-09-07
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -378,7 +378,7 @@ export declare class Agent {
|
|
|
378
378
|
setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void;
|
|
379
379
|
setRawSseEventInterceptor(fn: SimpleStreamOptions["onSseEvent"] | undefined): void;
|
|
380
380
|
setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
|
|
381
|
-
setProvisionalAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) =>
|
|
381
|
+
setProvisionalAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => boolean | undefined) | undefined): void;
|
|
382
382
|
setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
|
|
383
383
|
setShouldPause(fn: AgentLoopConfig["shouldPause"] | undefined): void;
|
|
384
384
|
/** The currently installed cooperative pause checkpoint, if any. */
|
|
@@ -64,6 +64,12 @@ export interface GenerateBranchSummaryOptions {
|
|
|
64
64
|
sessionId?: string;
|
|
65
65
|
/** Opaque provider conversation identity; never derived from branch-summary content. */
|
|
66
66
|
providerSessionId?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Marks this as GJC maintenance work rather than an interactive user turn.
|
|
69
|
+
* Agent-level providers (e.g. `devin-acp`) refuse maintenance calls they
|
|
70
|
+
* cannot serve instead of forwarding them to a billed upstream agent.
|
|
71
|
+
*/
|
|
72
|
+
maintenanceCall?: boolean;
|
|
67
73
|
/** Shared provider state map so the branch summary call reuses session-scoped transport/session caches. */
|
|
68
74
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
69
75
|
/** Hint that websocket transport should be preferred when supported by the provider implementation. */
|
|
@@ -194,6 +194,12 @@ export interface SummaryOptions {
|
|
|
194
194
|
remoteEndpoint?: string;
|
|
195
195
|
remoteInstructions?: string;
|
|
196
196
|
initiatorOverride?: MessageAttribution;
|
|
197
|
+
/**
|
|
198
|
+
* Marks this as GJC maintenance work rather than an interactive user turn.
|
|
199
|
+
* Agent-level providers (e.g. `devin-acp`) refuse maintenance calls they
|
|
200
|
+
* cannot serve instead of forwarding them to a billed upstream agent.
|
|
201
|
+
*/
|
|
202
|
+
maintenanceCall?: boolean;
|
|
197
203
|
metadata?: Record<string, unknown>;
|
|
198
204
|
convertToLlm?: ConvertToLlm;
|
|
199
205
|
/**
|
|
@@ -252,6 +258,12 @@ export interface HandoffOptions {
|
|
|
252
258
|
promptExtension?: string;
|
|
253
259
|
convertToLlm?: ConvertToLlm;
|
|
254
260
|
initiatorOverride?: MessageAttribution;
|
|
261
|
+
/**
|
|
262
|
+
* Marks this as GJC maintenance work rather than an interactive user turn.
|
|
263
|
+
* Agent-level providers (e.g. `devin-acp`) refuse maintenance calls they
|
|
264
|
+
* cannot serve instead of forwarding them to a billed upstream agent.
|
|
265
|
+
*/
|
|
266
|
+
maintenanceCall?: boolean;
|
|
255
267
|
metadata?: Record<string, unknown>;
|
|
256
268
|
/**
|
|
257
269
|
* Optional telemetry handle. When provided, the handoff LLM call is
|
package/dist/types/types.d.ts
CHANGED
|
@@ -442,8 +442,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
442
442
|
* Callers may abort synchronously to stop consuming buffered provider events.
|
|
443
443
|
*/
|
|
444
444
|
onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
445
|
-
/** Observe unmanaged provisional assistant deltas before public publication. */
|
|
446
|
-
onProvisionalAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) =>
|
|
445
|
+
/** Observe unmanaged provisional assistant deltas before public publication. Return false to reject the turn. */
|
|
446
|
+
onProvisionalAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => boolean | undefined;
|
|
447
447
|
/** True when the host consumes provisional assistant events for live safety checks. */
|
|
448
448
|
hasProvisionalAssistantMessageEventConsumer?: boolean;
|
|
449
449
|
/** Called for non-content tool-choice incapability stream events. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/agent-core",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.1",
|
|
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.
|
|
36
|
-
"@gajae-code/natives": "0.
|
|
37
|
-
"@gajae-code/utils": "0.
|
|
35
|
+
"@gajae-code/ai": "0.17.1",
|
|
36
|
+
"@gajae-code/natives": "0.17.1",
|
|
37
|
+
"@gajae-code/utils": "0.17.1",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
neutralizeReservedControlTokens,
|
|
35
35
|
stripUnusableReasoningItems,
|
|
36
36
|
} from "@gajae-code/ai/utils";
|
|
37
|
-
import {
|
|
37
|
+
import { copyProviderResolvedToolCall, isProviderResolvedToolCall } from "@gajae-code/ai/utils/block-symbols";
|
|
38
38
|
import {
|
|
39
39
|
attachUnicodeEscapeEvidence,
|
|
40
40
|
type UnicodeEscapeEvidence,
|
|
@@ -815,7 +815,12 @@ function managedRetryableFailure(failure: unknown): boolean {
|
|
|
815
815
|
// caller is not authorized to make, and the credential-mutating consumers
|
|
816
816
|
// downstream would block healthy credentials on the way.
|
|
817
817
|
if (trigger.class === "auth") return trigger.authDisposition !== "forbidden";
|
|
818
|
-
return
|
|
818
|
+
return (
|
|
819
|
+
trigger.class === "rate_limit" ||
|
|
820
|
+
trigger.class === "quota" ||
|
|
821
|
+
trigger.class === "credential" ||
|
|
822
|
+
trigger.class === "server"
|
|
823
|
+
);
|
|
819
824
|
}
|
|
820
825
|
|
|
821
826
|
function promoteTypedEmptyResponseStop(message: AssistantMessage): void {
|
|
@@ -874,6 +879,7 @@ function sanitizeProviderSafetyStopProvenance(
|
|
|
874
879
|
return repaired;
|
|
875
880
|
}
|
|
876
881
|
restoreTransientUnicodeEscapeEvidence(rebuilt.content, message);
|
|
882
|
+
restoreProviderResolvedMarkers(rebuilt.content, message);
|
|
877
883
|
return rebuilt;
|
|
878
884
|
}
|
|
879
885
|
const rebuilt = managedAssistantShell(message, model);
|
|
@@ -2027,6 +2033,7 @@ function losslessDetachedClone<T>(value: T): T {
|
|
|
2027
2033
|
"providerCode",
|
|
2028
2034
|
"openaiErrorCode",
|
|
2029
2035
|
"anthropicErrorType",
|
|
2036
|
+
"credentialModelUnavailable",
|
|
2030
2037
|
"retryAfterMs",
|
|
2031
2038
|
"headers",
|
|
2032
2039
|
] as const) {
|
|
@@ -2101,6 +2108,7 @@ function managedAssistantShell(
|
|
|
2101
2108
|
}
|
|
2102
2109
|
const content = rawArray === undefined ? [] : rawArray.flatMap(managedContentBlock);
|
|
2103
2110
|
restoreTransientUnicodeEscapeEvidence(content, value);
|
|
2111
|
+
restoreProviderResolvedMarkers(content, value);
|
|
2104
2112
|
const usage = managedAssistantUsage(managedAttemptSnapshot(managedProperty(source, "usage")));
|
|
2105
2113
|
const api = managedProperty(source, "api");
|
|
2106
2114
|
const provider = managedProperty(source, "provider");
|
|
@@ -2175,6 +2183,52 @@ function managedContentBlock(block: unknown): AssistantMessage["content"] {
|
|
|
2175
2183
|
return normalized ? [normalized] : [];
|
|
2176
2184
|
}
|
|
2177
2185
|
|
|
2186
|
+
/**
|
|
2187
|
+
* Carry the in-process provider-resolved marker across a managed snapshot.
|
|
2188
|
+
*
|
|
2189
|
+
* A managed shell deep-snapshots content, and symbol keys do not survive that.
|
|
2190
|
+
* Losing the marker would let the loop dispatch a tool call the provider already
|
|
2191
|
+
* executed, so it is re-attached by tool-call identity (`id` plus `name`), the
|
|
2192
|
+
* same way transient unicode-escape evidence is restored.
|
|
2193
|
+
*/
|
|
2194
|
+
function restoreProviderResolvedMarkers(destination: AssistantMessage["content"], source: unknown): void {
|
|
2195
|
+
const sourceContent = managedProperty(source, "content");
|
|
2196
|
+
if (!Array.isArray(sourceContent)) return;
|
|
2197
|
+
const marked = sourceContent.filter(
|
|
2198
|
+
(block): block is object => typeof block === "object" && block !== null && isProviderResolvedToolCall(block),
|
|
2199
|
+
);
|
|
2200
|
+
if (marked.length === 0) return;
|
|
2201
|
+
for (const destinationBlock of destination) {
|
|
2202
|
+
if (destinationBlock.type !== "toolCall") continue;
|
|
2203
|
+
const matches = marked.filter(
|
|
2204
|
+
candidate =>
|
|
2205
|
+
managedProperty(candidate, "id") === destinationBlock.id &&
|
|
2206
|
+
managedProperty(candidate, "name") === destinationBlock.name,
|
|
2207
|
+
);
|
|
2208
|
+
// Ambiguity suppresses every candidate rather than dispatching a call the
|
|
2209
|
+
// provider may already have executed: this predicate is a safety gate.
|
|
2210
|
+
for (const match of matches) copyProviderResolvedToolCall(destinationBlock, match as object);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Detach content the way `structuredClone` does, but keep the in-process
|
|
2216
|
+
* provider-resolved marker. The abort path clamps partial content and then
|
|
2217
|
+
* filters provider-resolved calls out of the synthesized aborted results, so a
|
|
2218
|
+
* plain structured clone would fabricate a failed tool result for a call the
|
|
2219
|
+
* provider already ran.
|
|
2220
|
+
*/
|
|
2221
|
+
function cloneContentPreservingProviderResolved(content: AssistantMessage["content"]): AssistantMessage["content"] {
|
|
2222
|
+
const cloned = structuredClone(content) as AssistantMessage["content"];
|
|
2223
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
2224
|
+
const source = content[index];
|
|
2225
|
+
const target = cloned[index];
|
|
2226
|
+
if (source?.type !== "toolCall" || target?.type !== "toolCall") continue;
|
|
2227
|
+
copyProviderResolvedToolCall(target, source);
|
|
2228
|
+
}
|
|
2229
|
+
return cloned;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2178
2232
|
function managedUnicodeEscapeEvidence(value: unknown): UnicodeEscapeEvidence | undefined {
|
|
2179
2233
|
if (!isManagedPlainRecord(value)) return undefined;
|
|
2180
2234
|
const envelopeReads = Object.fromEntries(
|
|
@@ -2618,6 +2672,7 @@ class ManagedAttemptTransaction {
|
|
|
2618
2672
|
#lastStagedShape: { stagedEventCount: number; stagedBytes: number; contentBlockCount: number } | undefined;
|
|
2619
2673
|
#discarded = false;
|
|
2620
2674
|
#committed = false;
|
|
2675
|
+
#rejected = false;
|
|
2621
2676
|
#degradedFieldDiagnostics = new Set<string>();
|
|
2622
2677
|
|
|
2623
2678
|
constructor(
|
|
@@ -2806,6 +2861,14 @@ class ManagedAttemptTransaction {
|
|
|
2806
2861
|
return this.#committed;
|
|
2807
2862
|
}
|
|
2808
2863
|
|
|
2864
|
+
reject(): void {
|
|
2865
|
+
this.#rejected = true;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
get rejected(): boolean {
|
|
2869
|
+
return this.#rejected;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2809
2872
|
acceptedAssistantSnapshot(message: AssistantMessage): AssistantMessage {
|
|
2810
2873
|
const sourceContent = Array.isArray(message.content) ? message.content : [];
|
|
2811
2874
|
const sourceMetadata: Array<EscapedToolCallMetadata | undefined> = [];
|
|
@@ -3167,6 +3230,7 @@ class ManagedAttemptTransaction {
|
|
|
3167
3230
|
managedProperty(snapshot, "content") as AssistantMessage["content"],
|
|
3168
3231
|
value,
|
|
3169
3232
|
);
|
|
3233
|
+
restoreProviderResolvedMarkers(managedProperty(snapshot, "content") as AssistantMessage["content"], value);
|
|
3170
3234
|
}
|
|
3171
3235
|
return snapshot;
|
|
3172
3236
|
}
|
|
@@ -4042,7 +4106,7 @@ async function runLoopBody(
|
|
|
4042
4106
|
message.stopReason !== "error" &&
|
|
4043
4107
|
message.stopReason !== "aborted" &&
|
|
4044
4108
|
escapedNonAsciiResampleAttempt < MAX_ESCAPED_NONASCII_RESAMPLES &&
|
|
4045
|
-
!escapedToolTransaction?.committed &&
|
|
4109
|
+
(!escapedToolTransaction?.committed || escapedToolTransaction.rejected) &&
|
|
4046
4110
|
hasEscapedNonAsciiToolCall(message)
|
|
4047
4111
|
) {
|
|
4048
4112
|
escapedNonAsciiResampleAttempt++;
|
|
@@ -4199,7 +4263,7 @@ async function runLoopBody(
|
|
|
4199
4263
|
// This maintains the tool_use/tool_result pairing that the API requires
|
|
4200
4264
|
type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
|
|
4201
4265
|
const toolCalls = message.content.filter(
|
|
4202
|
-
(c): c is ToolCallContent => c.type === "toolCall" && !
|
|
4266
|
+
(c): c is ToolCallContent => c.type === "toolCall" && !isProviderResolvedToolCall(c),
|
|
4203
4267
|
);
|
|
4204
4268
|
const toolResults: ToolResultMessage[] = [];
|
|
4205
4269
|
for (const toolCall of toolCalls) {
|
|
@@ -4232,7 +4296,7 @@ async function runLoopBody(
|
|
|
4232
4296
|
// Check for tool calls
|
|
4233
4297
|
type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
|
|
4234
4298
|
const toolCalls = message.content.filter(
|
|
4235
|
-
(c): c is ToolCallContent => c.type === "toolCall" && !
|
|
4299
|
+
(c): c is ToolCallContent => c.type === "toolCall" && !isProviderResolvedToolCall(c),
|
|
4236
4300
|
);
|
|
4237
4301
|
hasMoreToolCalls = toolCalls.length > 0;
|
|
4238
4302
|
|
|
@@ -4813,17 +4877,20 @@ async function streamAssistantResponse(
|
|
|
4813
4877
|
const event = next.value;
|
|
4814
4878
|
|
|
4815
4879
|
switch (event.type) {
|
|
4816
|
-
case "start":
|
|
4880
|
+
case "start": {
|
|
4817
4881
|
partialMessage = config.fallbackManaged
|
|
4818
4882
|
? managedAssistantShell(event.partial, config.model, managedDegradedFieldDiagnostics)
|
|
4819
4883
|
: event.partial;
|
|
4820
4884
|
context.messages.push(partialMessage);
|
|
4821
4885
|
addedPartial = true;
|
|
4886
|
+
let acceptedStart = true;
|
|
4822
4887
|
if (provisionalToolTransaction) {
|
|
4823
|
-
config.onProvisionalAssistantMessageEvent?.(partialMessage, event);
|
|
4888
|
+
acceptedStart = config.onProvisionalAssistantMessageEvent?.(partialMessage, event) !== false;
|
|
4889
|
+
if (!acceptedStart) provisionalToolTransaction.reject();
|
|
4824
4890
|
}
|
|
4825
|
-
stream.push({ type: "message_start", message: { ...partialMessage }, scope });
|
|
4891
|
+
if (acceptedStart) stream.push({ type: "message_start", message: { ...partialMessage }, scope });
|
|
4826
4892
|
break;
|
|
4893
|
+
}
|
|
4827
4894
|
|
|
4828
4895
|
case "toolChoiceIncapability":
|
|
4829
4896
|
config.onToolChoiceIncapability?.(event);
|
|
@@ -4856,26 +4923,28 @@ async function streamAssistantResponse(
|
|
|
4856
4923
|
? managedAssistantEventSnapshot(event, partialMessage, managedDegradedFieldDiagnostics)
|
|
4857
4924
|
: event;
|
|
4858
4925
|
context.messages[context.messages.length - 1] = partialMessage;
|
|
4926
|
+
let acceptedUpdate = true;
|
|
4859
4927
|
if (provisionalToolTransaction) {
|
|
4860
|
-
|
|
4861
|
-
|
|
4928
|
+
acceptedUpdate =
|
|
4929
|
+
config.onProvisionalAssistantMessageEvent?.(partialMessage, partialEvent) !== false;
|
|
4930
|
+
if (!acceptedUpdate) provisionalToolTransaction.reject();
|
|
4931
|
+
if (acceptedUpdate)
|
|
4932
|
+
provisionalToolTransaction.stageAssistantMessageEvent(partialMessage, partialEvent);
|
|
4862
4933
|
} else {
|
|
4863
4934
|
config.onAssistantMessageEvent?.(partialMessage, partialEvent);
|
|
4864
4935
|
}
|
|
4865
|
-
if (signal?.aborted) continue;
|
|
4936
|
+
if (!acceptedUpdate || signal?.aborted) continue;
|
|
4866
4937
|
stream.push({
|
|
4867
4938
|
type: "message_update",
|
|
4868
4939
|
assistantMessageEvent: partialEvent,
|
|
4869
4940
|
message: { ...partialMessage },
|
|
4870
4941
|
scope,
|
|
4871
4942
|
});
|
|
4872
|
-
//
|
|
4873
|
-
//
|
|
4874
|
-
//
|
|
4875
|
-
//
|
|
4876
|
-
|
|
4877
|
-
provisionalToolTransaction?.commitCallbacksAndUpdates();
|
|
4878
|
-
}
|
|
4943
|
+
// Publish unmanaged content as it arrives, including reasoning and
|
|
4944
|
+
// tool arguments. Publication does not accept or execute tools:
|
|
4945
|
+
// terminal validation and the consumer drain still precede dispatch.
|
|
4946
|
+
// A published escaped call is rejected rather than silently resampled.
|
|
4947
|
+
provisionalToolTransaction?.commitCallbacksAndUpdates();
|
|
4879
4948
|
}
|
|
4880
4949
|
break;
|
|
4881
4950
|
|
|
@@ -4935,7 +5004,7 @@ function emitAbortedAssistantMessage(
|
|
|
4935
5004
|
const now = Date.now();
|
|
4936
5005
|
const abortedMessage: AssistantMessage = {
|
|
4937
5006
|
role: "assistant",
|
|
4938
|
-
content: partialMessage ?
|
|
5007
|
+
content: partialMessage ? cloneContentPreservingProviderResolved(partialMessage.content) : [],
|
|
4939
5008
|
api: config.model.api,
|
|
4940
5009
|
provider: config.model.provider,
|
|
4941
5010
|
model: config.model.id,
|
|
@@ -5031,7 +5100,7 @@ async function executeToolCalls(
|
|
|
5031
5100
|
} = config;
|
|
5032
5101
|
type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
|
|
5033
5102
|
const toolCalls = assistantMessage.content.filter(
|
|
5034
|
-
(c): c is ToolCallContent => c.type === "toolCall" && !
|
|
5103
|
+
(c): c is ToolCallContent => c.type === "toolCall" && !isProviderResolvedToolCall(c),
|
|
5035
5104
|
);
|
|
5036
5105
|
const emittedToolResults: ToolResultMessage[] = [];
|
|
5037
5106
|
const toolCallInfos = toolCalls.map(call => ({ id: call.id, name: call.name }));
|
package/src/agent.ts
CHANGED
|
@@ -542,7 +542,10 @@ export class Agent {
|
|
|
542
542
|
#onResponse?: SimpleStreamOptions["onResponse"];
|
|
543
543
|
#onSseEvent?: SimpleStreamOptions["onSseEvent"];
|
|
544
544
|
#onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
545
|
-
#onProvisionalAssistantMessageEvent?: (
|
|
545
|
+
#onProvisionalAssistantMessageEvent?: (
|
|
546
|
+
message: AssistantMessage,
|
|
547
|
+
event: AssistantMessageEvent,
|
|
548
|
+
) => boolean | undefined;
|
|
546
549
|
#onToolChoiceIncapability?: AgentLoopConfig["onToolChoiceIncapability"];
|
|
547
550
|
#onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
|
|
548
551
|
#onBeforeYield?: () => Promise<void> | void;
|
|
@@ -928,7 +931,7 @@ export class Agent {
|
|
|
928
931
|
}
|
|
929
932
|
|
|
930
933
|
setProvisionalAssistantMessageEventInterceptor(
|
|
931
|
-
fn: ((message: AssistantMessage, event: AssistantMessageEvent) =>
|
|
934
|
+
fn: ((message: AssistantMessage, event: AssistantMessageEvent) => boolean | undefined) | undefined,
|
|
932
935
|
): void {
|
|
933
936
|
this.#onProvisionalAssistantMessageEvent = fn;
|
|
934
937
|
}
|
|
@@ -1030,81 +1033,145 @@ export class Agent {
|
|
|
1030
1033
|
// `this === undefined`, throwing "undefined is not an object (this.#optionsForCall)".
|
|
1031
1034
|
const read = source.read?.bind(source);
|
|
1032
1035
|
if (read) {
|
|
1033
|
-
guarded.read = async args => {
|
|
1036
|
+
guarded.read = async (args, signal, markNonAbortable) => {
|
|
1034
1037
|
this.#assertActiveRun(runId);
|
|
1035
|
-
const result = await read(args);
|
|
1038
|
+
const result = await read(args, signal, markNonAbortable);
|
|
1036
1039
|
this.#assertActiveRun(runId);
|
|
1037
1040
|
return result;
|
|
1038
1041
|
};
|
|
1039
1042
|
}
|
|
1040
1043
|
const ls = source.ls?.bind(source);
|
|
1041
1044
|
if (ls) {
|
|
1042
|
-
guarded.ls = async args => {
|
|
1045
|
+
guarded.ls = async (args, signal, markNonAbortable) => {
|
|
1043
1046
|
this.#assertActiveRun(runId);
|
|
1044
|
-
const result = await ls(args);
|
|
1047
|
+
const result = await ls(args, signal, markNonAbortable);
|
|
1045
1048
|
this.#assertActiveRun(runId);
|
|
1046
1049
|
return result;
|
|
1047
1050
|
};
|
|
1048
1051
|
}
|
|
1049
1052
|
const grep = source.grep?.bind(source);
|
|
1050
1053
|
if (grep) {
|
|
1051
|
-
guarded.grep = async args => {
|
|
1054
|
+
guarded.grep = async (args, signal) => {
|
|
1052
1055
|
this.#assertActiveRun(runId);
|
|
1053
|
-
const result = await grep(args);
|
|
1056
|
+
const result = await grep(args, signal);
|
|
1054
1057
|
this.#assertActiveRun(runId);
|
|
1055
1058
|
return result;
|
|
1056
1059
|
};
|
|
1057
1060
|
}
|
|
1058
1061
|
const write = source.write?.bind(source);
|
|
1059
1062
|
if (write) {
|
|
1060
|
-
guarded.write = async args => {
|
|
1063
|
+
guarded.write = async (args, signal, markNonAbortable) => {
|
|
1061
1064
|
this.#assertActiveRun(runId);
|
|
1062
|
-
const result = await write(args);
|
|
1065
|
+
const result = await write(args, signal, markNonAbortable);
|
|
1063
1066
|
this.#assertActiveRun(runId);
|
|
1064
1067
|
return result;
|
|
1065
1068
|
};
|
|
1066
1069
|
}
|
|
1067
1070
|
const deleteHandler = source.delete?.bind(source);
|
|
1068
1071
|
if (deleteHandler) {
|
|
1069
|
-
guarded.delete = async args => {
|
|
1072
|
+
guarded.delete = async (args, signal, markNonAbortable) => {
|
|
1070
1073
|
this.#assertActiveRun(runId);
|
|
1071
|
-
const result = await deleteHandler(args);
|
|
1074
|
+
const result = await deleteHandler(args, signal, markNonAbortable);
|
|
1072
1075
|
this.#assertActiveRun(runId);
|
|
1073
1076
|
return result;
|
|
1074
1077
|
};
|
|
1075
1078
|
}
|
|
1076
1079
|
const shell = source.shell?.bind(source);
|
|
1077
1080
|
if (shell) {
|
|
1078
|
-
guarded.shell = async args => {
|
|
1081
|
+
guarded.shell = async (args, signal, markNonAbortable) => {
|
|
1079
1082
|
this.#assertActiveRun(runId);
|
|
1080
|
-
const result = await shell(args);
|
|
1083
|
+
const result = await shell(args, signal, markNonAbortable);
|
|
1081
1084
|
this.#assertActiveRun(runId);
|
|
1082
1085
|
return result;
|
|
1083
1086
|
};
|
|
1084
1087
|
}
|
|
1085
1088
|
const shellStream = source.shellStream?.bind(source);
|
|
1086
1089
|
if (shellStream) {
|
|
1087
|
-
guarded.shellStream = async (args, callbacks) => {
|
|
1090
|
+
guarded.shellStream = async (args, callbacks, signal, markNonAbortable) => {
|
|
1088
1091
|
this.#assertActiveRun(runId);
|
|
1089
|
-
const result = await shellStream(args, callbacks);
|
|
1092
|
+
const result = await shellStream(args, callbacks, signal, markNonAbortable);
|
|
1090
1093
|
this.#assertActiveRun(runId);
|
|
1091
1094
|
return result;
|
|
1092
1095
|
};
|
|
1093
1096
|
}
|
|
1094
1097
|
const diagnostics = source.diagnostics?.bind(source);
|
|
1095
1098
|
if (diagnostics) {
|
|
1096
|
-
guarded.diagnostics = async args => {
|
|
1099
|
+
guarded.diagnostics = async (args, signal) => {
|
|
1097
1100
|
this.#assertActiveRun(runId);
|
|
1098
|
-
const result = await diagnostics(args);
|
|
1101
|
+
const result = await diagnostics(args, signal);
|
|
1099
1102
|
this.#assertActiveRun(runId);
|
|
1100
1103
|
return result;
|
|
1101
1104
|
};
|
|
1102
1105
|
}
|
|
1103
1106
|
const mcp = source.mcp?.bind(source);
|
|
1104
1107
|
if (mcp) {
|
|
1105
|
-
guarded.mcp = async call => {
|
|
1108
|
+
guarded.mcp = async (call, signal, markNonAbortable) => {
|
|
1109
|
+
this.#assertActiveRun(runId);
|
|
1110
|
+
const result = await mcp(call, signal, markNonAbortable);
|
|
1111
|
+
this.#assertActiveRun(runId);
|
|
1112
|
+
return result;
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
// Preserve Pi handlers end-to-end (probepark blocker): the production Agent guard previously dropped every pi* handler.
|
|
1116
|
+
const piRead = source.piRead?.bind(source);
|
|
1117
|
+
if (piRead) {
|
|
1118
|
+
guarded.piRead = async call => {
|
|
1119
|
+
this.#assertActiveRun(runId);
|
|
1120
|
+
const result = await piRead(call);
|
|
1121
|
+
this.#assertActiveRun(runId);
|
|
1122
|
+
return result;
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const piBash = source.piBash?.bind(source);
|
|
1126
|
+
if (piBash) {
|
|
1127
|
+
guarded.piBash = async call => {
|
|
1128
|
+
this.#assertActiveRun(runId);
|
|
1129
|
+
const result = await piBash(call);
|
|
1130
|
+
this.#assertActiveRun(runId);
|
|
1131
|
+
return result;
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
const piEdit = source.piEdit?.bind(source);
|
|
1135
|
+
if (piEdit) {
|
|
1136
|
+
guarded.piEdit = async call => {
|
|
1137
|
+
this.#assertActiveRun(runId);
|
|
1138
|
+
const result = await piEdit(call);
|
|
1139
|
+
this.#assertActiveRun(runId);
|
|
1140
|
+
return result;
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
const piWrite = source.piWrite?.bind(source);
|
|
1144
|
+
if (piWrite) {
|
|
1145
|
+
guarded.piWrite = async call => {
|
|
1146
|
+
this.#assertActiveRun(runId);
|
|
1147
|
+
const result = await piWrite(call);
|
|
1148
|
+
this.#assertActiveRun(runId);
|
|
1149
|
+
return result;
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const piGrep = source.piGrep?.bind(source);
|
|
1153
|
+
if (piGrep) {
|
|
1154
|
+
guarded.piGrep = async call => {
|
|
1106
1155
|
this.#assertActiveRun(runId);
|
|
1107
|
-
const result = await
|
|
1156
|
+
const result = await piGrep(call);
|
|
1157
|
+
this.#assertActiveRun(runId);
|
|
1158
|
+
return result;
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
const piFind = source.piFind?.bind(source);
|
|
1162
|
+
if (piFind) {
|
|
1163
|
+
guarded.piFind = async call => {
|
|
1164
|
+
this.#assertActiveRun(runId);
|
|
1165
|
+
const result = await piFind(call);
|
|
1166
|
+
this.#assertActiveRun(runId);
|
|
1167
|
+
return result;
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
const piLs = source.piLs?.bind(source);
|
|
1171
|
+
if (piLs) {
|
|
1172
|
+
guarded.piLs = async call => {
|
|
1173
|
+
this.#assertActiveRun(runId);
|
|
1174
|
+
const result = await piLs(call);
|
|
1108
1175
|
this.#assertActiveRun(runId);
|
|
1109
1176
|
return result;
|
|
1110
1177
|
};
|
|
@@ -1984,9 +2051,9 @@ export class Agent {
|
|
|
1984
2051
|
this.#onAssistantMessageEvent?.(message, event);
|
|
1985
2052
|
},
|
|
1986
2053
|
onProvisionalAssistantMessageEvent: (message, event) => {
|
|
1987
|
-
if (this.#activeRunId !== runId) return;
|
|
2054
|
+
if (this.#activeRunId !== runId) return false;
|
|
1988
2055
|
this.#state.streamMessage = message;
|
|
1989
|
-
this.#onProvisionalAssistantMessageEvent?.(message, event);
|
|
2056
|
+
return this.#onProvisionalAssistantMessageEvent?.(message, event);
|
|
1990
2057
|
},
|
|
1991
2058
|
hasProvisionalAssistantMessageEventConsumer: this.#onProvisionalAssistantMessageEvent !== undefined,
|
|
1992
2059
|
onToolChoiceIncapability: this.#onToolChoiceIncapability
|
|
@@ -93,6 +93,12 @@ export interface GenerateBranchSummaryOptions {
|
|
|
93
93
|
sessionId?: string;
|
|
94
94
|
/** Opaque provider conversation identity; never derived from branch-summary content. */
|
|
95
95
|
providerSessionId?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Marks this as GJC maintenance work rather than an interactive user turn.
|
|
98
|
+
* Agent-level providers (e.g. `devin-acp`) refuse maintenance calls they
|
|
99
|
+
* cannot serve instead of forwarding them to a billed upstream agent.
|
|
100
|
+
*/
|
|
101
|
+
maintenanceCall?: boolean;
|
|
96
102
|
/** Shared provider state map so the branch summary call reuses session-scoped transport/session caches. */
|
|
97
103
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
98
104
|
/** Hint that websocket transport should be preferred when supported by the provider implementation. */
|
|
@@ -337,6 +343,7 @@ export async function generateBranchSummary(
|
|
|
337
343
|
sessionId,
|
|
338
344
|
providerSessionId,
|
|
339
345
|
providerSessionState,
|
|
346
|
+
maintenanceCall: options.maintenanceCall,
|
|
340
347
|
preferWebsockets,
|
|
341
348
|
},
|
|
342
349
|
{ telemetry: options.telemetry, oneshotKind: "branch_summary" },
|
|
@@ -868,6 +868,12 @@ export interface SummaryOptions {
|
|
|
868
868
|
remoteEndpoint?: string;
|
|
869
869
|
remoteInstructions?: string;
|
|
870
870
|
initiatorOverride?: MessageAttribution;
|
|
871
|
+
/**
|
|
872
|
+
* Marks this as GJC maintenance work rather than an interactive user turn.
|
|
873
|
+
* Agent-level providers (e.g. `devin-acp`) refuse maintenance calls they
|
|
874
|
+
* cannot serve instead of forwarding them to a billed upstream agent.
|
|
875
|
+
*/
|
|
876
|
+
maintenanceCall?: boolean;
|
|
871
877
|
metadata?: Record<string, unknown>;
|
|
872
878
|
convertToLlm?: ConvertToLlm;
|
|
873
879
|
/**
|
|
@@ -1026,6 +1032,7 @@ export async function generateSummary(
|
|
|
1026
1032
|
apiKey,
|
|
1027
1033
|
reasoning: maintenanceReasoning(model),
|
|
1028
1034
|
initiatorOverride: options?.initiatorOverride,
|
|
1035
|
+
maintenanceCall: options?.maintenanceCall,
|
|
1029
1036
|
metadata: options?.metadata,
|
|
1030
1037
|
sessionId: options?.sessionId,
|
|
1031
1038
|
providerSessionId: options?.providerSessionId,
|
|
@@ -1065,6 +1072,12 @@ export interface HandoffOptions {
|
|
|
1065
1072
|
promptExtension?: string;
|
|
1066
1073
|
convertToLlm?: ConvertToLlm;
|
|
1067
1074
|
initiatorOverride?: MessageAttribution;
|
|
1075
|
+
/**
|
|
1076
|
+
* Marks this as GJC maintenance work rather than an interactive user turn.
|
|
1077
|
+
* Agent-level providers (e.g. `devin-acp`) refuse maintenance calls they
|
|
1078
|
+
* cannot serve instead of forwarding them to a billed upstream agent.
|
|
1079
|
+
*/
|
|
1080
|
+
maintenanceCall?: boolean;
|
|
1068
1081
|
metadata?: Record<string, unknown>;
|
|
1069
1082
|
/**
|
|
1070
1083
|
* Optional telemetry handle. When provided, the handoff LLM call is
|
|
@@ -1124,6 +1137,7 @@ export async function generateHandoff(
|
|
|
1124
1137
|
reasoning: maintenanceReasoning(model),
|
|
1125
1138
|
toolChoice: "none",
|
|
1126
1139
|
initiatorOverride: options.initiatorOverride,
|
|
1140
|
+
maintenanceCall: options.maintenanceCall,
|
|
1127
1141
|
metadata: options.metadata,
|
|
1128
1142
|
sessionId: options.sessionId,
|
|
1129
1143
|
providerSessionId: options.providerSessionId,
|
|
@@ -1397,6 +1411,7 @@ export async function compact(
|
|
|
1397
1411
|
remoteEndpoint: settings.remoteEnabled === false ? undefined : settings.remoteEndpoint,
|
|
1398
1412
|
remoteInstructions: options?.remoteInstructions,
|
|
1399
1413
|
initiatorOverride: options?.initiatorOverride,
|
|
1414
|
+
maintenanceCall: options?.maintenanceCall,
|
|
1400
1415
|
metadata: options?.metadata,
|
|
1401
1416
|
convertToLlm: options?.convertToLlm,
|
|
1402
1417
|
telemetry: options?.telemetry,
|
|
@@ -1574,6 +1589,7 @@ async function generateTurnPrefixSummary(
|
|
|
1574
1589
|
apiKey,
|
|
1575
1590
|
reasoning: maintenanceReasoning(model),
|
|
1576
1591
|
initiatorOverride: options?.initiatorOverride,
|
|
1592
|
+
maintenanceCall: options?.maintenanceCall,
|
|
1577
1593
|
metadata: options?.metadata,
|
|
1578
1594
|
sessionId: options?.sessionId,
|
|
1579
1595
|
providerSessionId: options?.providerSessionId,
|
package/src/types.ts
CHANGED
|
@@ -485,8 +485,11 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
485
485
|
* Callers may abort synchronously to stop consuming buffered provider events.
|
|
486
486
|
*/
|
|
487
487
|
onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
488
|
-
/** Observe unmanaged provisional assistant deltas before public publication. */
|
|
489
|
-
onProvisionalAssistantMessageEvent?: (
|
|
488
|
+
/** Observe unmanaged provisional assistant deltas before public publication. Return false to reject the turn. */
|
|
489
|
+
onProvisionalAssistantMessageEvent?: (
|
|
490
|
+
message: AssistantMessage,
|
|
491
|
+
event: AssistantMessageEvent,
|
|
492
|
+
) => boolean | undefined;
|
|
490
493
|
/** True when the host consumes provisional assistant events for live safety checks. */
|
|
491
494
|
hasProvisionalAssistantMessageEventConsumer?: boolean;
|
|
492
495
|
|