@bubblebrain-ai/bubble 0.0.19 → 0.0.21
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 +10 -0
- package/dist/agent.js +310 -18
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +29 -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 +86 -13
- 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/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +164 -0
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/bash.js +4 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +6 -5
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- 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/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -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 +568 -223
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +82 -5
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +303 -248
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +90 -6
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- 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,12 +5,14 @@
|
|
|
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";
|
|
11
12
|
import { type SubagentThreadSnapshot } from "./agent/subagent-control.js";
|
|
12
13
|
import type { SkillSummary } from "./skills/types.js";
|
|
13
14
|
import type { FileStateTracker } from "./tools/file-state.js";
|
|
15
|
+
export declare const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
|
|
14
16
|
export declare class AgentAbortError extends Error {
|
|
15
17
|
constructor(message?: string);
|
|
16
18
|
}
|
|
@@ -35,6 +37,9 @@ export interface AgentOptions {
|
|
|
35
37
|
onTodosUpdate?: (todos: Todo[]) => void;
|
|
36
38
|
onModeUpdate?: (mode: PermissionMode) => void;
|
|
37
39
|
hooks?: TurnHooks[];
|
|
40
|
+
externalHooks?: ExternalHookController;
|
|
41
|
+
agentRole?: "parent" | "subagent";
|
|
42
|
+
subAgentId?: string;
|
|
38
43
|
budgetLedger?: BudgetLedger;
|
|
39
44
|
budgetSource?: {
|
|
40
45
|
runId: string;
|
|
@@ -69,6 +74,9 @@ export declare class Agent {
|
|
|
69
74
|
private onMessageAppend?;
|
|
70
75
|
private onToolResult?;
|
|
71
76
|
private hookDefinitions;
|
|
77
|
+
private externalHooks?;
|
|
78
|
+
private agentRole;
|
|
79
|
+
private subAgentId?;
|
|
72
80
|
private maxTurns?;
|
|
73
81
|
private taskBudget?;
|
|
74
82
|
private budgetLedger?;
|
|
@@ -83,6 +91,8 @@ export declare class Agent {
|
|
|
83
91
|
private lastInputTokens;
|
|
84
92
|
private lastAnchorMessageCount;
|
|
85
93
|
constructor(options: AgentOptions);
|
|
94
|
+
private runExternalHook;
|
|
95
|
+
private injectHookModelContext;
|
|
86
96
|
/** Unlock a list of deferred tools so they're included in subsequent turns. */
|
|
87
97
|
unlockDeferredTools(names: string[]): void;
|
|
88
98
|
/** 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";
|
|
@@ -38,7 +40,43 @@ const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained
|
|
|
38
40
|
"Respond now with a concise, user-visible answer, or call an available tool if more work is required. " +
|
|
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
|
+
// Model-facing interruption boundary. Persisted into the transcript so the
|
|
44
|
+
// next turn sees an explicit stop instead of a dangling request — but it must
|
|
45
|
+
// never render in the UI as if the assistant said it (the TUIs strip it and
|
|
46
|
+
// show their own interrupt indicator instead).
|
|
47
|
+
export const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
|
|
48
|
+
function agentEventFromHookProgress(event) {
|
|
49
|
+
const source = `${event.source.scope}:${event.source.index}`;
|
|
50
|
+
if (event.type === "hook_start") {
|
|
51
|
+
return {
|
|
52
|
+
type: "hook_start",
|
|
53
|
+
eventName: event.eventName,
|
|
54
|
+
hookId: event.hookId,
|
|
55
|
+
source,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (event.type === "hook_end") {
|
|
59
|
+
return {
|
|
60
|
+
type: "hook_end",
|
|
61
|
+
eventName: event.eventName,
|
|
62
|
+
hookId: event.hookId,
|
|
63
|
+
source,
|
|
64
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
65
|
+
decision: event.decision ?? "allow",
|
|
66
|
+
reason: event.reason,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
type: "hook_error",
|
|
71
|
+
eventName: event.eventName,
|
|
72
|
+
hookId: event.hookId,
|
|
73
|
+
source,
|
|
74
|
+
elapsedMs: event.elapsedMs,
|
|
75
|
+
decision: event.decision,
|
|
76
|
+
reason: event.reason,
|
|
77
|
+
error: event.error ?? "Hook failed.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
42
80
|
export class AgentAbortError extends Error {
|
|
43
81
|
constructor(message = "Agent run cancelled.") {
|
|
44
82
|
super(message);
|
|
@@ -64,6 +102,9 @@ export class Agent {
|
|
|
64
102
|
onMessageAppend;
|
|
65
103
|
onToolResult;
|
|
66
104
|
hookDefinitions;
|
|
105
|
+
externalHooks;
|
|
106
|
+
agentRole;
|
|
107
|
+
subAgentId;
|
|
67
108
|
maxTurns;
|
|
68
109
|
taskBudget;
|
|
69
110
|
budgetLedger;
|
|
@@ -91,6 +132,9 @@ export class Agent {
|
|
|
91
132
|
this.onTodosUpdate = options.onTodosUpdate;
|
|
92
133
|
this.onModeUpdate = options.onModeUpdate;
|
|
93
134
|
this.hookDefinitions = options.hooks ?? [];
|
|
135
|
+
this.externalHooks = options.externalHooks;
|
|
136
|
+
this.agentRole = options.agentRole ?? "parent";
|
|
137
|
+
this.subAgentId = options.subAgentId;
|
|
94
138
|
this.maxTurns = options.maxTurns ?? options.steps;
|
|
95
139
|
this.taskBudget = options.taskBudget;
|
|
96
140
|
this.budgetLedger = options.budgetLedger;
|
|
@@ -120,6 +164,37 @@ export class Agent {
|
|
|
120
164
|
this.injectSystemReminder(buildDeferredToolsReminder(deferredNames));
|
|
121
165
|
}
|
|
122
166
|
}
|
|
167
|
+
async runExternalHook(request, abortSignal) {
|
|
168
|
+
const events = [];
|
|
169
|
+
if (!this.externalHooks) {
|
|
170
|
+
return {
|
|
171
|
+
result: {
|
|
172
|
+
eventName: request.eventName,
|
|
173
|
+
decision: "allow",
|
|
174
|
+
modelContext: [],
|
|
175
|
+
results: [],
|
|
176
|
+
diagnostics: [],
|
|
177
|
+
matched: 0,
|
|
178
|
+
},
|
|
179
|
+
events,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const result = await this.externalHooks.runEvent({
|
|
183
|
+
agentRole: this.agentRole,
|
|
184
|
+
subAgentId: this.subAgentId,
|
|
185
|
+
sessionId: this.sessionID,
|
|
186
|
+
...request,
|
|
187
|
+
}, {
|
|
188
|
+
abortSignal,
|
|
189
|
+
onProgress: (event) => events.push(agentEventFromHookProgress(event)),
|
|
190
|
+
});
|
|
191
|
+
return { result, events };
|
|
192
|
+
}
|
|
193
|
+
injectHookModelContext(result) {
|
|
194
|
+
for (const context of result.modelContext) {
|
|
195
|
+
this.injectSystemReminder(`[Hook ${result.eventName}] ${context}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
123
198
|
/** Unlock a list of deferred tools so they're included in subsequent turns. */
|
|
124
199
|
unlockDeferredTools(names) {
|
|
125
200
|
for (const n of names) {
|
|
@@ -259,6 +334,7 @@ export class Agent {
|
|
|
259
334
|
provider: this._providerId || "none",
|
|
260
335
|
model: this.apiModel || "none",
|
|
261
336
|
};
|
|
337
|
+
const runId = randomUUID();
|
|
262
338
|
const emit = (event) => {
|
|
263
339
|
traceEvent("agent_event", summarizeAgentEventForTrace(event), traceContext);
|
|
264
340
|
return event;
|
|
@@ -284,37 +360,68 @@ export class Agent {
|
|
|
284
360
|
reminderQueue.push(reminder);
|
|
285
361
|
};
|
|
286
362
|
const pendingInputCount = () => inputController?.pendingInputCount() ?? 0;
|
|
287
|
-
const applyPendingInputs = () => {
|
|
363
|
+
const applyPendingInputs = async () => {
|
|
288
364
|
const pendingInputs = inputController?.drainPendingInputs() ?? [];
|
|
289
365
|
if (pendingInputs.length === 0)
|
|
290
366
|
return [];
|
|
367
|
+
const events = [];
|
|
291
368
|
for (const input of pendingInputs) {
|
|
369
|
+
const hook = await this.runExternalHook({
|
|
370
|
+
eventName: "SteerInputApplied",
|
|
371
|
+
cwd,
|
|
372
|
+
runId,
|
|
373
|
+
target: "current_turn",
|
|
374
|
+
payload: {
|
|
375
|
+
id: input.id,
|
|
376
|
+
target: "current_turn",
|
|
377
|
+
...normalizeHookInput(input.content),
|
|
378
|
+
},
|
|
379
|
+
fullPayload: { prompt: input.content },
|
|
380
|
+
}, abortSignal);
|
|
381
|
+
events.push(...hook.events);
|
|
382
|
+
this.injectHookModelContext(hook.result);
|
|
292
383
|
this.appendMessage({ role: "user", content: input.content });
|
|
293
|
-
|
|
294
|
-
return [
|
|
295
|
-
...pendingInputs.map((input) => ({
|
|
384
|
+
events.push({
|
|
296
385
|
type: "input_applied",
|
|
297
386
|
id: input.id,
|
|
298
387
|
content: input.content,
|
|
299
388
|
target: "current_turn",
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
events.push({ type: "input_pending_changed", pending: pendingInputCount() });
|
|
392
|
+
return events;
|
|
303
393
|
};
|
|
304
|
-
const rejectPendingInputs = (reason) => {
|
|
394
|
+
const rejectPendingInputs = async (reason) => {
|
|
305
395
|
const pendingInputs = inputController?.drainPendingInputs() ?? [];
|
|
306
396
|
if (pendingInputs.length === 0)
|
|
307
397
|
return [];
|
|
308
|
-
|
|
309
|
-
|
|
398
|
+
const events = [];
|
|
399
|
+
for (const input of pendingInputs) {
|
|
400
|
+
const hook = await this.runExternalHook({
|
|
401
|
+
eventName: "QueuedInputRejected",
|
|
402
|
+
cwd,
|
|
403
|
+
runId,
|
|
404
|
+
target: "next_turn",
|
|
405
|
+
payload: {
|
|
406
|
+
id: input.id,
|
|
407
|
+
reason,
|
|
408
|
+
target: "next_turn",
|
|
409
|
+
...normalizeHookInput(input.content),
|
|
410
|
+
},
|
|
411
|
+
fullPayload: { prompt: input.content },
|
|
412
|
+
}, abortSignal);
|
|
413
|
+
events.push(...hook.events);
|
|
414
|
+
this.injectHookModelContext(hook.result);
|
|
415
|
+
events.push({
|
|
310
416
|
type: "input_rejected",
|
|
311
417
|
id: input.id,
|
|
312
418
|
content: input.content,
|
|
313
419
|
reason,
|
|
314
420
|
target: "next_turn",
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
events.push({ type: "input_pending_changed", pending: pendingInputCount() });
|
|
424
|
+
return events;
|
|
318
425
|
};
|
|
319
426
|
const flushGovernorReminders = () => {
|
|
320
427
|
for (const reminder of reminderQueue.splice(0, reminderQueue.length)) {
|
|
@@ -325,6 +432,26 @@ export class Agent {
|
|
|
325
432
|
this.setTodos([]);
|
|
326
433
|
yield emit({ type: "todos_updated", todos: [] });
|
|
327
434
|
}
|
|
435
|
+
const promptHook = await this.runExternalHook({
|
|
436
|
+
eventName: "UserPromptSubmit",
|
|
437
|
+
cwd,
|
|
438
|
+
runId,
|
|
439
|
+
target: typeof userInput === "string" ? userInput : "content_parts",
|
|
440
|
+
payload: normalizeHookInput(userInput),
|
|
441
|
+
fullPayload: { prompt: userInput },
|
|
442
|
+
}, abortSignal);
|
|
443
|
+
for (const event of promptHook.events)
|
|
444
|
+
yield emit(event);
|
|
445
|
+
if (promptHook.result.decision === "deny") {
|
|
446
|
+
const message = promptHook.result.reason
|
|
447
|
+
?? `Prompt blocked by hook ${promptHook.result.sourceHookId ?? "<unknown>"}.`;
|
|
448
|
+
yield emit({ type: "turn_start" });
|
|
449
|
+
yield emit({ type: "text_delta", content: message });
|
|
450
|
+
yield emit({ type: "turn_end", willContinue: false });
|
|
451
|
+
yield emit({ type: "agent_end" });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this.injectHookModelContext(promptHook.result);
|
|
328
455
|
this.appendMessage({ role: "user", content: userInput });
|
|
329
456
|
await hookBus.runBeforeTurn({
|
|
330
457
|
agent: this,
|
|
@@ -337,6 +464,7 @@ export class Agent {
|
|
|
337
464
|
flushGovernorReminders();
|
|
338
465
|
let consecutiveOverflowRecoveries = 0;
|
|
339
466
|
let consecutiveEmptyAssistantRecoveries = 0;
|
|
467
|
+
let consecutiveStreamInterruptionRetries = 0;
|
|
340
468
|
let step = 0;
|
|
341
469
|
let autoServersStopped = false;
|
|
342
470
|
const stopOwnedAutoServers = async () => {
|
|
@@ -353,7 +481,7 @@ export class Agent {
|
|
|
353
481
|
flushGovernorReminders();
|
|
354
482
|
for (const update of this.drainSubagentToolUpdates())
|
|
355
483
|
yield emit(update);
|
|
356
|
-
for (const event of applyPendingInputs())
|
|
484
|
+
for (const event of await applyPendingInputs())
|
|
357
485
|
yield emit(event);
|
|
358
486
|
yield emit({ type: "turn_start" });
|
|
359
487
|
step += 1;
|
|
@@ -404,6 +532,23 @@ export class Agent {
|
|
|
404
532
|
};
|
|
405
533
|
await hookBus.runBeforeModelCall(beforeModelCallCtx);
|
|
406
534
|
toolEntries = beforeModelCallCtx.toolEntries;
|
|
535
|
+
const preModelHook = await this.runExternalHook({
|
|
536
|
+
eventName: "PreModelCall",
|
|
537
|
+
cwd,
|
|
538
|
+
runId,
|
|
539
|
+
target: this.apiModel,
|
|
540
|
+
payload: {
|
|
541
|
+
providerId: this.providerId,
|
|
542
|
+
model: this.apiModel,
|
|
543
|
+
mode: this._mode,
|
|
544
|
+
toolCount: toolEntries.length,
|
|
545
|
+
...normalizeHookInput(userInput),
|
|
546
|
+
},
|
|
547
|
+
fullPayload: { prompt: userInput },
|
|
548
|
+
}, abortSignal);
|
|
549
|
+
for (const event of preModelHook.events)
|
|
550
|
+
yield emit(event);
|
|
551
|
+
this.injectHookModelContext(preModelHook.result);
|
|
407
552
|
flushGovernorReminders();
|
|
408
553
|
const textOnly = !!hookState.forceTextOnlyReason;
|
|
409
554
|
const toolDefinitions = toolEntries
|
|
@@ -610,6 +755,23 @@ export class Agent {
|
|
|
610
755
|
if (assistantAppended) {
|
|
611
756
|
throw error;
|
|
612
757
|
}
|
|
758
|
+
if (isProviderStreamInterruption(error)
|
|
759
|
+
&& !isAbortLikeError(error, abortSignal)
|
|
760
|
+
&& consecutiveStreamInterruptionRetries < MAX_STREAM_INTERRUPTION_RETRIES) {
|
|
761
|
+
// The provider stream died after partial content. The half-built
|
|
762
|
+
// assistantMsg was never appended to this.messages, and the next
|
|
763
|
+
// turn_start resets the streaming display, so re-issuing the whole
|
|
764
|
+
// request is safe.
|
|
765
|
+
consecutiveStreamInterruptionRetries += 1;
|
|
766
|
+
yield emit({
|
|
767
|
+
type: "provider_retry",
|
|
768
|
+
attempt: consecutiveStreamInterruptionRetries,
|
|
769
|
+
maxAttempts: MAX_STREAM_INTERRUPTION_RETRIES,
|
|
770
|
+
reason: "Provider stream interrupted mid-response.",
|
|
771
|
+
});
|
|
772
|
+
await sleepBeforeRetry(computeRetryDelayMs(consecutiveStreamInterruptionRetries), abortSignal).catch(() => undefined);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
613
775
|
if (!isContextOverflowError(error)) {
|
|
614
776
|
if (!isAbortLikeError(error, abortSignal) && shouldAppendModelInterruptedBoundary(this.messages)) {
|
|
615
777
|
this.appendMessage(createModelInterruptedMessage(error, {
|
|
@@ -631,6 +793,7 @@ export class Agent {
|
|
|
631
793
|
}
|
|
632
794
|
consecutiveOverflowRecoveries = 0;
|
|
633
795
|
consecutiveEmptyAssistantRecoveries = 0;
|
|
796
|
+
consecutiveStreamInterruptionRetries = 0;
|
|
634
797
|
// Execute tools if any
|
|
635
798
|
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
|
|
636
799
|
const parsedCalls = [];
|
|
@@ -695,6 +858,37 @@ export class Agent {
|
|
|
695
858
|
blockedResult = result;
|
|
696
859
|
},
|
|
697
860
|
});
|
|
861
|
+
const preToolHook = await this.runExternalHook({
|
|
862
|
+
eventName: "PreToolUse",
|
|
863
|
+
cwd,
|
|
864
|
+
runId,
|
|
865
|
+
target: tc.name,
|
|
866
|
+
payload: {
|
|
867
|
+
id: tc.id,
|
|
868
|
+
name: tc.name,
|
|
869
|
+
argsPreview: truncateHookText(tc.arguments, 1000),
|
|
870
|
+
},
|
|
871
|
+
fullPayload: {
|
|
872
|
+
toolArgs: tc.parsedArgs,
|
|
873
|
+
toolArguments: tc.arguments,
|
|
874
|
+
},
|
|
875
|
+
}, abortSignal);
|
|
876
|
+
for (const event of preToolHook.events)
|
|
877
|
+
yield emit(event);
|
|
878
|
+
this.injectHookModelContext(preToolHook.result);
|
|
879
|
+
if (preToolHook.result.decision === "deny") {
|
|
880
|
+
blockedResult = {
|
|
881
|
+
content: preToolHook.result.reason
|
|
882
|
+
?? `Tool call blocked by hook ${preToolHook.result.sourceHookId ?? "<unknown>"}.`,
|
|
883
|
+
isError: true,
|
|
884
|
+
metadata: {
|
|
885
|
+
hook: {
|
|
886
|
+
eventName: "PreToolUse",
|
|
887
|
+
hookId: preToolHook.result.sourceHookId,
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
};
|
|
891
|
+
}
|
|
698
892
|
assistantMsg.toolCalls[index] = {
|
|
699
893
|
id: tc.id,
|
|
700
894
|
name: tc.name,
|
|
@@ -729,6 +923,27 @@ export class Agent {
|
|
|
729
923
|
result = next;
|
|
730
924
|
},
|
|
731
925
|
});
|
|
926
|
+
const postToolHook = await this.runExternalHook({
|
|
927
|
+
eventName: result.isError ? "PostToolUseFailure" : "PostToolUse",
|
|
928
|
+
cwd,
|
|
929
|
+
runId,
|
|
930
|
+
target: tc.name,
|
|
931
|
+
payload: {
|
|
932
|
+
id: tc.id,
|
|
933
|
+
name: tc.name,
|
|
934
|
+
argsPreview: truncateHookText(tc.arguments, 1000),
|
|
935
|
+
resultPreview: truncateHookText(result.content, 1000),
|
|
936
|
+
isError: result.isError === true,
|
|
937
|
+
},
|
|
938
|
+
fullPayload: {
|
|
939
|
+
toolArgs: tc.parsedArgs,
|
|
940
|
+
toolArguments: tc.arguments,
|
|
941
|
+
toolResult: result,
|
|
942
|
+
},
|
|
943
|
+
}, abortSignal);
|
|
944
|
+
for (const event of postToolHook.events)
|
|
945
|
+
yield emit(event);
|
|
946
|
+
this.injectHookModelContext(postToolHook.result);
|
|
732
947
|
traceEvent("speculative_read_blocked", {
|
|
733
948
|
id: tc.id,
|
|
734
949
|
name: tc.name,
|
|
@@ -814,6 +1029,27 @@ export class Agent {
|
|
|
814
1029
|
result = next;
|
|
815
1030
|
},
|
|
816
1031
|
});
|
|
1032
|
+
const postToolHook = await this.runExternalHook({
|
|
1033
|
+
eventName: result.isError ? "PostToolUseFailure" : "PostToolUse",
|
|
1034
|
+
cwd,
|
|
1035
|
+
runId,
|
|
1036
|
+
target: tc.name,
|
|
1037
|
+
payload: {
|
|
1038
|
+
id: tc.id,
|
|
1039
|
+
name: tc.name,
|
|
1040
|
+
argsPreview: truncateHookText(tc.arguments, 1000),
|
|
1041
|
+
resultPreview: truncateHookText(result.content, 1000),
|
|
1042
|
+
isError: result.isError === true,
|
|
1043
|
+
},
|
|
1044
|
+
fullPayload: {
|
|
1045
|
+
toolArgs: tc.parsedArgs,
|
|
1046
|
+
toolArguments: tc.arguments,
|
|
1047
|
+
toolResult: result,
|
|
1048
|
+
},
|
|
1049
|
+
}, abortSignal);
|
|
1050
|
+
for (const event of postToolHook.events)
|
|
1051
|
+
yield emit(event);
|
|
1052
|
+
this.injectHookModelContext(postToolHook.result);
|
|
817
1053
|
// Honor the model's server-declared per-tool-output token cap (e.g.
|
|
818
1054
|
// gpt-5.5 reports 10000). Without this, 4-5 large file reads in a row
|
|
819
1055
|
// blow past the input window even though our local estimate looks fine.
|
|
@@ -885,13 +1121,28 @@ export class Agent {
|
|
|
885
1121
|
flushReminders: flushGovernorReminders,
|
|
886
1122
|
});
|
|
887
1123
|
flushGovernorReminders();
|
|
1124
|
+
const stopHook = await this.runExternalHook({
|
|
1125
|
+
eventName: "Stop",
|
|
1126
|
+
cwd,
|
|
1127
|
+
runId,
|
|
1128
|
+
target: "turn",
|
|
1129
|
+
payload: {
|
|
1130
|
+
providerId: this.providerId,
|
|
1131
|
+
model: this.apiModel,
|
|
1132
|
+
mode: this._mode,
|
|
1133
|
+
assistantChars: assistantMsg.content.length,
|
|
1134
|
+
toolCalls: assistantMsg.toolCalls?.length ?? 0,
|
|
1135
|
+
},
|
|
1136
|
+
}, abortSignal);
|
|
1137
|
+
for (const event of stopHook.events)
|
|
1138
|
+
yield emit(event);
|
|
888
1139
|
const willContinue = !!hookState.forceContinuationReason;
|
|
889
1140
|
yield emit({ type: "turn_end", usage: turnUsage, willContinue });
|
|
890
1141
|
if (willContinue) {
|
|
891
1142
|
delete hookState.forceContinuationReason;
|
|
892
1143
|
continue;
|
|
893
1144
|
}
|
|
894
|
-
for (const event of rejectPendingInputs("no_continuation"))
|
|
1145
|
+
for (const event of await rejectPendingInputs("no_continuation"))
|
|
895
1146
|
yield emit(event);
|
|
896
1147
|
break;
|
|
897
1148
|
}
|
|
@@ -913,6 +1164,19 @@ export class Agent {
|
|
|
913
1164
|
yield emit({ type: "todos_updated", todos: this.getTodos() });
|
|
914
1165
|
}
|
|
915
1166
|
}
|
|
1167
|
+
else {
|
|
1168
|
+
const stopFailureHook = await this.runExternalHook({
|
|
1169
|
+
eventName: "StopFailure",
|
|
1170
|
+
cwd,
|
|
1171
|
+
runId,
|
|
1172
|
+
target: "run_error",
|
|
1173
|
+
payload: {
|
|
1174
|
+
error: summarizeTraceError(error),
|
|
1175
|
+
},
|
|
1176
|
+
}, abortSignal);
|
|
1177
|
+
for (const event of stopFailureHook.events)
|
|
1178
|
+
yield emit(event);
|
|
1179
|
+
}
|
|
916
1180
|
throw error;
|
|
917
1181
|
}
|
|
918
1182
|
finally {
|
|
@@ -1226,6 +1490,26 @@ export class Agent {
|
|
|
1226
1490
|
this.pendingSubagentUpdates.push({ id: record.parentToolCallId, name: record.parentToolName, update });
|
|
1227
1491
|
}
|
|
1228
1492
|
};
|
|
1493
|
+
const runSubagentLifecycleHook = async (eventName, status, error) => {
|
|
1494
|
+
try {
|
|
1495
|
+
await this.runExternalHook({
|
|
1496
|
+
eventName,
|
|
1497
|
+
cwd,
|
|
1498
|
+
runId: record.runId,
|
|
1499
|
+
target: record.profile.name,
|
|
1500
|
+
payload: {
|
|
1501
|
+
agentId: record.agentId,
|
|
1502
|
+
nickname: record.nickname,
|
|
1503
|
+
profile: record.profile.name,
|
|
1504
|
+
status,
|
|
1505
|
+
error,
|
|
1506
|
+
},
|
|
1507
|
+
}, options.abortSignal);
|
|
1508
|
+
}
|
|
1509
|
+
catch {
|
|
1510
|
+
// Subagent lifecycle hooks are observe-only; never fail the subagent.
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1229
1513
|
const allTools = [...this.tools.values()];
|
|
1230
1514
|
const diagnostics = validateAgentProfileTools(allTools, record.profile, options.approval);
|
|
1231
1515
|
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
@@ -1236,6 +1520,7 @@ export class Agent {
|
|
|
1236
1520
|
record.status = "blocked";
|
|
1237
1521
|
record.error = blockingDiagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
1238
1522
|
record.updatedAt = Date.now();
|
|
1523
|
+
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1239
1524
|
emit("blocked", undefined, record.error);
|
|
1240
1525
|
this.notifySubagentWaiters(record);
|
|
1241
1526
|
return;
|
|
@@ -1251,6 +1536,7 @@ export class Agent {
|
|
|
1251
1536
|
record.status = "blocked";
|
|
1252
1537
|
record.error = error?.message || String(error);
|
|
1253
1538
|
record.updatedAt = Date.now();
|
|
1539
|
+
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1254
1540
|
emit("blocked", undefined, record.error);
|
|
1255
1541
|
this.notifySubagentWaiters(record);
|
|
1256
1542
|
return;
|
|
@@ -1258,6 +1544,7 @@ export class Agent {
|
|
|
1258
1544
|
record.agent = subAgent;
|
|
1259
1545
|
record.status = "running";
|
|
1260
1546
|
record.updatedAt = Date.now();
|
|
1547
|
+
await runSubagentLifecycleHook("SubagentStart", record.status);
|
|
1261
1548
|
emit("running", undefined, `Running ${record.nickname} (${record.profile.name})...`);
|
|
1262
1549
|
let turnSummaryBuffer = "";
|
|
1263
1550
|
let turnHadToolCall = false;
|
|
@@ -1304,6 +1591,7 @@ export class Agent {
|
|
|
1304
1591
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
1305
1592
|
record.error = error?.message || String(error);
|
|
1306
1593
|
record.updatedAt = Date.now();
|
|
1594
|
+
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1307
1595
|
emit(record.status, undefined, record.error);
|
|
1308
1596
|
this.notifySubagentWaiters(record);
|
|
1309
1597
|
return;
|
|
@@ -1315,6 +1603,7 @@ export class Agent {
|
|
|
1315
1603
|
record.status = "completed";
|
|
1316
1604
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
1317
1605
|
record.updatedAt = Date.now();
|
|
1606
|
+
await runSubagentLifecycleHook("SubagentStop", record.status);
|
|
1318
1607
|
emit("completed", undefined, record.summary || `${record.nickname} completed`);
|
|
1319
1608
|
this.notifySubagentWaiters(record);
|
|
1320
1609
|
}
|
|
@@ -1394,6 +1683,9 @@ export class Agent {
|
|
|
1394
1683
|
budgetSource: { runId: record.runId, subAgentId: record.agentId },
|
|
1395
1684
|
systemPrompt: childSystemPrompt,
|
|
1396
1685
|
hooks: this.hookDefinitions,
|
|
1686
|
+
externalHooks: this.externalHooks,
|
|
1687
|
+
agentRole: "subagent",
|
|
1688
|
+
subAgentId: record.agentId,
|
|
1397
1689
|
agentCategories: this.agentCategories,
|
|
1398
1690
|
providerFactory: this.providerFactory,
|
|
1399
1691
|
});
|
|
@@ -1550,7 +1842,7 @@ export class Agent {
|
|
|
1550
1842
|
message.content = sanitizeInternalReminderBlocks(message.content);
|
|
1551
1843
|
}
|
|
1552
1844
|
if (message.role === "assistant" && message.reasoning) {
|
|
1553
|
-
message.reasoning =
|
|
1845
|
+
message.reasoning = sanitizeInternalReasoningText(message.reasoning);
|
|
1554
1846
|
}
|
|
1555
1847
|
if (message.role === "assistant" && message.providerMetadata) {
|
|
1556
1848
|
message.providerMetadata = sanitizeAssistantProviderMetadata(message.providerMetadata);
|