@bubblebrain-ai/bubble 0.0.33 → 0.0.35
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/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/profiles.d.ts +0 -2
- package/dist/agent/profiles.js +0 -3
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +7 -0
- package/dist/agent/workflow/control.js +22 -1
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +7 -43
- package/dist/agent.js +50 -176
- package/dist/config.d.ts +1 -3
- package/dist/config.js +6 -5
- package/dist/context/overflow.js +2 -0
- package/dist/main.js +9 -1
- package/dist/model-catalog.d.ts +1 -1
- package/dist/model-catalog.js +25 -18
- package/dist/orchestrator/default-hooks.js +7 -1
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +21 -7
- package/dist/prompt/task-reminders.d.ts +1 -0
- package/dist/prompt/task-reminders.js +24 -1
- package/dist/provider-ai-sdk.d.ts +56 -0
- package/dist/provider-ai-sdk.js +518 -0
- package/dist/provider-registry.js +49 -3
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +8 -13
- package/dist/provider.js +4 -0
- package/dist/slash-commands/commands.js +3 -7
- package/dist/slash-commands/types.d.ts +1 -1
- package/dist/tools/agent-lifecycle.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +35 -263
- package/dist/tui-ink/app.js +51 -13
- package/dist/tui-ink/approval/approval-dialog.js +1 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- package/dist/tui-ink/input-box.d.ts +1 -6
- package/dist/tui-ink/input-box.js +14 -35
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +3 -1
- package/dist/tui-ink/model-picker.js +25 -14
- package/dist/tui-ink/plan-confirm.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +3 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +16 -30
- package/dist/variant/variant-resolver.d.ts +13 -0
- package/dist/variant/variant-resolver.js +26 -0
- package/package.json +9 -3
|
@@ -7,8 +7,8 @@ export declare class AgentAbortError extends Error {
|
|
|
7
7
|
* message strings.
|
|
8
8
|
*/
|
|
9
9
|
export declare class SubagentAbortError extends AgentAbortError {
|
|
10
|
-
readonly subagentReason: "interrupt" | "user_close"
|
|
11
|
-
constructor(message: string, subagentReason: "interrupt" | "user_close"
|
|
10
|
+
readonly subagentReason: "interrupt" | "user_close";
|
|
11
|
+
constructor(message: string, subagentReason: "interrupt" | "user_close");
|
|
12
12
|
}
|
|
13
13
|
/** Shown when the model produced no user-visible content despite recovery attempts. */
|
|
14
14
|
export declare const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
|
|
@@ -1,61 +1,20 @@
|
|
|
1
1
|
import type { TokenUsage } from "../types.js";
|
|
2
|
-
import type { SubagentTokenCap } from "./subagent-control.js";
|
|
3
2
|
export interface BudgetUsageSource {
|
|
4
3
|
runId: string;
|
|
5
4
|
subAgentId?: string;
|
|
6
5
|
}
|
|
7
|
-
export interface BudgetSnapshot {
|
|
8
|
-
spent: number;
|
|
9
|
-
limit?: number;
|
|
10
|
-
exhausted: boolean;
|
|
11
|
-
}
|
|
12
6
|
/**
|
|
13
7
|
* Shared token ledger for a parent and all of its children, with per-source
|
|
14
|
-
* accounting
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* "pool remaining"; see computeChildTokenCap.
|
|
8
|
+
* accounting. Pure bookkeeping: nothing is ever stopped for token usage —
|
|
9
|
+
* a child's only resource bound is the model context window, absorbed by
|
|
10
|
+
* compaction (design doc §6, revised 2026-07-06).
|
|
18
11
|
*/
|
|
19
12
|
export declare class BudgetLedger {
|
|
20
|
-
private readonly limit?;
|
|
21
13
|
private spent;
|
|
22
14
|
private readonly spentBySource;
|
|
23
|
-
private readonly controller;
|
|
24
|
-
constructor(limit?: number | undefined);
|
|
25
|
-
get signal(): AbortSignal;
|
|
26
15
|
recordUsage(usage: TokenUsage, source: BudgetUsageSource): void;
|
|
27
16
|
/** Tokens attributed to one child (or the parent when subAgentId is omitted). */
|
|
28
17
|
spentBy(subAgentId?: string): number;
|
|
29
|
-
|
|
30
|
-
remaining(): number | undefined;
|
|
31
|
-
get poolLimit(): number | undefined;
|
|
32
|
-
snapshot(): BudgetSnapshot;
|
|
18
|
+
totalSpent(): number;
|
|
33
19
|
}
|
|
34
|
-
/** Default absolute per-child soft cap; applies even on limit-free hosts. */
|
|
35
|
-
export declare const DEFAULT_CHILD_TOKEN_CAP = 200000;
|
|
36
|
-
/** Share of a limited pool reserved for the parent's own turns. */
|
|
37
|
-
export declare const PARENT_POOL_RESERVE_RATIO = 0.2;
|
|
38
|
-
/** Hard cap sits at least this many tokens above the soft cap (≈ 2 turns). */
|
|
39
|
-
export declare const CHILD_HARD_CAP_FLOOR = 20000;
|
|
40
|
-
/**
|
|
41
|
-
* Per-child token cap, fixed at dispatch (design doc §6). The soft cap is an
|
|
42
|
-
* absolute number (config default 200k) so it is effective on limit-free
|
|
43
|
-
* hosts; when the pool *is* limited, the fair share of what remains after the
|
|
44
|
-
* parent's reserve further bounds it. The cap never shrinks mid-run because
|
|
45
|
-
* siblings spawned later.
|
|
46
|
-
*/
|
|
47
|
-
export declare function computeChildTokenCap(options: {
|
|
48
|
-
ledger?: BudgetLedger;
|
|
49
|
-
subAgentId: string;
|
|
50
|
-
activeChildren: number;
|
|
51
|
-
configCap?: number;
|
|
52
|
-
profileMaxTokens?: number;
|
|
53
|
-
}): SubagentTokenCap;
|
|
54
|
-
/**
|
|
55
|
-
* Hard cap recomputed at each turn-boundary check: at least ~2 of this
|
|
56
|
-
* child's average turns above the soft cap, never below the absolute floor
|
|
57
|
-
* (design doc §6 — replaces the fixed 25% ratio that could be smaller than a
|
|
58
|
-
* single turn).
|
|
59
|
-
*/
|
|
60
|
-
export declare function childHardCap(soft: number, avgTurnTokens: number): number;
|
|
61
20
|
export declare function composeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
|
|
@@ -1,96 +1,26 @@
|
|
|
1
1
|
const PARENT_SOURCE_KEY = "__parent__";
|
|
2
2
|
/**
|
|
3
3
|
* Shared token ledger for a parent and all of its children, with per-source
|
|
4
|
-
* accounting
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* "pool remaining"; see computeChildTokenCap.
|
|
4
|
+
* accounting. Pure bookkeeping: nothing is ever stopped for token usage —
|
|
5
|
+
* a child's only resource bound is the model context window, absorbed by
|
|
6
|
+
* compaction (design doc §6, revised 2026-07-06).
|
|
8
7
|
*/
|
|
9
8
|
export class BudgetLedger {
|
|
10
|
-
limit;
|
|
11
9
|
spent = 0;
|
|
12
10
|
spentBySource = new Map();
|
|
13
|
-
controller = new AbortController();
|
|
14
|
-
constructor(limit) {
|
|
15
|
-
this.limit = limit;
|
|
16
|
-
}
|
|
17
|
-
get signal() {
|
|
18
|
-
return this.controller.signal;
|
|
19
|
-
}
|
|
20
11
|
recordUsage(usage, source) {
|
|
21
12
|
const delta = usage.promptTokens + usage.completionTokens;
|
|
22
13
|
this.spent += delta;
|
|
23
14
|
const key = source.subAgentId ?? PARENT_SOURCE_KEY;
|
|
24
15
|
this.spentBySource.set(key, (this.spentBySource.get(key) ?? 0) + delta);
|
|
25
|
-
if (this.limit !== undefined && this.spent >= this.limit && !this.controller.signal.aborted) {
|
|
26
|
-
this.controller.abort(budgetAbortError("Budget exhausted"));
|
|
27
|
-
}
|
|
28
16
|
}
|
|
29
17
|
/** Tokens attributed to one child (or the parent when subAgentId is omitted). */
|
|
30
18
|
spentBy(subAgentId) {
|
|
31
19
|
return this.spentBySource.get(subAgentId ?? PARENT_SOURCE_KEY) ?? 0;
|
|
32
20
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (this.limit === undefined)
|
|
36
|
-
return undefined;
|
|
37
|
-
return Math.max(0, this.limit - this.spent);
|
|
38
|
-
}
|
|
39
|
-
get poolLimit() {
|
|
40
|
-
return this.limit;
|
|
21
|
+
totalSpent() {
|
|
22
|
+
return this.spent;
|
|
41
23
|
}
|
|
42
|
-
snapshot() {
|
|
43
|
-
return {
|
|
44
|
-
spent: this.spent,
|
|
45
|
-
limit: this.limit,
|
|
46
|
-
exhausted: this.limit !== undefined && this.spent >= this.limit,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/** Default absolute per-child soft cap; applies even on limit-free hosts. */
|
|
51
|
-
export const DEFAULT_CHILD_TOKEN_CAP = 200_000;
|
|
52
|
-
/** Share of a limited pool reserved for the parent's own turns. */
|
|
53
|
-
export const PARENT_POOL_RESERVE_RATIO = 0.2;
|
|
54
|
-
/** Hard cap sits at least this many tokens above the soft cap (≈ 2 turns). */
|
|
55
|
-
export const CHILD_HARD_CAP_FLOOR = 20_000;
|
|
56
|
-
/**
|
|
57
|
-
* Per-child token cap, fixed at dispatch (design doc §6). The soft cap is an
|
|
58
|
-
* absolute number (config default 200k) so it is effective on limit-free
|
|
59
|
-
* hosts; when the pool *is* limited, the fair share of what remains after the
|
|
60
|
-
* parent's reserve further bounds it. The cap never shrinks mid-run because
|
|
61
|
-
* siblings spawned later.
|
|
62
|
-
*/
|
|
63
|
-
export function computeChildTokenCap(options) {
|
|
64
|
-
let soft = options.configCap ?? DEFAULT_CHILD_TOKEN_CAP;
|
|
65
|
-
if (options.profileMaxTokens !== undefined && options.profileMaxTokens > 0) {
|
|
66
|
-
soft = Math.min(soft, options.profileMaxTokens);
|
|
67
|
-
}
|
|
68
|
-
const limit = options.ledger?.poolLimit;
|
|
69
|
-
if (options.ledger && limit !== undefined) {
|
|
70
|
-
const reserve = Math.floor(limit * PARENT_POOL_RESERVE_RATIO);
|
|
71
|
-
const available = Math.max(0, (options.ledger.remaining() ?? 0) - reserve);
|
|
72
|
-
const share = Math.floor(available / (options.activeChildren + 1));
|
|
73
|
-
soft = Math.max(1, Math.min(soft, share));
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
soft,
|
|
77
|
-
hard: soft + CHILD_HARD_CAP_FLOOR,
|
|
78
|
-
baseline: options.ledger?.spentBy(options.subAgentId) ?? 0,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Hard cap recomputed at each turn-boundary check: at least ~2 of this
|
|
83
|
-
* child's average turns above the soft cap, never below the absolute floor
|
|
84
|
-
* (design doc §6 — replaces the fixed 25% ratio that could be smaller than a
|
|
85
|
-
* single turn).
|
|
86
|
-
*/
|
|
87
|
-
export function childHardCap(soft, avgTurnTokens) {
|
|
88
|
-
return soft + Math.max(CHILD_HARD_CAP_FLOOR, Math.ceil(avgTurnTokens * 2));
|
|
89
|
-
}
|
|
90
|
-
function budgetAbortError(message) {
|
|
91
|
-
const error = new Error(message);
|
|
92
|
-
error.name = "AbortError";
|
|
93
|
-
return error;
|
|
94
24
|
}
|
|
95
25
|
export function composeAbortSignals(signals) {
|
|
96
26
|
const active = signals.filter((signal) => !!signal);
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* turn-boundary budget enforcement, the handoff completeness guard, and the
|
|
9
9
|
* mapping of failures to SubagentFinalReason.
|
|
10
10
|
*/
|
|
11
|
-
import { BudgetLedger } from "./budget-ledger.js";
|
|
12
11
|
import type { SubagentRunOutcome } from "./subagent-scheduler.js";
|
|
13
12
|
import type { SubagentFinalReason, SubagentThreadRecord } from "./subagent-control.js";
|
|
14
13
|
import type { AgentEvent, Message, ToolRegistryEntry, ToolResult, ToolUpdate } from "../types.js";
|
|
@@ -24,7 +23,6 @@ export interface ChildRunOptions {
|
|
|
24
23
|
}
|
|
25
24
|
export interface ChildRunnerHost {
|
|
26
25
|
allTools(): ToolRegistryEntry[];
|
|
27
|
-
budgetLedger(): BudgetLedger | undefined;
|
|
28
26
|
emit(record: SubagentThreadRecord, options: ChildRunOptions, status: ToolUpdate["status"], event?: AgentEvent, message?: string): void;
|
|
29
27
|
runLifecycleHook(record: SubagentThreadRecord, cwd: string, eventName: "SubagentStart" | "SubagentStop", status?: string, error?: string, abortSignal?: AbortSignal): Promise<void>;
|
|
30
28
|
finalizeBlocked(record: SubagentThreadRecord, error: string, options: ChildRunOptions): void;
|
|
@@ -47,7 +45,7 @@ export declare function sanitizeSubagentSummary(value: string): string;
|
|
|
47
45
|
* to a trivial question is a complete handoff.
|
|
48
46
|
*/
|
|
49
47
|
export declare function needsExplicitFinalSummary(record: SubagentThreadRecord, executedAnyTool: boolean): boolean;
|
|
50
|
-
export declare function classifySubagentAbortReason(reason: unknown, parentSignal: AbortSignal | undefined
|
|
48
|
+
export declare function classifySubagentAbortReason(reason: unknown, parentSignal: AbortSignal | undefined): SubagentFinalReason;
|
|
51
49
|
/**
|
|
52
50
|
* Drops trailing "[model request interrupted ...]" boundary messages so a
|
|
53
51
|
* rate-limit re-entry resumes from clean history (design §4.5).
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* mapping of failures to SubagentFinalReason.
|
|
10
10
|
*/
|
|
11
11
|
import { AgentAbortError, EMPTY_ASSISTANT_FALLBACK, SubagentAbortError } from "./abort-errors.js";
|
|
12
|
-
import {
|
|
12
|
+
import { composeAbortSignals } from "./budget-ledger.js";
|
|
13
13
|
import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "../provider-artifacts.js";
|
|
14
14
|
import { isRateLimitError } from "../network/errors.js";
|
|
15
15
|
import { isProviderTransportError } from "../network/provider-transport.js";
|
|
@@ -68,13 +68,6 @@ export class ChildRunner {
|
|
|
68
68
|
let turnSummaryBuffer = "";
|
|
69
69
|
let turnHadToolCall = false;
|
|
70
70
|
let executedAnyTool = false;
|
|
71
|
-
// Per-child budget enforcement happens at turn boundaries (design §6):
|
|
72
|
-
// turn_end already carries usage, and a reminder injected here is seen by
|
|
73
|
-
// the very next provider call — unlike chunk-level aborts.
|
|
74
|
-
const cap = record.tokenCap;
|
|
75
|
-
let runTokens = 0;
|
|
76
|
-
let runTurns = 0;
|
|
77
|
-
let budgetSoftWarned = false;
|
|
78
71
|
// Re-entry after a rate limit: the input was applied on attempt 1, so the
|
|
79
72
|
// child history must not gain a second copy, and any stale interruption
|
|
80
73
|
// boundary from the failed call is stripped (design §4.5).
|
|
@@ -110,21 +103,6 @@ export class ChildRunner {
|
|
|
110
103
|
}
|
|
111
104
|
if (event.type === "turn_end" && event.usage) {
|
|
112
105
|
record.usage = mergeUsage(record.usage, event.usage);
|
|
113
|
-
runTokens += event.usage.promptTokens + event.usage.completionTokens;
|
|
114
|
-
runTurns += 1;
|
|
115
|
-
if (cap) {
|
|
116
|
-
if (!budgetSoftWarned && runTokens >= cap.soft) {
|
|
117
|
-
budgetSoftWarned = true;
|
|
118
|
-
// The hard cap is fixed when the warning fires: soft + ~2 of
|
|
119
|
-
// this child's average turns (absolute floor), so the child
|
|
120
|
-
// gets a real chance to wrap up before the kill (design §6).
|
|
121
|
-
cap.hard = childHardCap(cap.soft, runTokens / Math.max(1, runTurns));
|
|
122
|
-
subAgent.injectSystemReminder(buildChildBudgetWrapUpReminder(runTokens, cap.soft));
|
|
123
|
-
}
|
|
124
|
-
else if (budgetSoftWarned && runTokens >= cap.hard) {
|
|
125
|
-
record.abortController.abort(new SubagentAbortError(`Subagent ${record.agentId} exceeded its hard token cap (${cap.hard}).`, "budget"));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
106
|
}
|
|
129
107
|
if (event.type === "turn_end") {
|
|
130
108
|
const turnSummary = stripProviderProtocolArtifacts(turnSummaryBuffer).trim();
|
|
@@ -176,7 +154,7 @@ export class ChildRunner {
|
|
|
176
154
|
const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
|
|
177
155
|
record.status = cancelled ? "cancelled" : "failed";
|
|
178
156
|
record.finalReason = cancelled
|
|
179
|
-
? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal
|
|
157
|
+
? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal)
|
|
180
158
|
: "failed_transient";
|
|
181
159
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
182
160
|
record.error = error?.message || String(error);
|
|
@@ -267,23 +245,21 @@ export function needsExplicitFinalSummary(record, executedAnyTool) {
|
|
|
267
245
|
return false;
|
|
268
246
|
if (record.summary === EMPTY_ASSISTANT_FALLBACK)
|
|
269
247
|
return true;
|
|
270
|
-
|
|
248
|
+
// A schema-bearing child's handoff is a compact JSON value; short is
|
|
249
|
+
// complete by construction, and a restate turn would risk breaking the JSON.
|
|
250
|
+
if (!record.expectsStructuredOutput && estimateHandoffTokens(record.summary) < HANDOFF_TOKEN_FLOOR)
|
|
271
251
|
return true;
|
|
272
252
|
return isIntermediateHandoff(record.summary);
|
|
273
253
|
}
|
|
274
|
-
export function classifySubagentAbortReason(reason, parentSignal
|
|
254
|
+
export function classifySubagentAbortReason(reason, parentSignal) {
|
|
275
255
|
if (reason instanceof SubagentAbortError) {
|
|
276
256
|
switch (reason.subagentReason) {
|
|
277
257
|
case "interrupt":
|
|
278
258
|
return "cancelled_interrupt";
|
|
279
259
|
case "user_close":
|
|
280
260
|
return "cancelled_user";
|
|
281
|
-
case "budget":
|
|
282
|
-
return "cancelled_budget";
|
|
283
261
|
}
|
|
284
262
|
}
|
|
285
|
-
if (ledger?.snapshot().exhausted)
|
|
286
|
-
return "cancelled_budget";
|
|
287
263
|
if (parentSignal?.aborted)
|
|
288
264
|
return "cancelled_parent_abort";
|
|
289
265
|
return "cancelled_user";
|
|
@@ -302,13 +278,6 @@ export function stripTrailingModelInterruptedBoundary(messages) {
|
|
|
302
278
|
break;
|
|
303
279
|
}
|
|
304
280
|
}
|
|
305
|
-
function buildChildBudgetWrapUpReminder(spentTokens, softCap) {
|
|
306
|
-
return [
|
|
307
|
-
`Token budget notice: this subagent has used ~${Math.round(spentTokens)} tokens, crossing its ${softCap}-token budget.`,
|
|
308
|
-
"Wrap up now: stop opening new lines of investigation and produce your complete final handoff",
|
|
309
|
-
"(findings, conclusions, unfinished items) in your next message.",
|
|
310
|
-
].join(" ");
|
|
311
|
-
}
|
|
312
281
|
export function summarizeSubagentToolEnd(event) {
|
|
313
282
|
const metadata = (event.result.metadata ?? {});
|
|
314
283
|
const reason = readString(metadata.reason);
|
package/dist/agent/profiles.d.ts
CHANGED
|
@@ -20,8 +20,6 @@ export interface AgentProfile {
|
|
|
20
20
|
category?: string;
|
|
21
21
|
tools: AgentProfileTools;
|
|
22
22
|
maxTurns?: number;
|
|
23
|
-
/** Optional per-child token cap declared by the profile (may only lower the runtime default). */
|
|
24
|
-
maxTokens?: number;
|
|
25
23
|
approval: AgentProfileApproval;
|
|
26
24
|
nicknameCandidates?: string[];
|
|
27
25
|
prompt: string;
|
package/dist/agent/profiles.js
CHANGED
|
@@ -23,8 +23,6 @@ const SUBAGENT_DENY_TOOLS = new Set([
|
|
|
23
23
|
"send_input",
|
|
24
24
|
"close_agent",
|
|
25
25
|
"list_agents",
|
|
26
|
-
"agent_team",
|
|
27
|
-
"agent_batch",
|
|
28
26
|
"run_workflow",
|
|
29
27
|
"wait_workflow",
|
|
30
28
|
]);
|
|
@@ -323,7 +321,6 @@ function parseAgentProfileFile(raw, source, filePath) {
|
|
|
323
321
|
category: stringValue(frontmatter.category),
|
|
324
322
|
tools: toolsValue(frontmatter.tools),
|
|
325
323
|
maxTurns: numberValue(frontmatter.maxTurns),
|
|
326
|
-
maxTokens: numberValue(frontmatter.maxTokens),
|
|
327
324
|
approval: approvalValue(frontmatter.approval),
|
|
328
325
|
nicknameCandidates: stringArray(frontmatter.nicknameCandidates) ?? stringArray(frontmatter.nicknames),
|
|
329
326
|
prompt: parsed.body.trim(),
|
|
@@ -8,17 +8,8 @@ export type SubagentThreadStatus = "queued" | "running" | "completed" | "failed"
|
|
|
8
8
|
* rendered in lifecycle tool replies (design doc §3.1) — a resume hint is
|
|
9
9
|
* emitted iff the runtime judged the run resumable, never as a blanket string.
|
|
10
10
|
*/
|
|
11
|
-
export type SubagentFinalReason = "completed" | "failed_transient" | "failed_fatal" | "rate_limited_exhausted" | "blocked" | "cancelled_interrupt" | "cancelled_user" | "
|
|
11
|
+
export type SubagentFinalReason = "completed" | "failed_transient" | "failed_fatal" | "rate_limited_exhausted" | "blocked" | "cancelled_interrupt" | "cancelled_user" | "cancelled_parent_abort";
|
|
12
12
|
export declare function isResumableReason(reason: SubagentFinalReason): boolean;
|
|
13
|
-
/** Per-child token budget, fixed at dispatch time (design doc §6). */
|
|
14
|
-
export interface SubagentTokenCap {
|
|
15
|
-
/** Soft cap: crossing it injects a wrap-up reminder into the child. */
|
|
16
|
-
soft: number;
|
|
17
|
-
/** Hard cap: crossing it aborts this child only. Updated at turn checks. */
|
|
18
|
-
hard: number;
|
|
19
|
-
/** Ledger tokens already attributed to this child when the run started. */
|
|
20
|
-
baseline: number;
|
|
21
|
-
}
|
|
22
13
|
export interface SubagentThreadSnapshot {
|
|
23
14
|
agentId: string;
|
|
24
15
|
runId: string;
|
|
@@ -40,7 +31,6 @@ export interface SubagentThreadSnapshot {
|
|
|
40
31
|
deliveredAt?: number;
|
|
41
32
|
/** 1-based position in the scheduler queue while status is "queued". */
|
|
42
33
|
queuePosition?: number;
|
|
43
|
-
tokenCap?: SubagentTokenCap;
|
|
44
34
|
/** Present for write_worktree children: where the isolated checkout lives. */
|
|
45
35
|
worktree?: SubagentWorktree;
|
|
46
36
|
}
|
|
@@ -63,8 +53,11 @@ export interface SubagentThreadRecord {
|
|
|
63
53
|
createdAt: number;
|
|
64
54
|
updatedAt: number;
|
|
65
55
|
deliveredAt?: number;
|
|
66
|
-
tokenCap?: SubagentTokenCap;
|
|
67
56
|
worktree?: SubagentWorktree;
|
|
57
|
+
/** True when the child was asked for schema-validated structured output:
|
|
58
|
+
* a compact JSON handoff is complete by construction, so the token-floor
|
|
59
|
+
* restate turn must not fire on it. */
|
|
60
|
+
expectsStructuredOutput?: boolean;
|
|
68
61
|
/** True for agents spawned inside a run_workflow: kept out of list_agents /
|
|
69
62
|
* wait_agent and not persisted (design v2 appendix; option C). */
|
|
70
63
|
workflowInternal?: boolean;
|
|
@@ -9,7 +9,6 @@ export function isResumableReason(reason) {
|
|
|
9
9
|
case "completed":
|
|
10
10
|
case "failed_fatal":
|
|
11
11
|
case "blocked":
|
|
12
|
-
case "cancelled_budget":
|
|
13
12
|
return false;
|
|
14
13
|
}
|
|
15
14
|
}
|
|
@@ -33,7 +32,6 @@ export function snapshotSubagentThread(record) {
|
|
|
33
32
|
createdAt: record.createdAt,
|
|
34
33
|
updatedAt: record.updatedAt,
|
|
35
34
|
deliveredAt: record.deliveredAt,
|
|
36
|
-
tokenCap: record.tokenCap ? { ...record.tokenCap } : undefined,
|
|
37
35
|
worktree: record.worktree ? { ...record.worktree } : undefined,
|
|
38
36
|
};
|
|
39
37
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Persistence (design §7): final-state children are written to
|
|
8
8
|
* `<persistDir>/<agentId>.json` as snapshot + compacted message history, so a
|
|
9
9
|
* later process can resume them via send_input. The on-disk schema carries
|
|
10
|
-
* `finalReason` / `resumable` / `deliveredAt`
|
|
10
|
+
* `finalReason` / `resumable` / `deliveredAt` — the fields the
|
|
11
11
|
* reply protocol and delivery dedup depend on. Child transcripts never mix
|
|
12
12
|
* into the parent transcript.
|
|
13
13
|
*/
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Persistence (design §7): final-state children are written to
|
|
8
8
|
* `<persistDir>/<agentId>.json` as snapshot + compacted message history, so a
|
|
9
9
|
* later process can resume them via send_input. The on-disk schema carries
|
|
10
|
-
* `finalReason` / `resumable` / `deliveredAt`
|
|
10
|
+
* `finalReason` / `resumable` / `deliveredAt` — the fields the
|
|
11
11
|
* reply protocol and delivery dedup depend on. Child transcripts never mix
|
|
12
12
|
* into the parent transcript.
|
|
13
13
|
*/
|
|
@@ -84,7 +84,6 @@ export class SubagentStore {
|
|
|
84
84
|
createdAt: record.createdAt,
|
|
85
85
|
updatedAt: record.updatedAt,
|
|
86
86
|
deliveredAt: record.deliveredAt,
|
|
87
|
-
tokenCap: record.tokenCap,
|
|
88
87
|
messages: record.agent?.messages ?? record.messages,
|
|
89
88
|
};
|
|
90
89
|
writeFileSync(join(this.persistDir, `${record.agentId}.json`), JSON.stringify(payload));
|
|
@@ -135,7 +134,6 @@ export class SubagentStore {
|
|
|
135
134
|
createdAt: parsed.createdAt,
|
|
136
135
|
updatedAt: parsed.updatedAt,
|
|
137
136
|
deliveredAt: parsed.deliveredAt,
|
|
138
|
-
tokenCap: parsed.tokenCap,
|
|
139
137
|
abortController: new AbortController(),
|
|
140
138
|
waiters: new Set(),
|
|
141
139
|
messages: parsed.messages,
|
|
@@ -34,4 +34,11 @@ export interface WorkflowRunSnapshot {
|
|
|
34
34
|
snapshots: SubagentThreadSnapshot[];
|
|
35
35
|
}
|
|
36
36
|
/** System-reminder injected at the next turn when a background workflow finishes. */
|
|
37
|
+
/**
|
|
38
|
+
* Failed members must surface in EVERY channel that renders a finished
|
|
39
|
+
* workflow (delivery notice and wait_workflow): agent() converts failures to
|
|
40
|
+
* null inside the script, so without this line a "completed" workflow could
|
|
41
|
+
* silently omit items whose agents never delivered.
|
|
42
|
+
*/
|
|
43
|
+
export declare function workflowMemberWarning(snapshot: WorkflowRunSnapshot): string | undefined;
|
|
37
44
|
export declare function buildWorkflowDeliveryNotice(snapshot: WorkflowRunSnapshot): string;
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
/** Background dynamic-workflow run state + parent-facing snapshot (option C Phase 4). */
|
|
2
2
|
/** System-reminder injected at the next turn when a background workflow finishes. */
|
|
3
|
+
/**
|
|
4
|
+
* Failed members must surface in EVERY channel that renders a finished
|
|
5
|
+
* workflow (delivery notice and wait_workflow): agent() converts failures to
|
|
6
|
+
* null inside the script, so without this line a "completed" workflow could
|
|
7
|
+
* silently omit items whose agents never delivered.
|
|
8
|
+
*/
|
|
9
|
+
export function workflowMemberWarning(snapshot) {
|
|
10
|
+
const failed = snapshot.snapshots.filter((agent) => agent.status !== "completed" && agent.status !== "closed");
|
|
11
|
+
if (failed.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
const names = failed.map((agent) => `${agent.nickname} (${agent.status}${agent.error ? `: ${truncate(agent.error, 120)}` : ""})`).join("; ");
|
|
14
|
+
return `warning: ${failed.length} of ${snapshot.agentCount} agents did not complete — ${names}. Their agent() calls returned null; treat those items as missing, not done.`;
|
|
15
|
+
}
|
|
3
16
|
export function buildWorkflowDeliveryNotice(snapshot) {
|
|
4
17
|
const head = `workflow "${snapshot.title}" (${snapshot.runId}) ${snapshot.status} — ${snapshot.agentCount} agent${snapshot.agentCount === 1 ? "" : "s"}.`;
|
|
5
18
|
const lines = [head];
|
|
19
|
+
const warning = workflowMemberWarning(snapshot);
|
|
20
|
+
if (warning)
|
|
21
|
+
lines.push(warning);
|
|
6
22
|
if (snapshot.result?.ok) {
|
|
7
23
|
const rendered = typeof snapshot.result.value === "string"
|
|
8
24
|
? snapshot.result.value
|
|
@@ -12,7 +28,12 @@ export function buildWorkflowDeliveryNotice(snapshot) {
|
|
|
12
28
|
else if (snapshot.result && !snapshot.result.ok) {
|
|
13
29
|
lines.push(`error: ${snapshot.result.error}`);
|
|
14
30
|
}
|
|
15
|
-
|
|
31
|
+
if (snapshot.result && !snapshot.result.ok) {
|
|
32
|
+
lines.push("The workflow failed. If the error is in the script, fix it and issue a corrected run_workflow; do not integrate partial results as if complete.");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
lines.push("Do not re-run this workflow; integrate its result.");
|
|
36
|
+
}
|
|
16
37
|
return lines.join("\n");
|
|
17
38
|
}
|
|
18
39
|
function truncate(value, max) {
|
|
@@ -18,9 +18,8 @@ export interface WorkflowAgentOpts {
|
|
|
18
18
|
category?: string;
|
|
19
19
|
agentType?: string;
|
|
20
20
|
schema?: unknown;
|
|
21
|
+
/** Display name for this member in progress views (wired to the record nickname). */
|
|
21
22
|
label?: string;
|
|
22
|
-
phase?: string;
|
|
23
|
-
isolation?: string;
|
|
24
23
|
}
|
|
25
24
|
export interface WorkflowAgentSpec {
|
|
26
25
|
prompt: string;
|
|
@@ -72,4 +71,18 @@ export declare class WorkflowConcurrencyGate {
|
|
|
72
71
|
acquire(): Promise<void>;
|
|
73
72
|
release(): void;
|
|
74
73
|
}
|
|
74
|
+
/** The EXACT source runWorkflow executes — the syntax probe must compile this, not the raw script. */
|
|
75
|
+
export declare function buildWrappedWorkflowSource(script: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Submit-time syntax check: compiles the exact transformed source with
|
|
78
|
+
* QuickJS's compile-only flag (same engine, zero grammar divergence) so the
|
|
79
|
+
* model gets a teaching error as the immediate tool result instead of one
|
|
80
|
+
* full turn later through the background-run delivery path.
|
|
81
|
+
*/
|
|
82
|
+
export declare function precompileWorkflowScript(script: string): Promise<{
|
|
83
|
+
ok: true;
|
|
84
|
+
} | {
|
|
85
|
+
ok: false;
|
|
86
|
+
error: string;
|
|
87
|
+
}>;
|
|
75
88
|
export declare function runWorkflow(options: RunWorkflowOptions): Promise<RunWorkflowResult>;
|