@bubblebrain-ai/bubble 0.0.19 → 0.0.20
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/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +305 -17
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +28 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/main.js +32 -0
- package/dist/memory/prompts.js +3 -1
- package/dist/model-catalog.js +2 -0
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +0 -1
- package/dist/network/chatgpt-transport.js +40 -121
- package/dist/network/provider-transport.d.ts +32 -0
- package/dist/network/provider-transport.js +265 -0
- package/dist/network/retry.d.ts +29 -0
- package/dist/network/retry.js +88 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +127 -52
- package/dist/provider-openai-codex.js +19 -29
- package/dist/session-log.js +3 -3
- package/dist/slash-commands/commands.js +84 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.js +4 -4
- package/dist/tui/display-history.d.ts +4 -3
- package/dist/tui/display-history.js +34 -57
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/paste-placeholder.d.ts +1 -0
- package/dist/tui/paste-placeholder.js +7 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +260 -155
- package/dist/tui/trace-groups.js +40 -4
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.js +2 -1
- package/dist/tui-ink/trace-groups.js +40 -4
- package/dist/tui-opentui/app.js +2 -1
- package/dist/tui-opentui/trace-groups.js +40 -4
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import type { AssistantProviderMetadata } from "../types.js";
|
|
|
2
2
|
export declare function formatInternalReminderBlock(kind: string, content: string): string;
|
|
3
3
|
export declare function formatInternalContextBlock(kind: string, content: string): string;
|
|
4
4
|
export declare function sanitizeInternalReminderBlocks(text: string): string;
|
|
5
|
+
export declare function sanitizeInternalReasoningText(text: string): string;
|
|
5
6
|
export declare function sanitizeAssistantProviderMetadata(metadata: AssistantProviderMetadata | undefined): AssistantProviderMetadata | undefined;
|
|
6
7
|
export declare function createStreamingInternalReminderSanitizer(): {
|
|
7
8
|
push(delta: string): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const INTERNAL_TAG_PREFIX = "<bubble_internal_";
|
|
2
|
+
const MEMORY_CITATION_TAG = "<oai-mem-citation";
|
|
2
3
|
const INTERNAL_TAG_NAMES = ["reminder", "context"];
|
|
3
4
|
const LEGACY_RUNTIME_MARKERS = [
|
|
4
5
|
"Runtime reminder:\n",
|
|
@@ -6,6 +7,7 @@ const LEGACY_RUNTIME_MARKERS = [
|
|
|
6
7
|
];
|
|
7
8
|
const STREAM_MARKERS = [
|
|
8
9
|
INTERNAL_TAG_PREFIX,
|
|
10
|
+
MEMORY_CITATION_TAG,
|
|
9
11
|
...LEGACY_RUNTIME_MARKERS,
|
|
10
12
|
];
|
|
11
13
|
const LEGACY_REMINDER_END_PHRASES = [
|
|
@@ -37,6 +39,15 @@ export function sanitizeInternalReminderBlocks(text) {
|
|
|
37
39
|
const sanitizer = createStreamingInternalReminderSanitizer();
|
|
38
40
|
return sanitizer.push(text) + sanitizer.flush();
|
|
39
41
|
}
|
|
42
|
+
export function sanitizeInternalReasoningText(text) {
|
|
43
|
+
const withoutBlocks = sanitizeInternalReminderBlocks(text);
|
|
44
|
+
if (!withoutBlocks)
|
|
45
|
+
return withoutBlocks;
|
|
46
|
+
return withoutBlocks
|
|
47
|
+
.split(/\n{2,}/)
|
|
48
|
+
.filter((paragraph) => !containsInternalReminderReference(paragraph))
|
|
49
|
+
.join("\n\n");
|
|
50
|
+
}
|
|
40
51
|
export function sanitizeAssistantProviderMetadata(metadata) {
|
|
41
52
|
const anthropic = metadata?.anthropic;
|
|
42
53
|
const blocks = anthropic?.contentBlocks;
|
|
@@ -119,10 +130,31 @@ function formatInternalBlock(type, kind, content) {
|
|
|
119
130
|
const safeKind = kind.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
120
131
|
return `<bubble_internal_${type} kind="${safeKind}">\n${content}\n</bubble_internal_${type}>`;
|
|
121
132
|
}
|
|
133
|
+
function containsInternalReminderReference(text) {
|
|
134
|
+
return INTERNAL_REASONING_REFERENCE_PATTERNS.some((pattern) => pattern.test(text));
|
|
135
|
+
}
|
|
136
|
+
const INTERNAL_REASONING_REFERENCE_PATTERNS = [
|
|
137
|
+
/<bubble_internal_(?:reminder|context)\b/i,
|
|
138
|
+
/\bsystem\s+reminder\b/i,
|
|
139
|
+
/\bruntime\s+reminder\b/i,
|
|
140
|
+
/\bsystem\s+prompt\b/i,
|
|
141
|
+
/The following deferred tools are available via tool_search/i,
|
|
142
|
+
/Known deferred tools/i,
|
|
143
|
+
/\bdeferred tools\b/i,
|
|
144
|
+
/\bmcp__[a-z0-9_]+/i,
|
|
145
|
+
/\bMCP\s+arxiv\s+tools\b/i,
|
|
146
|
+
/\barxiv\s+MCP\s+tools\b/i,
|
|
147
|
+
/Subagent lifecycle truth/i,
|
|
148
|
+
/Count unique agent_id values only/i,
|
|
149
|
+
/Do not describe a subagent as running or still working/i,
|
|
150
|
+
];
|
|
122
151
|
function consumeInternalBlockAtStart(text, final) {
|
|
123
152
|
if (text.startsWith(INTERNAL_TAG_PREFIX)) {
|
|
124
153
|
return consumeStructuredInternalBlock(text, final);
|
|
125
154
|
}
|
|
155
|
+
if (text.startsWith(MEMORY_CITATION_TAG)) {
|
|
156
|
+
return consumeMemoryCitationBlock(text, final);
|
|
157
|
+
}
|
|
126
158
|
if (text.startsWith("Runtime reminder:\n")) {
|
|
127
159
|
return consumeLegacyRuntimeReminder(text, final);
|
|
128
160
|
}
|
|
@@ -131,6 +163,20 @@ function consumeInternalBlockAtStart(text, final) {
|
|
|
131
163
|
}
|
|
132
164
|
return undefined;
|
|
133
165
|
}
|
|
166
|
+
function consumeMemoryCitationBlock(text, final) {
|
|
167
|
+
const openMatch = text.match(/^<oai-mem-citation\b[^>]*>/);
|
|
168
|
+
if (!openMatch) {
|
|
169
|
+
return isPrefixOf(MEMORY_CITATION_TAG, text)
|
|
170
|
+
? final ? { consume: text.length } : { hold: true }
|
|
171
|
+
: undefined;
|
|
172
|
+
}
|
|
173
|
+
const closeTag = "</oai-mem-citation>";
|
|
174
|
+
const closeIndex = text.indexOf(closeTag, openMatch[0].length);
|
|
175
|
+
if (closeIndex < 0) {
|
|
176
|
+
return final ? { consume: text.length } : { hold: true };
|
|
177
|
+
}
|
|
178
|
+
return { consume: consumeTrailingLineBreaks(text, closeIndex + closeTag.length) };
|
|
179
|
+
}
|
|
134
180
|
function consumeStructuredInternalBlock(text, final) {
|
|
135
181
|
for (const tagName of INTERNAL_TAG_NAMES) {
|
|
136
182
|
const openMatch = text.match(new RegExp(`^<bubble_internal_${tagName}\\b[^>]*>`));
|
package/dist/agent.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { type ContextUsageSnapshot } from "./context/usage.js";
|
|
6
6
|
import type { AgentEvent, AgentInputController, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
|
|
7
7
|
import { type TurnHooks } from "./orchestrator/hooks.js";
|
|
8
|
+
import type { ExternalHookController } from "./hooks/controller.js";
|
|
8
9
|
import { type AgentCategoriesConfig, type ResolvedSubagentRoute } from "./agent/categories.js";
|
|
9
10
|
import { BudgetLedger } from "./agent/budget-ledger.js";
|
|
10
11
|
import { type AgentProfile, type SubagentRunResult } from "./agent/profiles.js";
|
|
@@ -35,6 +36,9 @@ export interface AgentOptions {
|
|
|
35
36
|
onTodosUpdate?: (todos: Todo[]) => void;
|
|
36
37
|
onModeUpdate?: (mode: PermissionMode) => void;
|
|
37
38
|
hooks?: TurnHooks[];
|
|
39
|
+
externalHooks?: ExternalHookController;
|
|
40
|
+
agentRole?: "parent" | "subagent";
|
|
41
|
+
subAgentId?: string;
|
|
38
42
|
budgetLedger?: BudgetLedger;
|
|
39
43
|
budgetSource?: {
|
|
40
44
|
runId: string;
|
|
@@ -69,6 +73,9 @@ export declare class Agent {
|
|
|
69
73
|
private onMessageAppend?;
|
|
70
74
|
private onToolResult?;
|
|
71
75
|
private hookDefinitions;
|
|
76
|
+
private externalHooks?;
|
|
77
|
+
private agentRole;
|
|
78
|
+
private subAgentId?;
|
|
72
79
|
private maxTurns?;
|
|
73
80
|
private taskBudget?;
|
|
74
81
|
private budgetLedger?;
|
|
@@ -83,6 +90,8 @@ export declare class Agent {
|
|
|
83
90
|
private lastInputTokens;
|
|
84
91
|
private lastAnchorMessageCount;
|
|
85
92
|
constructor(options: AgentOptions);
|
|
93
|
+
private runExternalHook;
|
|
94
|
+
private injectHookModelContext;
|
|
86
95
|
/** Unlock a list of deferred tools so they're included in subsequent turns. */
|
|
87
96
|
unlockDeferredTools(names: string[]): void;
|
|
88
97
|
/** All deferred tools in this session (for tool_search to inspect). */
|
package/dist/agent.js
CHANGED
|
@@ -8,11 +8,13 @@ import { compactMessagesWithLLM } from "./context/compact-llm.js";
|
|
|
8
8
|
import { estimateContextTokens, getContextBudget } from "./context/budget.js";
|
|
9
9
|
import { buildContextUsageSnapshot } from "./context/usage.js";
|
|
10
10
|
import { isContextOverflowError } from "./context/overflow.js";
|
|
11
|
+
import { computeRetryDelayMs, isProviderStreamInterruption, MAX_STREAM_INTERRUPTION_RETRIES, sleepBeforeRetry, } from "./network/retry.js";
|
|
11
12
|
import { projectMessages } from "./context/projector.js";
|
|
12
13
|
import { aggressivePruneMessages, markStableCurrentToolResultsForCache } from "./context/prune.js";
|
|
13
14
|
import { truncateToolOutputForModel } from "./context/tool-output-truncate.js";
|
|
14
15
|
import { buildDeferredToolsReminder, buildToolFreezeReminder, reminderForMode } from "./prompt/reminders.js";
|
|
15
16
|
import { HookBus } from "./orchestrator/hooks.js";
|
|
17
|
+
import { normalizeHookInput, truncateHookText, } from "./hooks/index.js";
|
|
16
18
|
import { createDefaultHooks } from "./orchestrator/default-hooks.js";
|
|
17
19
|
import { resolveModelRoute, resolveSubagentRoute } from "./agent/categories.js";
|
|
18
20
|
import { getSubtaskPolicy } from "./agent/subtask-policy.js";
|
|
@@ -20,7 +22,7 @@ import { composeAbortSignals } from "./agent/budget-ledger.js";
|
|
|
20
22
|
import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAgentProfile, validateAgentProfileTools } from "./agent/profiles.js";
|
|
21
23
|
import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
|
|
22
24
|
import { isHiddenToolResult } from "./agent/discovery-barrier.js";
|
|
23
|
-
import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMetadata, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
|
|
25
|
+
import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMetadata, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "./agent/internal-reminder-sanitizer.js";
|
|
24
26
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
25
27
|
import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
|
|
26
28
|
import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
|
|
@@ -39,6 +41,38 @@ const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained
|
|
|
39
41
|
"Do not put the final answer only in hidden reasoning.";
|
|
40
42
|
const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
|
|
41
43
|
const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
|
|
44
|
+
function agentEventFromHookProgress(event) {
|
|
45
|
+
const source = `${event.source.scope}:${event.source.index}`;
|
|
46
|
+
if (event.type === "hook_start") {
|
|
47
|
+
return {
|
|
48
|
+
type: "hook_start",
|
|
49
|
+
eventName: event.eventName,
|
|
50
|
+
hookId: event.hookId,
|
|
51
|
+
source,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (event.type === "hook_end") {
|
|
55
|
+
return {
|
|
56
|
+
type: "hook_end",
|
|
57
|
+
eventName: event.eventName,
|
|
58
|
+
hookId: event.hookId,
|
|
59
|
+
source,
|
|
60
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
61
|
+
decision: event.decision ?? "allow",
|
|
62
|
+
reason: event.reason,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
type: "hook_error",
|
|
67
|
+
eventName: event.eventName,
|
|
68
|
+
hookId: event.hookId,
|
|
69
|
+
source,
|
|
70
|
+
elapsedMs: event.elapsedMs,
|
|
71
|
+
decision: event.decision,
|
|
72
|
+
reason: event.reason,
|
|
73
|
+
error: event.error ?? "Hook failed.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
42
76
|
export class AgentAbortError extends Error {
|
|
43
77
|
constructor(message = "Agent run cancelled.") {
|
|
44
78
|
super(message);
|
|
@@ -64,6 +98,9 @@ export class Agent {
|
|
|
64
98
|
onMessageAppend;
|
|
65
99
|
onToolResult;
|
|
66
100
|
hookDefinitions;
|
|
101
|
+
externalHooks;
|
|
102
|
+
agentRole;
|
|
103
|
+
subAgentId;
|
|
67
104
|
maxTurns;
|
|
68
105
|
taskBudget;
|
|
69
106
|
budgetLedger;
|
|
@@ -91,6 +128,9 @@ export class Agent {
|
|
|
91
128
|
this.onTodosUpdate = options.onTodosUpdate;
|
|
92
129
|
this.onModeUpdate = options.onModeUpdate;
|
|
93
130
|
this.hookDefinitions = options.hooks ?? [];
|
|
131
|
+
this.externalHooks = options.externalHooks;
|
|
132
|
+
this.agentRole = options.agentRole ?? "parent";
|
|
133
|
+
this.subAgentId = options.subAgentId;
|
|
94
134
|
this.maxTurns = options.maxTurns ?? options.steps;
|
|
95
135
|
this.taskBudget = options.taskBudget;
|
|
96
136
|
this.budgetLedger = options.budgetLedger;
|
|
@@ -120,6 +160,37 @@ export class Agent {
|
|
|
120
160
|
this.injectSystemReminder(buildDeferredToolsReminder(deferredNames));
|
|
121
161
|
}
|
|
122
162
|
}
|
|
163
|
+
async runExternalHook(request, abortSignal) {
|
|
164
|
+
const events = [];
|
|
165
|
+
if (!this.externalHooks) {
|
|
166
|
+
return {
|
|
167
|
+
result: {
|
|
168
|
+
eventName: request.eventName,
|
|
169
|
+
decision: "allow",
|
|
170
|
+
modelContext: [],
|
|
171
|
+
results: [],
|
|
172
|
+
diagnostics: [],
|
|
173
|
+
matched: 0,
|
|
174
|
+
},
|
|
175
|
+
events,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const result = await this.externalHooks.runEvent({
|
|
179
|
+
agentRole: this.agentRole,
|
|
180
|
+
subAgentId: this.subAgentId,
|
|
181
|
+
sessionId: this.sessionID,
|
|
182
|
+
...request,
|
|
183
|
+
}, {
|
|
184
|
+
abortSignal,
|
|
185
|
+
onProgress: (event) => events.push(agentEventFromHookProgress(event)),
|
|
186
|
+
});
|
|
187
|
+
return { result, events };
|
|
188
|
+
}
|
|
189
|
+
injectHookModelContext(result) {
|
|
190
|
+
for (const context of result.modelContext) {
|
|
191
|
+
this.injectSystemReminder(`[Hook ${result.eventName}] ${context}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
123
194
|
/** Unlock a list of deferred tools so they're included in subsequent turns. */
|
|
124
195
|
unlockDeferredTools(names) {
|
|
125
196
|
for (const n of names) {
|
|
@@ -259,6 +330,7 @@ export class Agent {
|
|
|
259
330
|
provider: this._providerId || "none",
|
|
260
331
|
model: this.apiModel || "none",
|
|
261
332
|
};
|
|
333
|
+
const runId = randomUUID();
|
|
262
334
|
const emit = (event) => {
|
|
263
335
|
traceEvent("agent_event", summarizeAgentEventForTrace(event), traceContext);
|
|
264
336
|
return event;
|
|
@@ -284,37 +356,68 @@ export class Agent {
|
|
|
284
356
|
reminderQueue.push(reminder);
|
|
285
357
|
};
|
|
286
358
|
const pendingInputCount = () => inputController?.pendingInputCount() ?? 0;
|
|
287
|
-
const applyPendingInputs = () => {
|
|
359
|
+
const applyPendingInputs = async () => {
|
|
288
360
|
const pendingInputs = inputController?.drainPendingInputs() ?? [];
|
|
289
361
|
if (pendingInputs.length === 0)
|
|
290
362
|
return [];
|
|
363
|
+
const events = [];
|
|
291
364
|
for (const input of pendingInputs) {
|
|
365
|
+
const hook = await this.runExternalHook({
|
|
366
|
+
eventName: "SteerInputApplied",
|
|
367
|
+
cwd,
|
|
368
|
+
runId,
|
|
369
|
+
target: "current_turn",
|
|
370
|
+
payload: {
|
|
371
|
+
id: input.id,
|
|
372
|
+
target: "current_turn",
|
|
373
|
+
...normalizeHookInput(input.content),
|
|
374
|
+
},
|
|
375
|
+
fullPayload: { prompt: input.content },
|
|
376
|
+
}, abortSignal);
|
|
377
|
+
events.push(...hook.events);
|
|
378
|
+
this.injectHookModelContext(hook.result);
|
|
292
379
|
this.appendMessage({ role: "user", content: input.content });
|
|
293
|
-
|
|
294
|
-
return [
|
|
295
|
-
...pendingInputs.map((input) => ({
|
|
380
|
+
events.push({
|
|
296
381
|
type: "input_applied",
|
|
297
382
|
id: input.id,
|
|
298
383
|
content: input.content,
|
|
299
384
|
target: "current_turn",
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
events.push({ type: "input_pending_changed", pending: pendingInputCount() });
|
|
388
|
+
return events;
|
|
303
389
|
};
|
|
304
|
-
const rejectPendingInputs = (reason) => {
|
|
390
|
+
const rejectPendingInputs = async (reason) => {
|
|
305
391
|
const pendingInputs = inputController?.drainPendingInputs() ?? [];
|
|
306
392
|
if (pendingInputs.length === 0)
|
|
307
393
|
return [];
|
|
308
|
-
|
|
309
|
-
|
|
394
|
+
const events = [];
|
|
395
|
+
for (const input of pendingInputs) {
|
|
396
|
+
const hook = await this.runExternalHook({
|
|
397
|
+
eventName: "QueuedInputRejected",
|
|
398
|
+
cwd,
|
|
399
|
+
runId,
|
|
400
|
+
target: "next_turn",
|
|
401
|
+
payload: {
|
|
402
|
+
id: input.id,
|
|
403
|
+
reason,
|
|
404
|
+
target: "next_turn",
|
|
405
|
+
...normalizeHookInput(input.content),
|
|
406
|
+
},
|
|
407
|
+
fullPayload: { prompt: input.content },
|
|
408
|
+
}, abortSignal);
|
|
409
|
+
events.push(...hook.events);
|
|
410
|
+
this.injectHookModelContext(hook.result);
|
|
411
|
+
events.push({
|
|
310
412
|
type: "input_rejected",
|
|
311
413
|
id: input.id,
|
|
312
414
|
content: input.content,
|
|
313
415
|
reason,
|
|
314
416
|
target: "next_turn",
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
events.push({ type: "input_pending_changed", pending: pendingInputCount() });
|
|
420
|
+
return events;
|
|
318
421
|
};
|
|
319
422
|
const flushGovernorReminders = () => {
|
|
320
423
|
for (const reminder of reminderQueue.splice(0, reminderQueue.length)) {
|
|
@@ -325,6 +428,26 @@ export class Agent {
|
|
|
325
428
|
this.setTodos([]);
|
|
326
429
|
yield emit({ type: "todos_updated", todos: [] });
|
|
327
430
|
}
|
|
431
|
+
const promptHook = await this.runExternalHook({
|
|
432
|
+
eventName: "UserPromptSubmit",
|
|
433
|
+
cwd,
|
|
434
|
+
runId,
|
|
435
|
+
target: typeof userInput === "string" ? userInput : "content_parts",
|
|
436
|
+
payload: normalizeHookInput(userInput),
|
|
437
|
+
fullPayload: { prompt: userInput },
|
|
438
|
+
}, abortSignal);
|
|
439
|
+
for (const event of promptHook.events)
|
|
440
|
+
yield emit(event);
|
|
441
|
+
if (promptHook.result.decision === "deny") {
|
|
442
|
+
const message = promptHook.result.reason
|
|
443
|
+
?? `Prompt blocked by hook ${promptHook.result.sourceHookId ?? "<unknown>"}.`;
|
|
444
|
+
yield emit({ type: "turn_start" });
|
|
445
|
+
yield emit({ type: "text_delta", content: message });
|
|
446
|
+
yield emit({ type: "turn_end", willContinue: false });
|
|
447
|
+
yield emit({ type: "agent_end" });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.injectHookModelContext(promptHook.result);
|
|
328
451
|
this.appendMessage({ role: "user", content: userInput });
|
|
329
452
|
await hookBus.runBeforeTurn({
|
|
330
453
|
agent: this,
|
|
@@ -337,6 +460,7 @@ export class Agent {
|
|
|
337
460
|
flushGovernorReminders();
|
|
338
461
|
let consecutiveOverflowRecoveries = 0;
|
|
339
462
|
let consecutiveEmptyAssistantRecoveries = 0;
|
|
463
|
+
let consecutiveStreamInterruptionRetries = 0;
|
|
340
464
|
let step = 0;
|
|
341
465
|
let autoServersStopped = false;
|
|
342
466
|
const stopOwnedAutoServers = async () => {
|
|
@@ -353,7 +477,7 @@ export class Agent {
|
|
|
353
477
|
flushGovernorReminders();
|
|
354
478
|
for (const update of this.drainSubagentToolUpdates())
|
|
355
479
|
yield emit(update);
|
|
356
|
-
for (const event of applyPendingInputs())
|
|
480
|
+
for (const event of await applyPendingInputs())
|
|
357
481
|
yield emit(event);
|
|
358
482
|
yield emit({ type: "turn_start" });
|
|
359
483
|
step += 1;
|
|
@@ -404,6 +528,23 @@ export class Agent {
|
|
|
404
528
|
};
|
|
405
529
|
await hookBus.runBeforeModelCall(beforeModelCallCtx);
|
|
406
530
|
toolEntries = beforeModelCallCtx.toolEntries;
|
|
531
|
+
const preModelHook = await this.runExternalHook({
|
|
532
|
+
eventName: "PreModelCall",
|
|
533
|
+
cwd,
|
|
534
|
+
runId,
|
|
535
|
+
target: this.apiModel,
|
|
536
|
+
payload: {
|
|
537
|
+
providerId: this.providerId,
|
|
538
|
+
model: this.apiModel,
|
|
539
|
+
mode: this._mode,
|
|
540
|
+
toolCount: toolEntries.length,
|
|
541
|
+
...normalizeHookInput(userInput),
|
|
542
|
+
},
|
|
543
|
+
fullPayload: { prompt: userInput },
|
|
544
|
+
}, abortSignal);
|
|
545
|
+
for (const event of preModelHook.events)
|
|
546
|
+
yield emit(event);
|
|
547
|
+
this.injectHookModelContext(preModelHook.result);
|
|
407
548
|
flushGovernorReminders();
|
|
408
549
|
const textOnly = !!hookState.forceTextOnlyReason;
|
|
409
550
|
const toolDefinitions = toolEntries
|
|
@@ -610,6 +751,23 @@ export class Agent {
|
|
|
610
751
|
if (assistantAppended) {
|
|
611
752
|
throw error;
|
|
612
753
|
}
|
|
754
|
+
if (isProviderStreamInterruption(error)
|
|
755
|
+
&& !isAbortLikeError(error, abortSignal)
|
|
756
|
+
&& consecutiveStreamInterruptionRetries < MAX_STREAM_INTERRUPTION_RETRIES) {
|
|
757
|
+
// The provider stream died after partial content. The half-built
|
|
758
|
+
// assistantMsg was never appended to this.messages, and the next
|
|
759
|
+
// turn_start resets the streaming display, so re-issuing the whole
|
|
760
|
+
// request is safe.
|
|
761
|
+
consecutiveStreamInterruptionRetries += 1;
|
|
762
|
+
yield emit({
|
|
763
|
+
type: "provider_retry",
|
|
764
|
+
attempt: consecutiveStreamInterruptionRetries,
|
|
765
|
+
maxAttempts: MAX_STREAM_INTERRUPTION_RETRIES,
|
|
766
|
+
reason: "Provider stream interrupted mid-response.",
|
|
767
|
+
});
|
|
768
|
+
await sleepBeforeRetry(computeRetryDelayMs(consecutiveStreamInterruptionRetries), abortSignal).catch(() => undefined);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
613
771
|
if (!isContextOverflowError(error)) {
|
|
614
772
|
if (!isAbortLikeError(error, abortSignal) && shouldAppendModelInterruptedBoundary(this.messages)) {
|
|
615
773
|
this.appendMessage(createModelInterruptedMessage(error, {
|
|
@@ -631,6 +789,7 @@ export class Agent {
|
|
|
631
789
|
}
|
|
632
790
|
consecutiveOverflowRecoveries = 0;
|
|
633
791
|
consecutiveEmptyAssistantRecoveries = 0;
|
|
792
|
+
consecutiveStreamInterruptionRetries = 0;
|
|
634
793
|
// Execute tools if any
|
|
635
794
|
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
|
|
636
795
|
const parsedCalls = [];
|
|
@@ -695,6 +854,37 @@ export class Agent {
|
|
|
695
854
|
blockedResult = result;
|
|
696
855
|
},
|
|
697
856
|
});
|
|
857
|
+
const preToolHook = await this.runExternalHook({
|
|
858
|
+
eventName: "PreToolUse",
|
|
859
|
+
cwd,
|
|
860
|
+
runId,
|
|
861
|
+
target: tc.name,
|
|
862
|
+
payload: {
|
|
863
|
+
id: tc.id,
|
|
864
|
+
name: tc.name,
|
|
865
|
+
argsPreview: truncateHookText(tc.arguments, 1000),
|
|
866
|
+
},
|
|
867
|
+
fullPayload: {
|
|
868
|
+
toolArgs: tc.parsedArgs,
|
|
869
|
+
toolArguments: tc.arguments,
|
|
870
|
+
},
|
|
871
|
+
}, abortSignal);
|
|
872
|
+
for (const event of preToolHook.events)
|
|
873
|
+
yield emit(event);
|
|
874
|
+
this.injectHookModelContext(preToolHook.result);
|
|
875
|
+
if (preToolHook.result.decision === "deny") {
|
|
876
|
+
blockedResult = {
|
|
877
|
+
content: preToolHook.result.reason
|
|
878
|
+
?? `Tool call blocked by hook ${preToolHook.result.sourceHookId ?? "<unknown>"}.`,
|
|
879
|
+
isError: true,
|
|
880
|
+
metadata: {
|
|
881
|
+
hook: {
|
|
882
|
+
eventName: "PreToolUse",
|
|
883
|
+
hookId: preToolHook.result.sourceHookId,
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
698
888
|
assistantMsg.toolCalls[index] = {
|
|
699
889
|
id: tc.id,
|
|
700
890
|
name: tc.name,
|
|
@@ -729,6 +919,27 @@ export class Agent {
|
|
|
729
919
|
result = next;
|
|
730
920
|
},
|
|
731
921
|
});
|
|
922
|
+
const postToolHook = await this.runExternalHook({
|
|
923
|
+
eventName: result.isError ? "PostToolUseFailure" : "PostToolUse",
|
|
924
|
+
cwd,
|
|
925
|
+
runId,
|
|
926
|
+
target: tc.name,
|
|
927
|
+
payload: {
|
|
928
|
+
id: tc.id,
|
|
929
|
+
name: tc.name,
|
|
930
|
+
argsPreview: truncateHookText(tc.arguments, 1000),
|
|
931
|
+
resultPreview: truncateHookText(result.content, 1000),
|
|
932
|
+
isError: result.isError === true,
|
|
933
|
+
},
|
|
934
|
+
fullPayload: {
|
|
935
|
+
toolArgs: tc.parsedArgs,
|
|
936
|
+
toolArguments: tc.arguments,
|
|
937
|
+
toolResult: result,
|
|
938
|
+
},
|
|
939
|
+
}, abortSignal);
|
|
940
|
+
for (const event of postToolHook.events)
|
|
941
|
+
yield emit(event);
|
|
942
|
+
this.injectHookModelContext(postToolHook.result);
|
|
732
943
|
traceEvent("speculative_read_blocked", {
|
|
733
944
|
id: tc.id,
|
|
734
945
|
name: tc.name,
|
|
@@ -814,6 +1025,27 @@ export class Agent {
|
|
|
814
1025
|
result = next;
|
|
815
1026
|
},
|
|
816
1027
|
});
|
|
1028
|
+
const postToolHook = await this.runExternalHook({
|
|
1029
|
+
eventName: result.isError ? "PostToolUseFailure" : "PostToolUse",
|
|
1030
|
+
cwd,
|
|
1031
|
+
runId,
|
|
1032
|
+
target: tc.name,
|
|
1033
|
+
payload: {
|
|
1034
|
+
id: tc.id,
|
|
1035
|
+
name: tc.name,
|
|
1036
|
+
argsPreview: truncateHookText(tc.arguments, 1000),
|
|
1037
|
+
resultPreview: truncateHookText(result.content, 1000),
|
|
1038
|
+
isError: result.isError === true,
|
|
1039
|
+
},
|
|
1040
|
+
fullPayload: {
|
|
1041
|
+
toolArgs: tc.parsedArgs,
|
|
1042
|
+
toolArguments: tc.arguments,
|
|
1043
|
+
toolResult: result,
|
|
1044
|
+
},
|
|
1045
|
+
}, abortSignal);
|
|
1046
|
+
for (const event of postToolHook.events)
|
|
1047
|
+
yield emit(event);
|
|
1048
|
+
this.injectHookModelContext(postToolHook.result);
|
|
817
1049
|
// Honor the model's server-declared per-tool-output token cap (e.g.
|
|
818
1050
|
// gpt-5.5 reports 10000). Without this, 4-5 large file reads in a row
|
|
819
1051
|
// blow past the input window even though our local estimate looks fine.
|
|
@@ -885,13 +1117,28 @@ export class Agent {
|
|
|
885
1117
|
flushReminders: flushGovernorReminders,
|
|
886
1118
|
});
|
|
887
1119
|
flushGovernorReminders();
|
|
1120
|
+
const stopHook = await this.runExternalHook({
|
|
1121
|
+
eventName: "Stop",
|
|
1122
|
+
cwd,
|
|
1123
|
+
runId,
|
|
1124
|
+
target: "turn",
|
|
1125
|
+
payload: {
|
|
1126
|
+
providerId: this.providerId,
|
|
1127
|
+
model: this.apiModel,
|
|
1128
|
+
mode: this._mode,
|
|
1129
|
+
assistantChars: assistantMsg.content.length,
|
|
1130
|
+
toolCalls: assistantMsg.toolCalls?.length ?? 0,
|
|
1131
|
+
},
|
|
1132
|
+
}, abortSignal);
|
|
1133
|
+
for (const event of stopHook.events)
|
|
1134
|
+
yield emit(event);
|
|
888
1135
|
const willContinue = !!hookState.forceContinuationReason;
|
|
889
1136
|
yield emit({ type: "turn_end", usage: turnUsage, willContinue });
|
|
890
1137
|
if (willContinue) {
|
|
891
1138
|
delete hookState.forceContinuationReason;
|
|
892
1139
|
continue;
|
|
893
1140
|
}
|
|
894
|
-
for (const event of rejectPendingInputs("no_continuation"))
|
|
1141
|
+
for (const event of await rejectPendingInputs("no_continuation"))
|
|
895
1142
|
yield emit(event);
|
|
896
1143
|
break;
|
|
897
1144
|
}
|
|
@@ -913,6 +1160,19 @@ export class Agent {
|
|
|
913
1160
|
yield emit({ type: "todos_updated", todos: this.getTodos() });
|
|
914
1161
|
}
|
|
915
1162
|
}
|
|
1163
|
+
else {
|
|
1164
|
+
const stopFailureHook = await this.runExternalHook({
|
|
1165
|
+
eventName: "StopFailure",
|
|
1166
|
+
cwd,
|
|
1167
|
+
runId,
|
|
1168
|
+
target: "run_error",
|
|
1169
|
+
payload: {
|
|
1170
|
+
error: summarizeTraceError(error),
|
|
1171
|
+
},
|
|
1172
|
+
}, abortSignal);
|
|
1173
|
+
for (const event of stopFailureHook.events)
|
|
1174
|
+
yield emit(event);
|
|
1175
|
+
}
|
|
916
1176
|
throw error;
|
|
917
1177
|
}
|
|
918
1178
|
finally {
|
|
@@ -1226,6 +1486,26 @@ export class Agent {
|
|
|
1226
1486
|
this.pendingSubagentUpdates.push({ id: record.parentToolCallId, name: record.parentToolName, update });
|
|
1227
1487
|
}
|
|
1228
1488
|
};
|
|
1489
|
+
const runSubagentLifecycleHook = async (eventName, status, error) => {
|
|
1490
|
+
try {
|
|
1491
|
+
await this.runExternalHook({
|
|
1492
|
+
eventName,
|
|
1493
|
+
cwd,
|
|
1494
|
+
runId: record.runId,
|
|
1495
|
+
target: record.profile.name,
|
|
1496
|
+
payload: {
|
|
1497
|
+
agentId: record.agentId,
|
|
1498
|
+
nickname: record.nickname,
|
|
1499
|
+
profile: record.profile.name,
|
|
1500
|
+
status,
|
|
1501
|
+
error,
|
|
1502
|
+
},
|
|
1503
|
+
}, options.abortSignal);
|
|
1504
|
+
}
|
|
1505
|
+
catch {
|
|
1506
|
+
// Subagent lifecycle hooks are observe-only; never fail the subagent.
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1229
1509
|
const allTools = [...this.tools.values()];
|
|
1230
1510
|
const diagnostics = validateAgentProfileTools(allTools, record.profile, options.approval);
|
|
1231
1511
|
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
@@ -1236,6 +1516,7 @@ export class Agent {
|
|
|
1236
1516
|
record.status = "blocked";
|
|
1237
1517
|
record.error = blockingDiagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
1238
1518
|
record.updatedAt = Date.now();
|
|
1519
|
+
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1239
1520
|
emit("blocked", undefined, record.error);
|
|
1240
1521
|
this.notifySubagentWaiters(record);
|
|
1241
1522
|
return;
|
|
@@ -1251,6 +1532,7 @@ export class Agent {
|
|
|
1251
1532
|
record.status = "blocked";
|
|
1252
1533
|
record.error = error?.message || String(error);
|
|
1253
1534
|
record.updatedAt = Date.now();
|
|
1535
|
+
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1254
1536
|
emit("blocked", undefined, record.error);
|
|
1255
1537
|
this.notifySubagentWaiters(record);
|
|
1256
1538
|
return;
|
|
@@ -1258,6 +1540,7 @@ export class Agent {
|
|
|
1258
1540
|
record.agent = subAgent;
|
|
1259
1541
|
record.status = "running";
|
|
1260
1542
|
record.updatedAt = Date.now();
|
|
1543
|
+
await runSubagentLifecycleHook("SubagentStart", record.status);
|
|
1261
1544
|
emit("running", undefined, `Running ${record.nickname} (${record.profile.name})...`);
|
|
1262
1545
|
let turnSummaryBuffer = "";
|
|
1263
1546
|
let turnHadToolCall = false;
|
|
@@ -1304,6 +1587,7 @@ export class Agent {
|
|
|
1304
1587
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
1305
1588
|
record.error = error?.message || String(error);
|
|
1306
1589
|
record.updatedAt = Date.now();
|
|
1590
|
+
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1307
1591
|
emit(record.status, undefined, record.error);
|
|
1308
1592
|
this.notifySubagentWaiters(record);
|
|
1309
1593
|
return;
|
|
@@ -1315,6 +1599,7 @@ export class Agent {
|
|
|
1315
1599
|
record.status = "completed";
|
|
1316
1600
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
1317
1601
|
record.updatedAt = Date.now();
|
|
1602
|
+
await runSubagentLifecycleHook("SubagentStop", record.status);
|
|
1318
1603
|
emit("completed", undefined, record.summary || `${record.nickname} completed`);
|
|
1319
1604
|
this.notifySubagentWaiters(record);
|
|
1320
1605
|
}
|
|
@@ -1394,6 +1679,9 @@ export class Agent {
|
|
|
1394
1679
|
budgetSource: { runId: record.runId, subAgentId: record.agentId },
|
|
1395
1680
|
systemPrompt: childSystemPrompt,
|
|
1396
1681
|
hooks: this.hookDefinitions,
|
|
1682
|
+
externalHooks: this.externalHooks,
|
|
1683
|
+
agentRole: "subagent",
|
|
1684
|
+
subAgentId: record.agentId,
|
|
1397
1685
|
agentCategories: this.agentCategories,
|
|
1398
1686
|
providerFactory: this.providerFactory,
|
|
1399
1687
|
});
|
|
@@ -1550,7 +1838,7 @@ export class Agent {
|
|
|
1550
1838
|
message.content = sanitizeInternalReminderBlocks(message.content);
|
|
1551
1839
|
}
|
|
1552
1840
|
if (message.role === "assistant" && message.reasoning) {
|
|
1553
|
-
message.reasoning =
|
|
1841
|
+
message.reasoning = sanitizeInternalReasoningText(message.reasoning);
|
|
1554
1842
|
}
|
|
1555
1843
|
if (message.role === "assistant" && message.providerMetadata) {
|
|
1556
1844
|
message.providerMetadata = sanitizeAssistantProviderMetadata(message.providerMetadata);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PermissionCheckResult, PermissionQuery, PermissionRuleSet } from "../permissions/types.js";
|
|
2
2
|
import type { PermissionMode } from "../types.js";
|
|
3
|
+
import type { ExternalHookController } from "../hooks/controller.js";
|
|
3
4
|
import type { BashAllowlist } from "./session-cache.js";
|
|
4
5
|
import type { ApprovalController, ApprovalDecision, ApprovalRequest } from "./types.js";
|
|
5
6
|
export interface ApprovalControllerOptions {
|
|
@@ -23,6 +24,9 @@ export interface ApprovalControllerOptions {
|
|
|
23
24
|
* /permissions take effect immediately. Omit to disable rule-based gating.
|
|
24
25
|
*/
|
|
25
26
|
getRuleSet?: () => PermissionRuleSet;
|
|
27
|
+
/** External lifecycle hooks may observe or reject pending permission requests. */
|
|
28
|
+
externalHooks?: ExternalHookController;
|
|
29
|
+
sessionId?: string;
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* Default ApprovalController. Decision tree:
|
|
@@ -46,4 +50,6 @@ export declare class PermissionAwareApprovalController implements ApprovalContro
|
|
|
46
50
|
request(req: ApprovalRequest): Promise<ApprovalDecision>;
|
|
47
51
|
private requestToQuery;
|
|
48
52
|
private checkRequestRules;
|
|
53
|
+
private runPermissionRequestHook;
|
|
54
|
+
private runPermissionResultHook;
|
|
49
55
|
}
|