@bubblebrain-ai/bubble 0.0.45 → 0.0.47
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/child-runner.js +11 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
- package/dist/agent/internal-reminder-sanitizer.js +13 -0
- package/dist/agent/subagent/router.d.ts +65 -0
- package/dist/agent/subagent/router.js +198 -0
- package/dist/agent/subagent/runtime.d.ts +203 -0
- package/dist/agent/subagent/runtime.js +789 -0
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-store.d.ts +18 -2
- package/dist/agent/subagent-store.js +32 -1
- package/dist/agent/tool-intent.d.ts +10 -0
- package/dist/agent/tool-intent.js +18 -0
- package/dist/agent/tool-visibility.d.ts +10 -0
- package/dist/agent/tool-visibility.js +6 -0
- package/dist/agent/workflow/runs.d.ts +72 -0
- package/dist/agent/workflow/runs.js +157 -0
- package/dist/agent.d.ts +78 -96
- package/dist/agent.js +266 -1072
- package/dist/context/compact-llm.js +14 -5
- package/dist/context/compact.d.ts +9 -0
- package/dist/context/compact.js +36 -10
- package/dist/context/compaction-files.d.ts +44 -0
- package/dist/context/compaction-files.js +148 -0
- package/dist/context/llm-compactor.js +24 -5
- package/dist/feishu/agent-host/run-driver.js +3 -0
- package/dist/feishu/serve.js +13 -9
- package/dist/goal/tools.js +12 -3
- package/dist/main.js +24 -19
- package/dist/model-catalog.js +36 -2
- package/dist/model-config.d.ts +2 -0
- package/dist/model-data.generated.d.ts +16 -0
- package/dist/model-data.generated.js +275 -0
- package/dist/model-pricing.d.ts +11 -7
- package/dist/model-pricing.js +0 -0
- package/dist/oauth/grok.d.ts +1 -1
- package/dist/oauth/grok.js +58 -29
- package/dist/oauth/openai-codex.js +13 -0
- package/dist/oauth/storage.d.ts +9 -2
- package/dist/oauth/storage.js +16 -9
- package/dist/orchestrator/default-hooks.d.ts +20 -0
- package/dist/orchestrator/default-hooks.js +47 -295
- package/dist/orchestrator/hooks.d.ts +6 -39
- package/dist/permission/mode.js +1 -1
- package/dist/prompt/reminders.d.ts +9 -35
- package/dist/prompt/reminders.js +30 -136
- package/dist/prompt/task-reminders.d.ts +0 -31
- package/dist/prompt/task-reminders.js +0 -87
- package/dist/provider-ai-sdk.d.ts +2 -0
- package/dist/provider-ai-sdk.js +1 -0
- package/dist/provider-anthropic.d.ts +2 -0
- package/dist/provider-anthropic.js +3 -1
- package/dist/provider-ark-responses.d.ts +2 -0
- package/dist/provider-ark-responses.js +1 -0
- package/dist/provider-registry.d.ts +13 -0
- package/dist/provider-registry.js +83 -0
- package/dist/provider-transform.js +37 -1
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +13 -1
- package/dist/sdk/index.d.ts +152 -0
- package/dist/sdk/index.js +400 -0
- package/dist/session.js +9 -1
- package/dist/slash-commands/commands.js +33 -19
- package/dist/tools/exit-plan-mode.js +8 -0
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +11 -5
- package/dist/tui-ink/app.d.ts +2 -6
- package/dist/tui-ink/app.js +11 -18
- package/dist/tui-ink/display-history.d.ts +0 -2
- package/dist/tui-ink/display-history.js +0 -52
- package/dist/tui-ink/footer.js +2 -2
- package/dist/tui-ink/message-list.js +62 -23
- package/dist/tui-ink/run.js +10 -5
- package/dist/tui-ink/sync-output.d.ts +17 -0
- package/dist/tui-ink/sync-output.js +35 -0
- package/dist/tui-ink/theme.d.ts +0 -1
- package/dist/tui-ink/theme.js +0 -2
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/types.d.ts +21 -1
- package/package.json +16 -2
- package/dist/agent/discovery-barrier.d.ts +0 -21
- package/dist/agent/discovery-barrier.js +0 -173
- package/dist/agent/evidence-tracker.d.ts +0 -15
- package/dist/agent/evidence-tracker.js +0 -93
- package/dist/agent/execution-governor.d.ts +0 -36
- package/dist/agent/execution-governor.js +0 -218
- package/dist/agent/task-classifier.d.ts +0 -3
- package/dist/agent/task-classifier.js +0 -114
- package/dist/agent/task-size.d.ts +0 -9
- package/dist/agent/task-size.js +0 -33
- package/dist/agent/tool-arbiter.d.ts +0 -7
- package/dist/agent/tool-arbiter.js +0 -33
- package/dist/orchestrator/workflow.d.ts +0 -10
- package/dist/orchestrator/workflow.js +0 -22
|
@@ -61,6 +61,11 @@ export interface SubagentThreadRecord {
|
|
|
61
61
|
/** True for agents spawned inside a run_workflow: kept out of list_agents /
|
|
62
62
|
* wait_agent and not persisted (design v2 appendix; option C). */
|
|
63
63
|
workflowInternal?: boolean;
|
|
64
|
+
/** A SubagentStart hook fired for the current logical run and its paired
|
|
65
|
+
* SubagentStop has not fired yet (design §9). Lets terminal paths that
|
|
66
|
+
* assume "the run never started" — cancel during a retry backoff — close
|
|
67
|
+
* the pair instead of leaking an unmatched Start. */
|
|
68
|
+
hookStopPending?: boolean;
|
|
64
69
|
abortController: AbortController;
|
|
65
70
|
waiters: Set<() => void>;
|
|
66
71
|
agent?: {
|
|
@@ -13,9 +13,25 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type SubagentThreadRecord, type SubagentThreadStatus } from "./subagent-control.js";
|
|
15
15
|
export declare class SubagentStore {
|
|
16
|
-
private readonly persistDir?;
|
|
17
16
|
private readonly threads;
|
|
18
|
-
|
|
17
|
+
private persistDir?;
|
|
18
|
+
constructor(persistDir?: string);
|
|
19
|
+
/**
|
|
20
|
+
* Repoints persistence at a different session's directory (the TUI reuses
|
|
21
|
+
* one Agent across session switches; see Agent.setSessionID).
|
|
22
|
+
*
|
|
23
|
+
* Final non-workflow records are evicted first: they are already persisted
|
|
24
|
+
* in the OLD directory and reloadable by switching back, and keeping them
|
|
25
|
+
* would leak one session's children into another's list_agents — growing
|
|
26
|
+
* monotonically with every switch. Live records stay (their runs are still
|
|
27
|
+
* attached to this process; their eventual persist lands in the new
|
|
28
|
+
* directory — an accepted tradeoff, preferable to losing them). Workflow-
|
|
29
|
+
* internal records stay because a running workflow's schema-correction
|
|
30
|
+
* retry looks its members up in this store by agentId.
|
|
31
|
+
*
|
|
32
|
+
* `undefined` disables persistence and loads nothing; threads are kept.
|
|
33
|
+
*/
|
|
34
|
+
repoint(persistDir: string | undefined): void;
|
|
19
35
|
get(agentId: string): SubagentThreadRecord | undefined;
|
|
20
36
|
set(record: SubagentThreadRecord): void;
|
|
21
37
|
values(): SubagentThreadRecord[];
|
|
@@ -16,11 +16,37 @@ import { join } from "node:path";
|
|
|
16
16
|
import { isFinalSubagentThreadStatus, } from "./subagent-control.js";
|
|
17
17
|
const PERSIST_SCHEMA_VERSION = 1;
|
|
18
18
|
export class SubagentStore {
|
|
19
|
-
persistDir;
|
|
20
19
|
threads = new Map();
|
|
20
|
+
persistDir;
|
|
21
21
|
constructor(persistDir) {
|
|
22
22
|
this.persistDir = persistDir;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Repoints persistence at a different session's directory (the TUI reuses
|
|
26
|
+
* one Agent across session switches; see Agent.setSessionID).
|
|
27
|
+
*
|
|
28
|
+
* Final non-workflow records are evicted first: they are already persisted
|
|
29
|
+
* in the OLD directory and reloadable by switching back, and keeping them
|
|
30
|
+
* would leak one session's children into another's list_agents — growing
|
|
31
|
+
* monotonically with every switch. Live records stay (their runs are still
|
|
32
|
+
* attached to this process; their eventual persist lands in the new
|
|
33
|
+
* directory — an accepted tradeoff, preferable to losing them). Workflow-
|
|
34
|
+
* internal records stay because a running workflow's schema-correction
|
|
35
|
+
* retry looks its members up in this store by agentId.
|
|
36
|
+
*
|
|
37
|
+
* `undefined` disables persistence and loads nothing; threads are kept.
|
|
38
|
+
*/
|
|
39
|
+
repoint(persistDir) {
|
|
40
|
+
if (persistDir === this.persistDir)
|
|
41
|
+
return;
|
|
42
|
+
for (const [agentId, record] of this.threads) {
|
|
43
|
+
if (isFinalSubagentThreadStatus(record.status) && !record.workflowInternal) {
|
|
44
|
+
this.threads.delete(agentId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.persistDir = persistDir;
|
|
48
|
+
this.loadPersisted();
|
|
49
|
+
}
|
|
24
50
|
get(agentId) {
|
|
25
51
|
return this.threads.get(agentId);
|
|
26
52
|
}
|
|
@@ -62,6 +88,11 @@ export class SubagentStore {
|
|
|
62
88
|
return;
|
|
63
89
|
if (!isFinalSubagentThreadStatus(record.status))
|
|
64
90
|
return;
|
|
91
|
+
// Workflow-internal members never persist (design: they never re-import
|
|
92
|
+
// on restart). The onFinal gate alone was not enough — markDelivered
|
|
93
|
+
// reaches persist too (known-defects #5) — so the store enforces it.
|
|
94
|
+
if (record.workflowInternal)
|
|
95
|
+
return;
|
|
65
96
|
try {
|
|
66
97
|
mkdirSync(this.persistDir, { recursive: true });
|
|
67
98
|
const payload = {
|
|
@@ -24,6 +24,16 @@ export interface ParsedSearchCommand {
|
|
|
24
24
|
pattern: string;
|
|
25
25
|
path?: string;
|
|
26
26
|
include?: string;
|
|
27
|
+
/**
|
|
28
|
+
* True only when a structured-grep rewrite would execute EXACTLY what the
|
|
29
|
+
* model asked for. False whenever anything was dropped or reinterpreted:
|
|
30
|
+
* unknown flags (-i, -A, -w, ...), --iglob (case-insensitivity lost),
|
|
31
|
+
* --include under rg (not an rg flag - a faithful run would error), more
|
|
32
|
+
* than one path, or the `grep` binary itself (BRE dialect vs the tool's
|
|
33
|
+
* rg engine). Observation (intent classification) tolerates lossy parses;
|
|
34
|
+
* execution rewriting must not (tool-arbiter).
|
|
35
|
+
*/
|
|
36
|
+
lossless: boolean;
|
|
27
37
|
}
|
|
28
38
|
export interface ParsedReadCommand {
|
|
29
39
|
path: string;
|
|
@@ -79,6 +79,10 @@ export function parseSearchBashCommand(command) {
|
|
|
79
79
|
if (!["grep", "rg", "ripgrep"].includes(binary)) {
|
|
80
80
|
return undefined;
|
|
81
81
|
}
|
|
82
|
+
// The structured grep tool shells out to rg, so only rg-origin commands
|
|
83
|
+
// can be dialect-identical; GNU grep patterns are BRE and must not be
|
|
84
|
+
// silently re-executed under rg's Rust regex engine.
|
|
85
|
+
let lossless = binary !== "grep";
|
|
82
86
|
const positional = [];
|
|
83
87
|
let include;
|
|
84
88
|
for (let index = 1; index < tokens.length; index++) {
|
|
@@ -87,14 +91,24 @@ export function parseSearchBashCommand(command) {
|
|
|
87
91
|
continue;
|
|
88
92
|
if (token === "--glob" || token === "--iglob" || token === "--include") {
|
|
89
93
|
include = tokens[index + 1];
|
|
94
|
+
// --iglob loses case-insensitivity in translation; --include is not an
|
|
95
|
+
// rg flag at all (a faithful run would error, and hiding that from the
|
|
96
|
+
// model masks its mistake). Only --glob maps 1:1 onto the tool.
|
|
97
|
+
if (token !== "--glob")
|
|
98
|
+
lossless = false;
|
|
90
99
|
index += 1;
|
|
91
100
|
continue;
|
|
92
101
|
}
|
|
93
102
|
if (token.startsWith("--glob=") || token.startsWith("--iglob=") || token.startsWith("--include=")) {
|
|
94
103
|
include = token.slice(token.indexOf("=") + 1);
|
|
104
|
+
if (!token.startsWith("--glob="))
|
|
105
|
+
lossless = false;
|
|
95
106
|
continue;
|
|
96
107
|
}
|
|
97
108
|
if (token.startsWith("-")) {
|
|
109
|
+
// Any other flag (-i, -A 3, -w, -l, ...) has no representation in the
|
|
110
|
+
// structured tool; dropping it would change the search's meaning.
|
|
111
|
+
lossless = false;
|
|
98
112
|
continue;
|
|
99
113
|
}
|
|
100
114
|
positional.push(token);
|
|
@@ -102,11 +116,15 @@ export function parseSearchBashCommand(command) {
|
|
|
102
116
|
if (positional.length === 0) {
|
|
103
117
|
return undefined;
|
|
104
118
|
}
|
|
119
|
+
// A third positional (multiple search roots) cannot be expressed either.
|
|
120
|
+
if (positional.length > 2)
|
|
121
|
+
lossless = false;
|
|
105
122
|
const [pattern, maybePath] = positional;
|
|
106
123
|
return {
|
|
107
124
|
pattern,
|
|
108
125
|
path: maybePath,
|
|
109
126
|
include,
|
|
127
|
+
lossless,
|
|
110
128
|
};
|
|
111
129
|
}
|
|
112
130
|
export function parseReadBashCommand(command) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript visibility of tool results.
|
|
3
|
+
*
|
|
4
|
+
* `hiddenFromTranscript` is a general mechanism: a hook may block a tool call
|
|
5
|
+
* and mark the exchange hidden so it never reaches the model's transcript or
|
|
6
|
+
* the UI. These helpers are the canonical way to test for it.
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolResult, ToolResultMetadata } from "../types.js";
|
|
9
|
+
export declare function isHiddenToolResult(result: ToolResult | undefined): result is ToolResult;
|
|
10
|
+
export declare function isHiddenToolMetadata(metadata: ToolResultMetadata | undefined): boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type WorkflowRunSnapshot } from "./control.js";
|
|
2
|
+
import type { AgentProfile } from "../profiles.js";
|
|
3
|
+
import type { SubagentThreadSnapshot } from "../subagent-control.js";
|
|
4
|
+
import type { ToolUpdate } from "../../types.js";
|
|
5
|
+
export interface WorkflowExecuteOptions {
|
|
6
|
+
script: string;
|
|
7
|
+
args?: unknown;
|
|
8
|
+
parentToolCallId: string;
|
|
9
|
+
abortSignal?: AbortSignal;
|
|
10
|
+
directEmit?: (update: ToolUpdate) => void;
|
|
11
|
+
queueUpdates?: boolean;
|
|
12
|
+
ensureProfileTrusted?: (profile: AgentProfile) => Promise<{
|
|
13
|
+
content: string | unknown;
|
|
14
|
+
} | undefined>;
|
|
15
|
+
}
|
|
16
|
+
export interface WorkflowExecuteOutcome {
|
|
17
|
+
result: {
|
|
18
|
+
ok: true;
|
|
19
|
+
value: unknown;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
agentCount: number;
|
|
25
|
+
logs: string[];
|
|
26
|
+
snapshots: SubagentThreadSnapshot[];
|
|
27
|
+
}
|
|
28
|
+
export interface WorkflowLedgerDeps {
|
|
29
|
+
/** Runs the script. Supplied by Agent — the ledger never executes anything itself. */
|
|
30
|
+
execute(cwd: string, options: WorkflowExecuteOptions): Promise<WorkflowExecuteOutcome>;
|
|
31
|
+
}
|
|
32
|
+
export declare class WorkflowLedger {
|
|
33
|
+
private readonly deps;
|
|
34
|
+
private readonly runs;
|
|
35
|
+
/** Completed runs whose result has not yet been announced to the parent. */
|
|
36
|
+
private readonly pendingDeliveries;
|
|
37
|
+
constructor(deps: WorkflowLedgerDeps);
|
|
38
|
+
hasRunning(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Starts a workflow in the BACKGROUND: returns a runId immediately; the
|
|
41
|
+
* script runs detached, its agents stream progress through the queued
|
|
42
|
+
* channel (drained at turn boundaries like spawn_agent), and its result is
|
|
43
|
+
* ingested at the next turn. Collect explicitly with wait().
|
|
44
|
+
*/
|
|
45
|
+
start(cwd: string, options: {
|
|
46
|
+
script: string;
|
|
47
|
+
args?: unknown;
|
|
48
|
+
title?: string;
|
|
49
|
+
parentToolCallId: string;
|
|
50
|
+
abortSignal?: AbortSignal;
|
|
51
|
+
ensureProfileTrusted?: (profile: AgentProfile) => Promise<{
|
|
52
|
+
content: string | unknown;
|
|
53
|
+
} | undefined>;
|
|
54
|
+
}): {
|
|
55
|
+
runId: string;
|
|
56
|
+
title: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Blocks until a background workflow reaches a final state (or times out).
|
|
60
|
+
* `timeoutMs` must already be normalized by the caller.
|
|
61
|
+
*/
|
|
62
|
+
wait(runId: string, timeoutMs: number): Promise<WorkflowRunSnapshot | undefined>;
|
|
63
|
+
/** Cancels a running background workflow. */
|
|
64
|
+
close(runId: string): WorkflowRunSnapshot | undefined;
|
|
65
|
+
list(): WorkflowRunSnapshot[];
|
|
66
|
+
/**
|
|
67
|
+
* Completed background-workflow results to announce before the next turn
|
|
68
|
+
* (§5 analog). Marks each record delivered, so a notice is emitted once.
|
|
69
|
+
*/
|
|
70
|
+
drainDeliveryNotices(): string[];
|
|
71
|
+
private snapshot;
|
|
72
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background workflow run ledger (option C Phase 4).
|
|
3
|
+
*
|
|
4
|
+
* Owns run records, waiters, cancellation and the pending-delivery set —
|
|
5
|
+
* everything about tracking a detached workflow EXCEPT actually executing the
|
|
6
|
+
* script, which stays on Agent (it needs subagent dispatch, profiles and the
|
|
7
|
+
* concurrency gate) and arrives here as the single injected `execute` callback.
|
|
8
|
+
*
|
|
9
|
+
* Delivery notices are PULLED (`drainDeliveryNotices`) rather than pushed into
|
|
10
|
+
* the parent, matching ResultIntegrator.drainNotices: the ledger never holds a
|
|
11
|
+
* reference back to the agent.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { getBubbleHome } from "../../bubble-home.js";
|
|
17
|
+
import { composeAbortSignals } from "../budget-ledger.js";
|
|
18
|
+
import { buildWorkflowDeliveryNotice, renderWorkflowResultValue } from "./control.js";
|
|
19
|
+
export class WorkflowLedger {
|
|
20
|
+
deps;
|
|
21
|
+
runs = new Map();
|
|
22
|
+
/** Completed runs whose result has not yet been announced to the parent. */
|
|
23
|
+
pendingDeliveries = new Set();
|
|
24
|
+
constructor(deps) {
|
|
25
|
+
this.deps = deps;
|
|
26
|
+
}
|
|
27
|
+
hasRunning() {
|
|
28
|
+
return [...this.runs.values()].some((record) => record.status === "running");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Starts a workflow in the BACKGROUND: returns a runId immediately; the
|
|
32
|
+
* script runs detached, its agents stream progress through the queued
|
|
33
|
+
* channel (drained at turn boundaries like spawn_agent), and its result is
|
|
34
|
+
* ingested at the next turn. Collect explicitly with wait().
|
|
35
|
+
*/
|
|
36
|
+
start(cwd, options) {
|
|
37
|
+
const runId = randomUUID();
|
|
38
|
+
const abortController = new AbortController();
|
|
39
|
+
const composed = composeAbortSignals([options.abortSignal, abortController.signal]);
|
|
40
|
+
if (composed) {
|
|
41
|
+
composed.addEventListener("abort", () => abortController.abort(composed.reason), { once: true });
|
|
42
|
+
}
|
|
43
|
+
const record = {
|
|
44
|
+
runId,
|
|
45
|
+
title: options.title ?? "workflow",
|
|
46
|
+
status: "running",
|
|
47
|
+
agentCount: 0,
|
|
48
|
+
snapshots: [],
|
|
49
|
+
logs: [],
|
|
50
|
+
abortController,
|
|
51
|
+
waiters: new Set(),
|
|
52
|
+
createdAt: Date.now(),
|
|
53
|
+
parentToolCallId: options.parentToolCallId,
|
|
54
|
+
};
|
|
55
|
+
this.runs.set(runId, record);
|
|
56
|
+
record.promise = this.deps.execute(cwd, {
|
|
57
|
+
script: options.script,
|
|
58
|
+
args: options.args,
|
|
59
|
+
parentToolCallId: options.parentToolCallId,
|
|
60
|
+
abortSignal: abortController.signal,
|
|
61
|
+
queueUpdates: true,
|
|
62
|
+
ensureProfileTrusted: options.ensureProfileTrusted,
|
|
63
|
+
}).then((out) => {
|
|
64
|
+
record.agentCount = out.agentCount;
|
|
65
|
+
record.snapshots = out.snapshots;
|
|
66
|
+
record.logs = out.logs;
|
|
67
|
+
record.result = out.result;
|
|
68
|
+
record.status = out.result.ok ? "completed" : (abortController.signal.aborted ? "cancelled" : "failed");
|
|
69
|
+
if (out.result.ok)
|
|
70
|
+
record.resultPath = persistWorkflowResult(runId, out.result.value);
|
|
71
|
+
}, (error) => {
|
|
72
|
+
record.result = { ok: false, error: error?.message || String(error) };
|
|
73
|
+
record.status = "failed";
|
|
74
|
+
}).finally(() => {
|
|
75
|
+
record.updatedAt = Date.now();
|
|
76
|
+
this.pendingDeliveries.add(runId);
|
|
77
|
+
for (const waiter of record.waiters)
|
|
78
|
+
waiter();
|
|
79
|
+
record.waiters.clear();
|
|
80
|
+
});
|
|
81
|
+
return { runId, title: record.title };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Blocks until a background workflow reaches a final state (or times out).
|
|
85
|
+
* `timeoutMs` must already be normalized by the caller.
|
|
86
|
+
*/
|
|
87
|
+
async wait(runId, timeoutMs) {
|
|
88
|
+
const record = this.runs.get(runId);
|
|
89
|
+
if (!record)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (record.status === "running") {
|
|
92
|
+
let waiter;
|
|
93
|
+
await Promise.race([
|
|
94
|
+
new Promise((resolve) => { waiter = resolve; record.waiters.add(resolve); }),
|
|
95
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
96
|
+
]).finally(() => { if (waiter)
|
|
97
|
+
record.waiters.delete(waiter); });
|
|
98
|
+
}
|
|
99
|
+
if (record.status !== "running")
|
|
100
|
+
this.pendingDeliveries.delete(runId);
|
|
101
|
+
return this.snapshot(record);
|
|
102
|
+
}
|
|
103
|
+
/** Cancels a running background workflow. */
|
|
104
|
+
close(runId) {
|
|
105
|
+
const record = this.runs.get(runId);
|
|
106
|
+
if (!record)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (record.status === "running")
|
|
109
|
+
record.abortController.abort(new Error("workflow cancelled"));
|
|
110
|
+
return this.snapshot(record);
|
|
111
|
+
}
|
|
112
|
+
list() {
|
|
113
|
+
return [...this.runs.values()].map((record) => this.snapshot(record));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Completed background-workflow results to announce before the next turn
|
|
117
|
+
* (§5 analog). Marks each record delivered, so a notice is emitted once.
|
|
118
|
+
*/
|
|
119
|
+
drainDeliveryNotices() {
|
|
120
|
+
if (this.pendingDeliveries.size === 0)
|
|
121
|
+
return [];
|
|
122
|
+
const notices = [];
|
|
123
|
+
for (const runId of [...this.pendingDeliveries]) {
|
|
124
|
+
this.pendingDeliveries.delete(runId);
|
|
125
|
+
const record = this.runs.get(runId);
|
|
126
|
+
if (!record || record.status === "running" || record.deliveredAt)
|
|
127
|
+
continue;
|
|
128
|
+
record.deliveredAt = Date.now();
|
|
129
|
+
notices.push(buildWorkflowDeliveryNotice(this.snapshot(record)));
|
|
130
|
+
}
|
|
131
|
+
return notices;
|
|
132
|
+
}
|
|
133
|
+
snapshot(record) {
|
|
134
|
+
return {
|
|
135
|
+
runId: record.runId,
|
|
136
|
+
title: record.title,
|
|
137
|
+
status: record.status,
|
|
138
|
+
agentCount: record.agentCount,
|
|
139
|
+
result: record.result,
|
|
140
|
+
resultPath: record.resultPath,
|
|
141
|
+
logs: record.logs,
|
|
142
|
+
snapshots: record.snapshots,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function persistWorkflowResult(runId, value) {
|
|
147
|
+
try {
|
|
148
|
+
const dir = join(getBubbleHome(), "workflows");
|
|
149
|
+
mkdirSync(dir, { recursive: true });
|
|
150
|
+
const path = join(dir, `${runId}.result.txt`);
|
|
151
|
+
writeFileSync(path, renderWorkflowResultValue(value));
|
|
152
|
+
return path;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type TurnHooks } from "./orchestrator/hooks.js";
|
|
|
8
8
|
import type { ExternalHookController } from "./hooks/controller.js";
|
|
9
9
|
import { type AgentCategoriesConfig, type ResolvedSubagentRoute } from "./agent/categories.js";
|
|
10
10
|
import { type AgentRoutingConfig, type RoutableModelEntry, type RoutableModelIndex, type RoutingSnapshotAccessor } from "./agent/routing-catalog.js";
|
|
11
|
+
import { SubagentRuntime } from "./agent/subagent/runtime.js";
|
|
11
12
|
import { type WorkflowRunSnapshot } from "./agent/workflow/control.js";
|
|
12
13
|
import { BudgetLedger } from "./agent/budget-ledger.js";
|
|
13
14
|
import { type AgentProfile, type SubagentRunResult } from "./agent/profiles.js";
|
|
@@ -52,6 +53,15 @@ export interface AgentOptions {
|
|
|
52
53
|
onToolResult?: (toolName: string, result: ToolResult) => void;
|
|
53
54
|
onTodosUpdate?: (todos: Todo[]) => void;
|
|
54
55
|
onModeUpdate?: (mode: PermissionMode) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Fired when MULTI-TURN compaction rewrites resident history, with the
|
|
58
|
+
* summary text. Hosts persist it (SessionManager.applyLLMCompaction) so the
|
|
59
|
+
* compacted state — including the cumulative file blocks — survives resume;
|
|
60
|
+
* without this, meta summaries live only in memory (session-log drops meta).
|
|
61
|
+
* Sub-turn summaries never fire it: they cover only the current turn, and a
|
|
62
|
+
* session-level summary entry would make resume skip earlier history.
|
|
63
|
+
*/
|
|
64
|
+
onCompactionApplied?: (summary: string) => void;
|
|
55
65
|
hooks?: TurnHooks[];
|
|
56
66
|
externalHooks?: ExternalHookController;
|
|
57
67
|
agentRole?: "parent" | "subagent";
|
|
@@ -119,7 +129,6 @@ export declare class Agent {
|
|
|
119
129
|
* If the model read the nudge once and chose not to delegate, repeating
|
|
120
130
|
* it is noise.
|
|
121
131
|
*/
|
|
122
|
-
largeTaskNudgeConsumed: boolean;
|
|
123
132
|
private _providerId;
|
|
124
133
|
private _model;
|
|
125
134
|
private tools;
|
|
@@ -129,6 +138,7 @@ export declare class Agent {
|
|
|
129
138
|
private _mode;
|
|
130
139
|
private _modeVersion;
|
|
131
140
|
private onModeUpdate?;
|
|
141
|
+
private onCompactionApplied?;
|
|
132
142
|
private _todos;
|
|
133
143
|
private _todosVersion;
|
|
134
144
|
private onTodosUpdate?;
|
|
@@ -147,32 +157,21 @@ export declare class Agent {
|
|
|
147
157
|
private fileStateTracker?;
|
|
148
158
|
private agentCategories;
|
|
149
159
|
private agentRouting;
|
|
150
|
-
|
|
151
|
-
private
|
|
152
|
-
/** Detector state (design §6): once per session, absolute defaulted count. */
|
|
153
|
-
private routingReminderFired;
|
|
154
|
-
private defaultedRoutingStreak;
|
|
155
|
-
private pendingRoutingReminder?;
|
|
160
|
+
/** Owns the routing decision chain, the §6 detector state and the §4 menu. */
|
|
161
|
+
private readonly router;
|
|
156
162
|
private providerFactory?;
|
|
157
|
-
private readonly subagentStore;
|
|
158
|
-
private readonly subagentScheduler;
|
|
159
|
-
private readonly childRunner;
|
|
160
|
-
private readonly resultIntegrator;
|
|
161
|
-
/** Background dynamic-workflow runs (option C Phase 4), keyed by runId. */
|
|
162
|
-
private readonly workflowRuns;
|
|
163
|
-
/** runIds whose completed result should be ingested at the next turn. */
|
|
164
|
-
private readonly pendingWorkflowDeliveries;
|
|
165
|
-
private subagentsConfig;
|
|
166
|
-
private readonly rateLimitPolicy?;
|
|
167
|
-
private pendingSubagentUpdates;
|
|
168
163
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* its own, so without this wake the background children's queued lifecycle
|
|
172
|
-
* updates sit undrained until the tool settles — the UI would only show
|
|
173
|
-
* subagent traces after the whole team finished.
|
|
164
|
+
* Subagent thread lifecycle + workflow script execution: the store,
|
|
165
|
+
* scheduler, ChildRunner and their dispatch/ingestion plumbing.
|
|
174
166
|
*/
|
|
175
|
-
private readonly
|
|
167
|
+
private readonly subagents;
|
|
168
|
+
/**
|
|
169
|
+
* Background dynamic-workflow runs (option C Phase 4): run records, waiters,
|
|
170
|
+
* cancellation and the pending-delivery set. Execution stays on Agent.
|
|
171
|
+
*/
|
|
172
|
+
private readonly workflowLedger;
|
|
173
|
+
private subagentsConfig;
|
|
174
|
+
private readonly rateLimitPolicy?;
|
|
176
175
|
private lastInputTokens;
|
|
177
176
|
private lastAnchorMessageCount;
|
|
178
177
|
private compactionStats;
|
|
@@ -191,6 +190,13 @@ export declare class Agent {
|
|
|
191
190
|
* accurate (background-tasks design §2.2c).
|
|
192
191
|
*/
|
|
193
192
|
setSessionID(sessionID: string | undefined): void;
|
|
193
|
+
/**
|
|
194
|
+
* Children persist next to the session file so a later process can resume
|
|
195
|
+
* them via send_input (design §7). An explicit subagents.persistDir config
|
|
196
|
+
* always wins over the sessionID derivation. Child agents themselves
|
|
197
|
+
* (agentRole "subagent") never persist children — no recursion exists.
|
|
198
|
+
*/
|
|
199
|
+
private deriveSubagentPersistDir;
|
|
194
200
|
getSessionID(): string | undefined;
|
|
195
201
|
/**
|
|
196
202
|
* Background-task reminder, state-change gated (design §2.3a): returns a
|
|
@@ -227,6 +233,20 @@ export declare class Agent {
|
|
|
227
233
|
private getActiveToolEntries;
|
|
228
234
|
injectSystemReminder(content: string): void;
|
|
229
235
|
injectModeReminder(): void;
|
|
236
|
+
/**
|
|
237
|
+
* Mode reminders are enter-announcements with no cancel semantics, so a stale
|
|
238
|
+
* one keeps describing a mode the session left long ago. Shift+Tab cycles
|
|
239
|
+
* through plan on the way to bypassPermissions, which used to leave a live
|
|
240
|
+
* "Plan mode is now ACTIVE" (plus its "call exit_plan_mode" rule) in history
|
|
241
|
+
* for the rest of the session — the model then planned instead of building,
|
|
242
|
+
* and hunted for a tool that is filtered out outside plan mode.
|
|
243
|
+
*
|
|
244
|
+
* Hidden rather than spliced: keeping the array shape intact leaves message
|
|
245
|
+
* indices (usage anchors, compaction bookkeeping) valid, and the transcript
|
|
246
|
+
* still shows what happened. Cache cost is one invalidated prefix per real
|
|
247
|
+
* mode switch — the no-op path above keeps repeat injections free.
|
|
248
|
+
*/
|
|
249
|
+
private retireStaleModeReminders;
|
|
230
250
|
get role(): "parent" | "subagent";
|
|
231
251
|
get model(): string;
|
|
232
252
|
set model(value: string);
|
|
@@ -278,32 +298,8 @@ export declare class Agent {
|
|
|
278
298
|
* if the provider stream itself errors.
|
|
279
299
|
*/
|
|
280
300
|
summarizeForCompaction(oldMessages: Message[], onDelta?: (full: string, delta: string) => void, abortSignal?: AbortSignal): Promise<string>;
|
|
281
|
-
runSubAgent(input: string | ContentPart[], cwd: string, options:
|
|
282
|
-
|
|
283
|
-
runId: string;
|
|
284
|
-
subAgentId: string;
|
|
285
|
-
parentToolCallId: string;
|
|
286
|
-
category?: string;
|
|
287
|
-
route?: ResolvedSubagentRoute;
|
|
288
|
-
approval?: "fail" | "disabled";
|
|
289
|
-
emitUpdate?: (update: ToolUpdate) => void;
|
|
290
|
-
description?: string;
|
|
291
|
-
abortSignal?: AbortSignal;
|
|
292
|
-
nickname?: string;
|
|
293
|
-
forkContext?: boolean;
|
|
294
|
-
}): Promise<SubagentRunResult>;
|
|
295
|
-
spawnSubAgent(input: string | ContentPart[], cwd: string, options: {
|
|
296
|
-
profile: AgentProfile;
|
|
297
|
-
parentToolCallId: string;
|
|
298
|
-
category?: string;
|
|
299
|
-
model?: string;
|
|
300
|
-
effort?: ThinkingLevel;
|
|
301
|
-
route?: ResolvedSubagentRoute;
|
|
302
|
-
approval?: "fail" | "disabled";
|
|
303
|
-
description?: string;
|
|
304
|
-
abortSignal?: AbortSignal;
|
|
305
|
-
forkContext?: boolean;
|
|
306
|
-
}): Promise<SubagentThreadSnapshot>;
|
|
301
|
+
runSubAgent(input: string | ContentPart[], cwd: string, options: Parameters<SubagentRuntime["runSubAgent"]>[2]): Promise<SubagentRunResult>;
|
|
302
|
+
spawnSubAgent(input: string | ContentPart[], cwd: string, options: Parameters<SubagentRuntime["spawnSubAgent"]>[2]): Promise<SubagentThreadSnapshot>;
|
|
307
303
|
waitSubAgents(options?: {
|
|
308
304
|
agentIds?: string[];
|
|
309
305
|
timeoutMs?: number;
|
|
@@ -321,9 +317,18 @@ export declare class Agent {
|
|
|
321
317
|
* never be used to answer "am I currently delegating?".
|
|
322
318
|
*/
|
|
323
319
|
activeSubAgentCount(): number;
|
|
320
|
+
listSubAgents(): SubagentThreadSnapshot[];
|
|
321
|
+
/** Marks a child's full summary as delivered to parent context (design §3.3). */
|
|
322
|
+
markSubagentDelivered(agentId: string): void;
|
|
323
|
+
/**
|
|
324
|
+
* The child store, still reachable at its old name. Several tests inspect a
|
|
325
|
+
* spawned child's record (and its live child Agent) through
|
|
326
|
+
* `(agent as any).subagentStore`; keeping this forward means the extraction
|
|
327
|
+
* changed no caller, test or otherwise.
|
|
328
|
+
*/
|
|
329
|
+
private get subagentStore();
|
|
324
330
|
/** run_workflow children are workflowInternal and invisible to listSubAgents. */
|
|
325
331
|
hasRunningWorkflow(): boolean;
|
|
326
|
-
listSubAgents(): SubagentThreadSnapshot[];
|
|
327
332
|
/**
|
|
328
333
|
* Dynamic workflow (option C): runs an LLM-authored JS orchestration script in
|
|
329
334
|
* a QuickJS sandbox. Each agent() call in the script becomes a real scheduled
|
|
@@ -379,68 +384,45 @@ export declare class Agent {
|
|
|
379
384
|
/** Cancels a running background workflow. */
|
|
380
385
|
closeWorkflow(runId: string): WorkflowRunSnapshot | undefined;
|
|
381
386
|
listWorkflows(): WorkflowRunSnapshot[];
|
|
382
|
-
private snapshotWorkflow;
|
|
383
387
|
/** Injects completed background-workflow results before the next turn (§5 analog). */
|
|
384
388
|
private flushWorkflowDeliveries;
|
|
385
|
-
private executeWorkflow;
|
|
386
|
-
/** Marks a child's full summary as delivered to parent context (design §3.3). */
|
|
387
|
-
markSubagentDelivered(agentId: string): void;
|
|
388
|
-
private snapshotSubagent;
|
|
389
|
-
/** Returns the blocking diagnostic message when the profile cannot run, else undefined. */
|
|
390
|
-
private admitSubagentProfile;
|
|
391
|
-
/**
|
|
392
|
-
* Background children (queueUpdates) get their results ingested before the
|
|
393
|
-
* parent's next inference turn (design §5); foreground children (team,
|
|
394
|
-
* legacy task) deliver through their tool result instead.
|
|
395
|
-
*/
|
|
396
|
-
private maybeEnqueueIngestion;
|
|
397
|
-
private flushSubagentIngestions;
|
|
398
|
-
private finalizeSubagentBlocked;
|
|
399
|
-
private dispatchSubagentRun;
|
|
400
|
-
private emitSubagentLifecycle;
|
|
401
|
-
/** Lets a blocked tool-execution loop drain freshly queued subagent updates. */
|
|
402
|
-
private wakeSubagentUpdateWaiters;
|
|
403
|
-
private runSubagentLifecycleHookFor;
|
|
404
389
|
/**
|
|
405
390
|
* Resolves a child's model route. Priority, highest first (design v2 §1.1):
|
|
406
391
|
* call-site override (model/effort) > profile.model > category > inherit parent.
|
|
407
392
|
* The call-site override is what lets the model say "opus for this reviewer,
|
|
408
393
|
* haiku for these twenty scouts" per spawn/batch member at request time.
|
|
409
394
|
*/
|
|
395
|
+
/** Kept as a thin delegator: routing decisions now live in SubagentRouter. */
|
|
410
396
|
private resolveRouteForSubagent;
|
|
411
|
-
/** Live snapshot for the CURRENT parent route; undefined when no accessor is wired. */
|
|
412
|
-
private currentRoutingSnapshot;
|
|
413
|
-
/**
|
|
414
|
-
* Early route validation at dispatch time (design §7). Throws with an
|
|
415
|
-
* actionable message so the model self-corrects in the same turn. The
|
|
416
|
-
* cross-provider lock (§7.2.1) is snapshot-independent; catalog checks
|
|
417
|
-
* degrade silently when no snapshot is available (§7.0).
|
|
418
|
-
*/
|
|
419
|
-
private validateRouteForDispatch;
|
|
420
397
|
/** Routable catalog across runnable providers (design v3.6); undefined when unwired. */
|
|
421
398
|
listRoutableModels(): RoutableModelEntry[] | undefined;
|
|
422
399
|
/**
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* parent
|
|
426
|
-
*
|
|
400
|
+
* The ONE place a child Agent is constructed. The runtime hands over what it
|
|
401
|
+
* resolved (provider/route/tools/prompt); everything inherited from the
|
|
402
|
+
* parent is filled in here, which is also what keeps the deliberately
|
|
403
|
+
* ABSENT arguments absent — `onMessageAppend` (a child must not append to
|
|
404
|
+
* the parent's session log), `sessionID` (a child must not stop the
|
|
405
|
+
* parent's auto servers) and the routing catalog (children route by
|
|
406
|
+
* default, not from the parent's menu).
|
|
427
407
|
*/
|
|
428
|
-
private
|
|
429
|
-
/** Consumed by the turn hooks; also closes the counting window (§6). */
|
|
430
|
-
consumePendingRoutingReminder(): string | undefined;
|
|
431
|
-
private createSubagentThreadRecord;
|
|
432
|
-
private runSubagentThread;
|
|
433
|
-
private createSubAgentInstance;
|
|
434
|
-
private resolveProviderForRoute;
|
|
408
|
+
private createChildAgent;
|
|
435
409
|
private forkMessagesForSubagent;
|
|
436
|
-
private buildSubagentUpdate;
|
|
437
|
-
private queueSubagentUpdate;
|
|
438
|
-
private drainSubagentToolUpdates;
|
|
439
|
-
private activeSubagentNicknames;
|
|
440
|
-
private resolveSubagentTargets;
|
|
441
410
|
private maybeCompactResidentHistory;
|
|
411
|
+
/**
|
|
412
|
+
* Hand a multi-turn compaction summary to the host for session persistence.
|
|
413
|
+
* Failure is swallowed: losing persistence must never break the run itself.
|
|
414
|
+
*/
|
|
415
|
+
private persistCompactionSummary;
|
|
442
416
|
private appendMessage;
|
|
443
417
|
private appendInterruptedAssistantBoundary;
|
|
444
418
|
private clearTodosAfterInterruptedRun;
|
|
445
419
|
private executeTool;
|
|
446
420
|
}
|
|
421
|
+
/** Exported for tests: the wake latch below is easiest to pin directly. */
|
|
422
|
+
export declare function createUpdateQueue<T>(): {
|
|
423
|
+
push(item: T): void;
|
|
424
|
+
drain(): T[];
|
|
425
|
+
hasItems(): boolean;
|
|
426
|
+
wait(signal?: AbortSignal): Promise<"woken" | "aborted">;
|
|
427
|
+
wake(): void;
|
|
428
|
+
};
|