@bubblebrain-ai/bubble 0.0.58 → 0.0.60
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/README.md +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
package/dist/skills/format.js
CHANGED
|
@@ -1,47 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
/** Default size of the always-present skill catalog (the listed lines, not the header). */
|
|
2
|
+
export const SKILL_CATALOG_CHARS = 12_000;
|
|
3
|
+
/** Each description is cut to this length; it only has to tell the model when to load. */
|
|
4
|
+
export const SKILL_CATALOG_DESC_CHARS = 120;
|
|
5
|
+
export const SKILL_CATALOG_HEADING = "## Skills";
|
|
6
|
+
/** First line under the heading; together they mark the catalog unambiguously. */
|
|
7
|
+
export const SKILL_CATALOG_INTRO = "Skills are task-specific instructions loaded on demand. When a task matches a skill below, call `skill` with its exact name before starting, then follow it. Load only what clearly applies.";
|
|
3
8
|
const SOURCE_PRIORITY = {
|
|
4
9
|
project: 0,
|
|
5
10
|
configured: 1,
|
|
6
11
|
user: 2,
|
|
7
12
|
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The always-present skill catalog: name and a one-line description per skill,
|
|
15
|
+
* so the model knows what exists and loads a body with `skill` only when it
|
|
16
|
+
* applies. Ordering is deterministic (source, then name) so the system prompt
|
|
17
|
+
* stays byte-identical, and cacheable, while the skill set is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function buildSkillCatalog(skills, options = {}) {
|
|
20
|
+
const budgetChars = Math.max(0, Math.floor(options.budgetChars ?? SKILL_CATALOG_CHARS));
|
|
21
|
+
const empty = { text: "", listed: 0, unlisted: skills.length, chars: 0, budgetChars };
|
|
22
|
+
if (skills.length === 0 || budgetChars === 0)
|
|
23
|
+
return empty;
|
|
11
24
|
const sorted = [...skills].sort((a, b) => {
|
|
12
25
|
const ap = SOURCE_PRIORITY[a.source ?? "user"] ?? 3;
|
|
13
26
|
const bp = SOURCE_PRIORITY[b.source ?? "user"] ?? 3;
|
|
14
27
|
if (ap !== bp)
|
|
15
28
|
return ap - bp;
|
|
16
|
-
|
|
29
|
+
// Code-point order, not locale collation: the prompt must be byte-identical across runtimes.
|
|
30
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
17
31
|
});
|
|
18
|
-
const header = [
|
|
19
|
-
"Skills provide specialized instructions and workflows for specific tasks.",
|
|
20
|
-
"Use the skill system when a task clearly matches a skill description.",
|
|
21
|
-
"Available skills:",
|
|
22
|
-
];
|
|
23
32
|
const lines = [];
|
|
24
33
|
let used = 0;
|
|
25
|
-
let dropped = 0;
|
|
26
34
|
for (const skill of sorted) {
|
|
27
35
|
const line = formatSkillLine(skill);
|
|
28
|
-
if (used + line.length + 1 >
|
|
29
|
-
dropped = sorted.length - lines.length;
|
|
36
|
+
if (used + line.length + 1 > budgetChars)
|
|
30
37
|
break;
|
|
31
|
-
}
|
|
32
38
|
lines.push(line);
|
|
33
39
|
used += line.length + 1;
|
|
34
40
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
41
|
+
if (lines.length === 0)
|
|
42
|
+
return empty;
|
|
43
|
+
const unlisted = sorted.length - lines.length;
|
|
44
|
+
if (unlisted > 0) {
|
|
45
|
+
lines.push(options.searchable
|
|
46
|
+
? `- … and ${unlisted} more skills not listed here; use skill_search to find them.`
|
|
47
|
+
: `- … and ${unlisted} more skills not listed here.`);
|
|
37
48
|
}
|
|
38
|
-
|
|
49
|
+
const text = [
|
|
50
|
+
SKILL_CATALOG_HEADING,
|
|
51
|
+
SKILL_CATALOG_INTRO,
|
|
52
|
+
...lines,
|
|
53
|
+
].join("\n");
|
|
54
|
+
return { text, listed: sorted.length - unlisted, unlisted, chars: used, budgetChars };
|
|
55
|
+
}
|
|
56
|
+
/** The catalog section text (see buildSkillCatalog). */
|
|
57
|
+
export function formatSkillsPrompt(skills, options = {}) {
|
|
58
|
+
return buildSkillCatalog(skills, options).text;
|
|
39
59
|
}
|
|
40
60
|
function formatSkillLine(skill) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
const raw = (skill.description ?? "").replace(/\s+/g, " ").trim();
|
|
62
|
+
// Cut by code points so an emoji is never split into a lone surrogate, which some APIs reject.
|
|
63
|
+
const points = Array.from(raw);
|
|
64
|
+
const desc = points.length > SKILL_CATALOG_DESC_CHARS
|
|
65
|
+
? points.slice(0, SKILL_CATALOG_DESC_CHARS - 1).join("").trimEnd() + "…"
|
|
66
|
+
: raw;
|
|
67
|
+
return desc ? `- ${skill.name}: ${desc}` : `- ${skill.name}`;
|
|
47
68
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { SlashCommand } from "./types.js";
|
|
1
2
|
import type { UnifiedCommand } from "./unified.js";
|
|
3
|
+
/** Rebuild the system prompt from the agent's live options (model, tools, memory, Skills catalog). */
|
|
4
|
+
export declare function syncSystemPrompt(ctx: Pick<Parameters<SlashCommand["handler"]>[1], "agent" | "cwd">, model: string): void;
|
|
2
5
|
/**
|
|
3
6
|
* Public export — built-in commands tagged with `source: "builtin"` so the
|
|
4
7
|
* registry and TUI can group them uniformly alongside MCP-derived commands.
|
|
@@ -3,6 +3,7 @@ import { formatContextUsage } from "../context/usage.js";
|
|
|
3
3
|
import { formatDiagnostics } from "../lsp/index.js";
|
|
4
4
|
import { normalizeNameForMCP } from "../mcp/name.js";
|
|
5
5
|
import { parseRule } from "../permissions/rule.js";
|
|
6
|
+
import { describeRepoCapabilities } from "../permissions/trust.js";
|
|
6
7
|
import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
|
|
7
8
|
import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
|
|
8
9
|
import { SessionManager } from "../session.js";
|
|
@@ -181,16 +182,18 @@ function persistSelectedModel(model, ctx) {
|
|
|
181
182
|
ctx.sessionManager.appendMarker("model_switch", model);
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
+
/** Rebuild the system prompt from the agent's live options (model, tools, memory, Skills catalog). */
|
|
186
|
+
export function syncSystemPrompt(ctx, model) {
|
|
185
187
|
const { providerId } = decodeModel(model);
|
|
186
188
|
const toolPromptOptions = typeof ctx.agent.getSystemPromptToolOptions === "function"
|
|
187
189
|
? ctx.agent.getSystemPromptToolOptions()
|
|
188
190
|
: {};
|
|
189
191
|
ctx.agent.setSystemPrompt(buildSystemPrompt({
|
|
190
192
|
agentName: "Bubble",
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
// Same placeholders as startup when no model is configured yet.
|
|
194
|
+
configuredProvider: providerId || "none",
|
|
195
|
+
configuredModel: model ? displayModel(model) : "none",
|
|
196
|
+
configuredModelId: model || "none",
|
|
194
197
|
thinkingLevel: ctx.agent.thinking,
|
|
195
198
|
workingDir: ctx.cwd,
|
|
196
199
|
...toolPromptOptions,
|
|
@@ -697,7 +700,9 @@ const builtinSlashCommandEntries = [
|
|
|
697
700
|
name: "login",
|
|
698
701
|
description: "Login to OpenAI OAuth or Grok Subscription. Usage: /login [openai|grok]",
|
|
699
702
|
async handler(args, ctx) {
|
|
700
|
-
|
|
703
|
+
// No default provider: a bare command must ask which account, never
|
|
704
|
+
// silently act on OpenAI now that more than one OAuth provider exists.
|
|
705
|
+
const providerId = args?.trim();
|
|
701
706
|
if (!providerId) {
|
|
702
707
|
ctx.openPicker("login");
|
|
703
708
|
return;
|
|
@@ -825,7 +830,9 @@ const builtinSlashCommandEntries = [
|
|
|
825
830
|
name: "logout",
|
|
826
831
|
description: "Remove local login credentials. Usage: /logout [openai|grok]",
|
|
827
832
|
async handler(args, ctx) {
|
|
828
|
-
|
|
833
|
+
// No default provider: a bare command must ask which account, never
|
|
834
|
+
// silently act on OpenAI now that more than one OAuth provider exists.
|
|
835
|
+
const providerId = args?.trim();
|
|
829
836
|
if (!providerId) {
|
|
830
837
|
ctx.openPicker("logout");
|
|
831
838
|
return;
|
|
@@ -833,10 +840,13 @@ const builtinSlashCommandEntries = [
|
|
|
833
840
|
if (isGrokSubscriptionProviderId(providerId)) {
|
|
834
841
|
return logoutGrokSubscription(ctx);
|
|
835
842
|
}
|
|
836
|
-
|
|
843
|
+
// Alias-aware: `openai` may be backed by the legacy `openai-codex` key.
|
|
844
|
+
const loginKeys = ctx.registry.getOAuthLoginKeys(providerId);
|
|
845
|
+
if (loginKeys.length === 0) {
|
|
837
846
|
return `No OAuth credentials found for ${providerId}.`;
|
|
838
847
|
}
|
|
839
|
-
|
|
848
|
+
for (const key of loginKeys)
|
|
849
|
+
ctx.registry.getAuthStorage().remove(key);
|
|
840
850
|
const fallback = ctx.registry.getDefault();
|
|
841
851
|
if (fallback?.apiKey) {
|
|
842
852
|
const fallbackModel = ctx.registry.getDefaultModel(fallback.id);
|
|
@@ -878,7 +888,7 @@ const builtinSlashCommandEntries = [
|
|
|
878
888
|
},
|
|
879
889
|
{
|
|
880
890
|
name: "permissions",
|
|
881
|
-
description: "Inspect or edit allow/deny rules. Subcommands: add <scope> <list> <rule>, remove <scope> <list> <rule>, clear (session
|
|
891
|
+
description: "Inspect or edit allow/deny rules. Subcommands: add <scope> <list> <rule>, remove <scope> <list> <rule>, clear (session approvals), reload.",
|
|
882
892
|
async handler(args, ctx) {
|
|
883
893
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
884
894
|
const sub = tokens[0] ?? "";
|
|
@@ -892,7 +902,7 @@ const builtinSlashCommandEntries = [
|
|
|
892
902
|
if (size === 0)
|
|
893
903
|
return "Bash allowlist is already empty.";
|
|
894
904
|
ctx.bashAllowlist.clear();
|
|
895
|
-
return `Cleared ${size} bash
|
|
905
|
+
return `Cleared ${size} session-approved bash command${size === 1 ? "" : "s"}.`;
|
|
896
906
|
}
|
|
897
907
|
if (sub === "reload") {
|
|
898
908
|
if (!ctx.settingsManager)
|
|
@@ -920,6 +930,11 @@ const builtinSlashCommandEntries = [
|
|
|
920
930
|
for (const r of merged.ruleSet.allow)
|
|
921
931
|
lines.push(` ${r.source}`);
|
|
922
932
|
}
|
|
933
|
+
const untrusted = merged.untrusted;
|
|
934
|
+
if (untrusted.allow.length + untrusted.mcpServers.length + untrusted.lspServers.length > 0) {
|
|
935
|
+
lines.push("", "This folder is not trusted, so these stay off (Bubble asks at startup):");
|
|
936
|
+
lines.push(...describeRepoCapabilities(untrusted).map((line) => ` ${line}`));
|
|
937
|
+
}
|
|
923
938
|
lines.push("", `Deny rules (${merged.ruleSet.deny.length}):`);
|
|
924
939
|
if (merged.ruleSet.deny.length === 0) {
|
|
925
940
|
lines.push(" (none)");
|
|
@@ -939,13 +954,13 @@ const builtinSlashCommandEntries = [
|
|
|
939
954
|
const entries = ctx.bashAllowlist.list();
|
|
940
955
|
if (lines.length > 0)
|
|
941
956
|
lines.push("");
|
|
942
|
-
lines.push(`Session bash
|
|
957
|
+
lines.push(`Session-approved bash commands (${entries.length}):`);
|
|
943
958
|
if (entries.length === 0) {
|
|
944
|
-
lines.push(' (none) —
|
|
959
|
+
lines.push(' (none) — "don\'t ask again for this exact command this session" adds entries here');
|
|
945
960
|
}
|
|
946
961
|
else {
|
|
947
|
-
for (const
|
|
948
|
-
lines.push(` ${
|
|
962
|
+
for (const command of entries)
|
|
963
|
+
lines.push(` ${command}`);
|
|
949
964
|
}
|
|
950
965
|
}
|
|
951
966
|
if (lines.length === 0) {
|
package/dist/system-prompt.d.ts
CHANGED
|
@@ -26,8 +26,10 @@ export interface SystemPromptOptions {
|
|
|
26
26
|
mode?: PermissionMode;
|
|
27
27
|
/** Current date override */
|
|
28
28
|
currentDate?: string;
|
|
29
|
-
/** Prompt-visible skills */
|
|
29
|
+
/** Prompt-visible skills, listed as the Skills catalog when `skill` is a tool */
|
|
30
30
|
skills?: SkillSummary[];
|
|
31
|
+
/** Catalog budget in characters; 0 turns the catalog off */
|
|
32
|
+
skillCatalogChars?: number;
|
|
31
33
|
/** Prompt-visible memory guidance and summaries */
|
|
32
34
|
memoryPrompt?: string;
|
|
33
35
|
/** Durable child-agent profile prompt used for subagents. */
|
package/dist/tasks/manager.d.ts
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* process, so the event loop keeps owning it (design §2.2a). Reaping is
|
|
21
21
|
* three-layered (design §2.2b): graceful shutdownTasks() with escalation, a
|
|
22
22
|
* SIGKILL-only path for signal handlers, and a process.once("exit") backstop.
|
|
23
|
+
*
|
|
24
|
+
* Finalization waits for stdio to drain: Node may emit "exit" before the last
|
|
25
|
+
* pipe chunks are delivered, so a record is settled on "close" — with a short
|
|
26
|
+
* grace timer from "exit" as the fallback, because a detached grandchild that
|
|
27
|
+
* inherited the pipes (`some-server &`) can keep "close" from ever firing.
|
|
28
|
+
* Same pattern as POST_EXIT_STDIO_GRACE_MS in src/tools/bash.ts.
|
|
23
29
|
*/
|
|
24
30
|
import { type ChildProcess } from "node:child_process";
|
|
25
31
|
export type ManagedServerPurpose = "preview" | "verification";
|
|
@@ -99,6 +105,7 @@ export declare class ProcessManager {
|
|
|
99
105
|
adoptTask(input: AdoptBackgroundTaskInput): BackgroundTaskInfo;
|
|
100
106
|
private reserveTaskSlot;
|
|
101
107
|
private registerTaskChild;
|
|
108
|
+
private finalizeExitedTask;
|
|
102
109
|
private finalizeTask;
|
|
103
110
|
getTask(id: string): BackgroundTaskInfo | undefined;
|
|
104
111
|
listTasks(ownerSessionId?: string): BackgroundTaskInfo[];
|
package/dist/tasks/manager.js
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* process, so the event loop keeps owning it (design §2.2a). Reaping is
|
|
21
21
|
* three-layered (design §2.2b): graceful shutdownTasks() with escalation, a
|
|
22
22
|
* SIGKILL-only path for signal handlers, and a process.once("exit") backstop.
|
|
23
|
+
*
|
|
24
|
+
* Finalization waits for stdio to drain: Node may emit "exit" before the last
|
|
25
|
+
* pipe chunks are delivered, so a record is settled on "close" — with a short
|
|
26
|
+
* grace timer from "exit" as the fallback, because a detached grandchild that
|
|
27
|
+
* inherited the pipes (`some-server &`) can keep "close" from ever firing.
|
|
28
|
+
* Same pattern as POST_EXIT_STDIO_GRACE_MS in src/tools/bash.ts.
|
|
23
29
|
*/
|
|
24
30
|
import { spawn } from "node:child_process";
|
|
25
31
|
import { existsSync } from "node:fs";
|
|
@@ -28,6 +34,7 @@ import { platform } from "node:os";
|
|
|
28
34
|
import { appendTailCapped, killProcessTree, stripAnsi, tail } from "./process-utils.js";
|
|
29
35
|
const MAX_LOG_BYTES = 96 * 1024;
|
|
30
36
|
const STOP_FORCE_AFTER_MS = 1000;
|
|
37
|
+
const POST_EXIT_STDIO_GRACE_MS = 150;
|
|
31
38
|
const MAX_RUNNING_TASKS_PER_SESSION = 8;
|
|
32
39
|
const MAX_FINISHED_TASKS = 20;
|
|
33
40
|
function countLineBreaks(value) {
|
|
@@ -143,14 +150,18 @@ export class ProcessManager {
|
|
|
143
150
|
append(Buffer.from(`\n[task failed to start: ${error.message}]\n`));
|
|
144
151
|
this.finalizeTask(record, "failed", null);
|
|
145
152
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
153
|
+
record.settled = onChildSettled(child, {
|
|
154
|
+
onExit: (code) => {
|
|
155
|
+
record.exited = { code };
|
|
156
|
+
},
|
|
157
|
+
onSettled: (code) => this.finalizeExitedTask(record, code),
|
|
150
158
|
});
|
|
151
159
|
this.bumpTaskState(record);
|
|
152
160
|
return this.publicTask(record);
|
|
153
161
|
}
|
|
162
|
+
finalizeExitedTask(record, code) {
|
|
163
|
+
this.finalizeTask(record, code === 0 ? "completed" : "failed", code);
|
|
164
|
+
}
|
|
154
165
|
finalizeTask(record, status, exitCode) {
|
|
155
166
|
if (record.status !== "running")
|
|
156
167
|
return;
|
|
@@ -242,13 +253,20 @@ export class ProcessManager {
|
|
|
242
253
|
if (record.status !== "running")
|
|
243
254
|
return this.publicTask(record);
|
|
244
255
|
const pid = record.child?.pid ?? record.pid;
|
|
245
|
-
// Mark first so the exit listener does not double-finalize as failed.
|
|
246
|
-
|
|
256
|
+
// Mark first so the exit listener does not double-finalize as failed. A
|
|
257
|
+
// task that already exited on its own keeps its real outcome instead, and
|
|
258
|
+
// is left to the close/grace path: finish listeners snapshot the output
|
|
259
|
+
// tail, so they must not fire before the pending pipe data is appended.
|
|
260
|
+
if (!record.exited)
|
|
261
|
+
this.finalizeTask(record, "killed", null);
|
|
247
262
|
if (pid) {
|
|
263
|
+
// Also reaps pipe-holding grandchildren, which lets "close" fire promptly.
|
|
248
264
|
killProcessTree(pid, "SIGTERM");
|
|
249
265
|
const forceTimer = setTimeout(() => killProcessTree(pid, "SIGKILL"), STOP_FORCE_AFTER_MS);
|
|
250
266
|
forceTimer.unref?.();
|
|
251
267
|
}
|
|
268
|
+
if (record.exited)
|
|
269
|
+
await record.settled;
|
|
252
270
|
return this.publicTask(record);
|
|
253
271
|
}
|
|
254
272
|
/** Graceful reaping for shutdownRuntime (design §2.2b layer 1). */
|
|
@@ -266,7 +284,12 @@ export class ProcessManager {
|
|
|
266
284
|
const pid = task.child?.pid ?? task.pid;
|
|
267
285
|
if (pid)
|
|
268
286
|
killProcessTree(pid, "SIGKILL");
|
|
269
|
-
if (task.status
|
|
287
|
+
if (task.status !== "running")
|
|
288
|
+
continue;
|
|
289
|
+
// Exited on its own inside the drain window: bounded wait for its output.
|
|
290
|
+
if (task.exited)
|
|
291
|
+
await task.settled;
|
|
292
|
+
else
|
|
270
293
|
this.finalizeTask(task, "killed", null);
|
|
271
294
|
}
|
|
272
295
|
}
|
|
@@ -318,7 +341,7 @@ export class ProcessManager {
|
|
|
318
341
|
return [...this.registry.values()].filter((record) => record.kind === "task");
|
|
319
342
|
}
|
|
320
343
|
publicTask(record) {
|
|
321
|
-
const { child: _child, output: _output, outputLineBreaks: _outputLineBreaks, outputHasData: _outputHasData, outputEndsWithNewline: _outputEndsWithNewline, ...info } = record;
|
|
344
|
+
const { child: _child, exited: _exited, settled: _settled, output: _output, outputLineBreaks: _outputLineBreaks, outputHasData: _outputHasData, outputEndsWithNewline: _outputEndsWithNewline, ...info } = record;
|
|
322
345
|
return { ...info };
|
|
323
346
|
}
|
|
324
347
|
// -- servers (moved verbatim from server-manager.ts, kind-filtered) --------
|
|
@@ -379,18 +402,31 @@ export class ProcessManager {
|
|
|
379
402
|
record.status = "failed";
|
|
380
403
|
appendServerLog(record, `\n[server failed: ${error.message}]\n`);
|
|
381
404
|
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
405
|
+
// Settle after stdio drains so a fast crash surfaces its logs in the
|
|
406
|
+
// "did not become ready" error instead of racing the status flip.
|
|
407
|
+
record.settled = onChildSettled(child, {
|
|
408
|
+
unrefTimer: true,
|
|
409
|
+
onExit: (code) => {
|
|
410
|
+
record.exited = { code };
|
|
411
|
+
},
|
|
412
|
+
onSettled: (code) => {
|
|
413
|
+
record.exitCode = code;
|
|
414
|
+
if (record.status !== "stopped") {
|
|
415
|
+
record.status = code === 0 ? "exited" : "failed";
|
|
416
|
+
}
|
|
417
|
+
record.child = undefined;
|
|
418
|
+
},
|
|
388
419
|
});
|
|
389
420
|
const timeoutSec = input.timeoutSec ?? 30;
|
|
390
421
|
const ready = readinessUrl
|
|
391
422
|
? await waitForReadiness(record, readinessUrl, timeoutSec)
|
|
392
423
|
: await waitForProcessToStayAlive(record, Math.min(timeoutSec, 2));
|
|
393
|
-
|
|
424
|
+
// `exited` is checked on its own: the status flip lags "exit" by the drain
|
|
425
|
+
// window, so a server dying right at the deadline still reads as active.
|
|
426
|
+
if (!ready || record.exited) {
|
|
427
|
+
// Bounded: lets the crash output land before it is quoted below.
|
|
428
|
+
if (record.exited)
|
|
429
|
+
await record.settled;
|
|
394
430
|
const logs = record.logs.trim();
|
|
395
431
|
await this.stopManagedServer(id);
|
|
396
432
|
throw new Error(`Server ${id} did not become ready within ${timeoutSec}s.${logs ? `\n\nLogs:\n${tail(logs, 4000)}` : ""}`);
|
|
@@ -479,6 +515,46 @@ process.once("exit", () => {
|
|
|
479
515
|
// ---------------------------------------------------------------------------
|
|
480
516
|
// Server internals (moved verbatim)
|
|
481
517
|
// ---------------------------------------------------------------------------
|
|
518
|
+
/**
|
|
519
|
+
* Calls `onSettled` exactly once, after the child has exited AND its stdio has
|
|
520
|
+
* drained ("close"), or POST_EXIT_STDIO_GRACE_MS after "exit" when something
|
|
521
|
+
* else still holds the pipes open. `onExit` fires synchronously at "exit".
|
|
522
|
+
* The returned promise resolves right after `onSettled` has run.
|
|
523
|
+
*/
|
|
524
|
+
function onChildSettled(child, handlers) {
|
|
525
|
+
let settled = false;
|
|
526
|
+
let resolveSettled;
|
|
527
|
+
const done = new Promise((resolve) => {
|
|
528
|
+
resolveSettled = resolve;
|
|
529
|
+
});
|
|
530
|
+
let graceTimer;
|
|
531
|
+
const settle = (code) => {
|
|
532
|
+
if (settled)
|
|
533
|
+
return;
|
|
534
|
+
settled = true;
|
|
535
|
+
if (graceTimer)
|
|
536
|
+
clearTimeout(graceTimer);
|
|
537
|
+
try {
|
|
538
|
+
handlers.onSettled(code);
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
resolveSettled();
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
child.once("exit", (code) => {
|
|
545
|
+
handlers.onExit?.(code);
|
|
546
|
+
if (settled)
|
|
547
|
+
return;
|
|
548
|
+
// Timers run before the poll phase: if the loop stalled past the grace
|
|
549
|
+
// period, output already sitting in the pipe has not been read yet. Yield
|
|
550
|
+
// through one poll phase (setImmediate) so it is appended before settling.
|
|
551
|
+
graceTimer = setTimeout(() => setImmediate(() => settle(code)), POST_EXIT_STDIO_GRACE_MS);
|
|
552
|
+
if (handlers.unrefTimer)
|
|
553
|
+
graceTimer.unref?.();
|
|
554
|
+
});
|
|
555
|
+
child.once("close", (code) => settle(code));
|
|
556
|
+
return done;
|
|
557
|
+
}
|
|
482
558
|
function publicServerInfo(server) {
|
|
483
559
|
return {
|
|
484
560
|
id: server.id,
|
|
@@ -506,7 +582,7 @@ function unrefStream(stream) {
|
|
|
506
582
|
async function waitForReadiness(server, url, timeoutSec) {
|
|
507
583
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
508
584
|
while (Date.now() < deadline) {
|
|
509
|
-
if (!
|
|
585
|
+
if (!isServerAlive(server))
|
|
510
586
|
return false;
|
|
511
587
|
if (await canFetch(url))
|
|
512
588
|
return true;
|
|
@@ -517,11 +593,11 @@ async function waitForReadiness(server, url, timeoutSec) {
|
|
|
517
593
|
async function waitForProcessToStayAlive(server, timeoutSec) {
|
|
518
594
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
519
595
|
while (Date.now() < deadline) {
|
|
520
|
-
if (!
|
|
596
|
+
if (!isServerAlive(server))
|
|
521
597
|
return false;
|
|
522
598
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
523
599
|
}
|
|
524
|
-
return
|
|
600
|
+
return isServerAlive(server);
|
|
525
601
|
}
|
|
526
602
|
async function canFetch(url) {
|
|
527
603
|
const controller = new AbortController();
|
|
@@ -537,6 +613,9 @@ async function canFetch(url) {
|
|
|
537
613
|
clearTimeout(timeout);
|
|
538
614
|
}
|
|
539
615
|
}
|
|
616
|
+
function isServerAlive(server) {
|
|
617
|
+
return !server.exited && isActiveStatus(server.status);
|
|
618
|
+
}
|
|
540
619
|
function isActiveStatus(status) {
|
|
541
620
|
return status === "starting" || status === "ready" || status === "running";
|
|
542
621
|
}
|
|
@@ -6,6 +6,7 @@ import { THINKING_LEVELS } from "../types.js";
|
|
|
6
6
|
import { buildWorkflowResultBlock, workflowMemberWarning } from "../agent/workflow/control.js";
|
|
7
7
|
import { precompileWorkflowScript } from "../agent/workflow/runtime.js";
|
|
8
8
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
9
|
+
import { SUBAGENT_WAIT_GUIDANCE } from "../agent/subagent-wait-guidance.js";
|
|
9
10
|
/**
|
|
10
11
|
* Session-scoped trust decisions for project profiles, keyed by file path +
|
|
11
12
|
* content hash so an edited file re-prompts (design §10.2). Shared across the
|
|
@@ -121,6 +122,7 @@ export function createSpawnAgentTool(options = {}, sharedTrust) {
|
|
|
121
122
|
parameters: {
|
|
122
123
|
type: "object",
|
|
123
124
|
properties: {
|
|
125
|
+
description: { type: "string", description: "Short task title for the activity card (under 60 characters). Put the full instructions in message." },
|
|
124
126
|
agent_type: { type: "string", description: "Subagent profile or role name. Defaults to default. Built-in types include default, explorer, and worker; see the tool description for custom profiles." },
|
|
125
127
|
agent: { type: "string", description: "Alias for agent_type." },
|
|
126
128
|
category: { type: "string", description: "Optional semantic category for model/thinking routing, such as quick, deep, explore, review, frontend, or writing." },
|
|
@@ -196,7 +198,7 @@ export function createWaitAgentTool() {
|
|
|
196
198
|
effect: "read",
|
|
197
199
|
description: [
|
|
198
200
|
"Wait for one or more spawned subagents to reach a final status and return snapshots.",
|
|
199
|
-
|
|
201
|
+
...SUBAGENT_WAIT_GUIDANCE,
|
|
200
202
|
].join(" "),
|
|
201
203
|
parameters: {
|
|
202
204
|
type: "object",
|
|
@@ -204,10 +206,10 @@ export function createWaitAgentTool() {
|
|
|
204
206
|
agent_id: { type: "string", description: "A single agent id to wait for." },
|
|
205
207
|
agent_ids: {
|
|
206
208
|
type: "array",
|
|
207
|
-
description: "Agent ids to wait for. If omitted,
|
|
209
|
+
description: "Agent ids to wait for; returns when any target finishes. Prefer explicit IDs for remaining work. If omitted, considers all non-closed subagents, including already-finished ones.",
|
|
208
210
|
items: { type: "string" },
|
|
209
211
|
},
|
|
210
|
-
timeout_ms: { type: "number", description: "Maximum wait time in milliseconds. Defaults to 30000." },
|
|
212
|
+
timeout_ms: { type: "number", description: "Maximum wait time in milliseconds, not a mandatory delay; returns early when a target finishes. Choose according to your next decision. Defaults to 30000." },
|
|
211
213
|
},
|
|
212
214
|
additionalProperties: false,
|
|
213
215
|
},
|
|
@@ -249,7 +251,7 @@ export function createSendInputTool() {
|
|
|
249
251
|
name: "send_input",
|
|
250
252
|
readOnly: true,
|
|
251
253
|
effect: "read",
|
|
252
|
-
description: "Send a follow-up message to an existing subagent thread.
|
|
254
|
+
description: "Send a follow-up message to an existing subagent thread. While running or queued, text input is queued and applied at the next safe boundary without interruption. Use interrupt:true only to cancel and redirect it. Restarting a finished child goes through the scheduler like any spawn.",
|
|
253
255
|
parameters: {
|
|
254
256
|
type: "object",
|
|
255
257
|
properties: {
|
|
@@ -277,7 +279,7 @@ export function createSendInputTool() {
|
|
|
277
279
|
abortSignal: ctx.abortSignal,
|
|
278
280
|
});
|
|
279
281
|
return formatLifecycleResult("send_input", [snapshot], [
|
|
280
|
-
|
|
282
|
+
`${snapshot.pendingInputCount ? "Queued input for" : "Sent input to"} ${snapshot.nickname} (${snapshot.agentName})`,
|
|
281
283
|
`agent_id: ${snapshot.agentId}`,
|
|
282
284
|
`status: ${snapshot.status}`,
|
|
283
285
|
...(snapshot.status === "queued" ? [queuedStatusLine(snapshot)] : []),
|
|
@@ -562,7 +564,7 @@ function spawnNextSteps(snapshot) {
|
|
|
562
564
|
];
|
|
563
565
|
}
|
|
564
566
|
return [
|
|
565
|
-
`next: call wait_agent for ${snapshot.agentId}
|
|
567
|
+
`next: continue useful non-overlapping work; call wait_agent for ${snapshot.agentId} when its result is needed for your next step or final synthesis`,
|
|
566
568
|
];
|
|
567
569
|
}
|
|
568
570
|
function queuedStatusLine(snapshot) {
|
|
@@ -581,7 +583,7 @@ function waitTimeoutGuidance(snapshots) {
|
|
|
581
583
|
if (running.length > 0) {
|
|
582
584
|
lines.push(`${running.length} child${running.length === 1 ? " is" : "ren are"} still running.`);
|
|
583
585
|
}
|
|
584
|
-
lines.push("
|
|
586
|
+
lines.push("Reassess whether a child's result blocks your next meaningful step: continue useful independent work if available, otherwise call wait_agent again with a suitable timeout. Do not duplicate the delegated work.");
|
|
585
587
|
return lines;
|
|
586
588
|
}
|
|
587
589
|
function formatLifecycleResult(toolName, snapshots, header) {
|
|
@@ -717,6 +719,8 @@ function snapshotToMetadata(snapshot) {
|
|
|
717
719
|
error: snapshot.error,
|
|
718
720
|
createdAt: snapshot.createdAt,
|
|
719
721
|
updatedAt: snapshot.updatedAt,
|
|
722
|
+
pendingInputCount: snapshot.pendingInputCount,
|
|
723
|
+
inputDelivery: snapshot.inputDelivery,
|
|
720
724
|
};
|
|
721
725
|
}
|
|
722
726
|
function unsupportedProfile(profile) {
|
|
@@ -33,7 +33,7 @@ export declare class WorktreeApprovalController implements ApprovalController {
|
|
|
33
33
|
* exposes a cloneForChild hook (the standard `read`, which carries a
|
|
34
34
|
* FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
|
|
35
35
|
* members of a fan-out never share mutable tool state. Stateless tools
|
|
36
|
-
* (
|
|
36
|
+
* (ls/grep, web/memory/skill) and custom/mock tools without the hook are
|
|
37
37
|
* passed through unchanged. Write children get full isolation via
|
|
38
38
|
* createWorktreeChildTools instead.
|
|
39
39
|
*/
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { isAbsolute, resolve, sep } from "node:path";
|
|
12
12
|
import { createBashTool } from "./bash.js";
|
|
13
13
|
import { createEditTool } from "./edit.js";
|
|
14
|
-
import {
|
|
14
|
+
import { createLsTool } from "./ls.js";
|
|
15
15
|
import { createGrepTool } from "./grep.js";
|
|
16
16
|
import { createReadTool } from "./read.js";
|
|
17
17
|
import { createWriteTool } from "./write.js";
|
|
@@ -86,7 +86,7 @@ export class WorktreeApprovalController {
|
|
|
86
86
|
return { decision: "ask" };
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
const WORKTREE_TOOL_NAMES = new Set(["read", "
|
|
89
|
+
const WORKTREE_TOOL_NAMES = new Set(["read", "ls", "grep", "edit", "write", "bash"]);
|
|
90
90
|
/**
|
|
91
91
|
* Builds the write child's toolset bound to its worktree: fresh instances
|
|
92
92
|
* with their own FileStateTracker and the worktree approval policy. A
|
|
@@ -97,7 +97,7 @@ const WORKTREE_TOOL_NAMES = new Set(["read", "glob", "grep", "edit", "write", "b
|
|
|
97
97
|
* exposes a cloneForChild hook (the standard `read`, which carries a
|
|
98
98
|
* FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
|
|
99
99
|
* members of a fan-out never share mutable tool state. Stateless tools
|
|
100
|
-
* (
|
|
100
|
+
* (ls/grep, web/memory/skill) and custom/mock tools without the hook are
|
|
101
101
|
* passed through unchanged. Write children get full isolation via
|
|
102
102
|
* createWorktreeChildTools instead.
|
|
103
103
|
*/
|
|
@@ -109,7 +109,7 @@ export function createWorktreeChildTools(worktreeCwd, include) {
|
|
|
109
109
|
const fileState = new FileStateTracker(worktreeCwd);
|
|
110
110
|
const tools = [
|
|
111
111
|
createReadTool(worktreeCwd, approval, undefined, fileState),
|
|
112
|
-
|
|
112
|
+
createLsTool(worktreeCwd),
|
|
113
113
|
createGrepTool(worktreeCwd),
|
|
114
114
|
createEditTool(worktreeCwd, approval, undefined, fileState),
|
|
115
115
|
createWriteTool(worktreeCwd, {}, approval, undefined, fileState),
|
|
@@ -2,13 +2,11 @@ export interface EditOperation {
|
|
|
2
2
|
oldText: string;
|
|
3
3
|
newText: string;
|
|
4
4
|
}
|
|
5
|
-
export type EditMatchMode = "exact" | "trimmed" | "unescaped" | "normalized-line" | "smart-line" | "markdown-table" | "single-line-whitespace";
|
|
6
5
|
export interface EditApplyOptions {
|
|
7
6
|
path?: string;
|
|
8
7
|
}
|
|
9
8
|
export interface EditMatchInfo {
|
|
10
9
|
editIndex: number;
|
|
11
|
-
mode: EditMatchMode;
|
|
12
10
|
start: number;
|
|
13
11
|
end: number;
|
|
14
12
|
}
|
|
@@ -25,4 +23,3 @@ export declare class EditApplyError extends Error {
|
|
|
25
23
|
constructor(message: string, status?: "no_match" | "blocked");
|
|
26
24
|
}
|
|
27
25
|
export declare function applyEditsToContent(rawContent: string, edits: EditOperation[], options?: EditApplyOptions): AppliedEditResult;
|
|
28
|
-
export declare function formatEditMatchNotes(matches: EditMatchInfo[]): string;
|