@bubblebrain-ai/bubble 0.0.13 → 0.0.14
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/execution-governor.js +1 -1
- package/dist/agent/tool-intent.js +1 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +589 -316
- package/dist/approval/controller.d.ts +1 -0
- package/dist/approval/controller.js +20 -3
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +14 -1
- package/dist/context/compact.js +9 -3
- package/dist/context/projector.js +27 -12
- package/dist/debug-trace.d.ts +27 -0
- package/dist/debug-trace.js +385 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/serve.js +7 -1
- package/dist/main.js +28 -0
- package/dist/model-catalog.js +1 -0
- package/dist/orchestrator/default-hooks.js +19 -8
- package/dist/orchestrator/hooks.d.ts +1 -0
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.d.ts +5 -6
- package/dist/prompt/reminders.js +8 -9
- package/dist/prompt/runtime.js +2 -2
- package/dist/provider-openai-codex.d.ts +7 -0
- package/dist/provider-openai-codex.js +265 -124
- package/dist/provider-registry.d.ts +2 -0
- package/dist/provider-registry.js +58 -9
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +5 -1
- package/dist/session-log.js +13 -1
- package/dist/slash-commands/commands.js +12 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/stats/usage.d.ts +52 -0
- package/dist/stats/usage.js +414 -0
- package/dist/tools/apply-patch.d.ts +9 -0
- package/dist/tools/apply-patch.js +330 -0
- package/dist/tools/bash.js +205 -44
- package/dist/tools/edit-apply.d.ts +5 -2
- package/dist/tools/edit-apply.js +221 -31
- package/dist/tools/edit.js +12 -3
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +12 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch-apply.d.ts +41 -0
- package/dist/tools/patch-apply.js +312 -0
- package/dist/tools/server-manager.d.ts +36 -0
- package/dist/tools/server-manager.js +234 -0
- package/dist/tools/server.d.ts +6 -0
- package/dist/tools/server.js +245 -0
- package/dist/tools/write.d.ts +3 -6
- package/dist/tools/write.js +26 -46
- package/dist/tui/display-history.d.ts +1 -0
- package/dist/tui/display-history.js +5 -4
- package/dist/tui/edit-diff.js +6 -1
- package/dist/tui/model-picker-data.d.ts +10 -0
- package/dist/tui/model-picker-data.js +32 -0
- package/dist/tui/run.js +632 -89
- package/dist/tui/tool-renderers/fallback.js +1 -1
- package/dist/tui/tool-renderers/write-preview.js +2 -0
- package/dist/tui/trace-groups.js +10 -3
- package/dist/tui-ink/app.js +1 -4
- package/dist/tui-ink/approval/approval-dialog.js +7 -1
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +5 -4
- package/dist/tui-ink/message-list.js +14 -8
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/tui-opentui/app.js +2 -0
- package/dist/tui-opentui/approval/approval-dialog.js +7 -1
- package/dist/tui-opentui/display-history.d.ts +1 -0
- package/dist/tui-opentui/display-history.js +5 -4
- package/dist/tui-opentui/edit-diff.js +6 -1
- package/dist/tui-opentui/message-list.js +6 -3
- package/dist/tui-opentui/trace-groups.js +10 -3
- package/dist/types.d.ts +12 -2
- package/package.json +1 -1
|
@@ -26,8 +26,7 @@ export class PermissionAwareApprovalController {
|
|
|
26
26
|
return checkPermission(ruleSet, query);
|
|
27
27
|
}
|
|
28
28
|
async request(req) {
|
|
29
|
-
const
|
|
30
|
-
const ruleResult = this.checkRules(query);
|
|
29
|
+
const ruleResult = this.checkRequestRules(req);
|
|
31
30
|
if (ruleResult.decision === "deny") {
|
|
32
31
|
return {
|
|
33
32
|
action: "reject",
|
|
@@ -38,7 +37,7 @@ export class PermissionAwareApprovalController {
|
|
|
38
37
|
if (mode === "bypassPermissions") {
|
|
39
38
|
return { action: "approve" };
|
|
40
39
|
}
|
|
41
|
-
if (mode === "default" && (req.type === "edit" || req.type === "write")) {
|
|
40
|
+
if (mode === "default" && (req.type === "edit" || req.type === "write" || req.type === "patch")) {
|
|
42
41
|
return { action: "approve" };
|
|
43
42
|
}
|
|
44
43
|
if (mode === "plan") {
|
|
@@ -71,8 +70,26 @@ export class PermissionAwareApprovalController {
|
|
|
71
70
|
return { tool: "Write", path: req.path, cwd: this.options.cwd };
|
|
72
71
|
case "edit":
|
|
73
72
|
return { tool: "Edit", path: req.path, cwd: this.options.cwd };
|
|
73
|
+
case "patch":
|
|
74
|
+
return { tool: "Edit", path: req.path, cwd: this.options.cwd };
|
|
74
75
|
case "lsp":
|
|
75
76
|
return { tool: "Lsp", path: req.path, cwd: this.options.cwd };
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
checkRequestRules(req) {
|
|
80
|
+
if (req.type !== "patch")
|
|
81
|
+
return this.checkRules(this.requestToQuery(req));
|
|
82
|
+
const perFile = req.files.map((file) => this.checkRules({
|
|
83
|
+
tool: file.kind === "add" ? "Write" : "Edit",
|
|
84
|
+
path: file.path,
|
|
85
|
+
cwd: this.options.cwd,
|
|
86
|
+
}));
|
|
87
|
+
const denied = perFile.find((result) => result.decision === "deny");
|
|
88
|
+
if (denied)
|
|
89
|
+
return denied;
|
|
90
|
+
if (perFile.length > 0 && perFile.every((result) => result.decision === "allow")) {
|
|
91
|
+
return { decision: "allow", rule: perFile[0].rule };
|
|
92
|
+
}
|
|
93
|
+
return { decision: "ask" };
|
|
94
|
+
}
|
|
78
95
|
}
|
|
@@ -24,6 +24,8 @@ function approvalRequestLabel(req) {
|
|
|
24
24
|
return `Edit to ${req.path}`;
|
|
25
25
|
case "write":
|
|
26
26
|
return `${req.fileExists ? "Overwrite" : "Write"} to ${req.path}`;
|
|
27
|
+
case "patch":
|
|
28
|
+
return `Patch to ${req.path}`;
|
|
27
29
|
case "bash":
|
|
28
30
|
return `Bash command \`${req.command}\``;
|
|
29
31
|
case "lsp":
|
package/dist/approval/types.d.ts
CHANGED
|
@@ -22,6 +22,19 @@ export interface WriteApprovalRequest {
|
|
|
22
22
|
diff?: string;
|
|
23
23
|
fileExists: boolean;
|
|
24
24
|
}
|
|
25
|
+
export interface PatchApprovalRequest {
|
|
26
|
+
type: "patch";
|
|
27
|
+
/** Human-readable path summary for compact UIs. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** All absolute paths touched by the patch. */
|
|
30
|
+
paths: string[];
|
|
31
|
+
files: Array<{
|
|
32
|
+
path: string;
|
|
33
|
+
kind: "add" | "update" | "delete";
|
|
34
|
+
}>;
|
|
35
|
+
/** Combined unified diff for all file changes. */
|
|
36
|
+
diff: string;
|
|
37
|
+
}
|
|
25
38
|
export interface BashApprovalRequest {
|
|
26
39
|
type: "bash";
|
|
27
40
|
command: string;
|
|
@@ -32,7 +45,7 @@ export interface LspApprovalRequest {
|
|
|
32
45
|
path: string;
|
|
33
46
|
operation: string;
|
|
34
47
|
}
|
|
35
|
-
export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | BashApprovalRequest | LspApprovalRequest;
|
|
48
|
+
export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | PatchApprovalRequest | BashApprovalRequest | LspApprovalRequest;
|
|
36
49
|
export type ApprovalDecision = {
|
|
37
50
|
action: "approve";
|
|
38
51
|
feedback?: string;
|
package/dist/context/compact.js
CHANGED
|
@@ -255,9 +255,8 @@ function entriesToMessages(entries) {
|
|
|
255
255
|
break;
|
|
256
256
|
case "assistant_message":
|
|
257
257
|
messages.push({
|
|
258
|
+
...entry.message,
|
|
258
259
|
role: "assistant",
|
|
259
|
-
content: entry.message.content,
|
|
260
|
-
reasoning: entry.message.reasoning,
|
|
261
260
|
});
|
|
262
261
|
break;
|
|
263
262
|
case "tool_call": {
|
|
@@ -358,7 +357,14 @@ function findLatestSummaryIndex(entries) {
|
|
|
358
357
|
return -1;
|
|
359
358
|
}
|
|
360
359
|
function nextSummaryId(entries) {
|
|
361
|
-
|
|
360
|
+
let max = 0;
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
const match = /^(\d+)/.exec(entry.id);
|
|
363
|
+
if (!match)
|
|
364
|
+
continue;
|
|
365
|
+
max = Math.max(max, Number(match[1]));
|
|
366
|
+
}
|
|
367
|
+
return `${max + 1}`;
|
|
362
368
|
}
|
|
363
369
|
function cloneMessage(message) {
|
|
364
370
|
if (message.role === "assistant") {
|
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
import { getContextBudget } from "./budget.js";
|
|
2
2
|
import { compactCurrentTurnToolGroups, compactMessages } from "./compact.js";
|
|
3
3
|
import { pruneMessages } from "./prune.js";
|
|
4
|
-
// Prefix-cache invariant:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// system/meta message in its original position so the cacheable prefix
|
|
9
|
-
// stays byte-identical across turns where compaction didn't fire. Inserting
|
|
10
|
-
// new dynamic content (summaries, etc.) AFTER system+meta is safe; inserting
|
|
11
|
-
// it within or before them is not.
|
|
4
|
+
// Prefix-cache invariant: only the leading static system prompt is promoted to
|
|
5
|
+
// the first provider message. Runtime meta reminders stay in the conversational
|
|
6
|
+
// body at their original relative position, so a new per-turn reminder does not
|
|
7
|
+
// rewrite the cacheable prefix before the existing history.
|
|
12
8
|
export function projectMessages(messages, options = {}) {
|
|
13
9
|
const mode = options.mode ?? "full";
|
|
14
10
|
const projectedBody = [];
|
|
15
11
|
const systemContext = [];
|
|
12
|
+
let inLeadingSystemPrefix = true;
|
|
16
13
|
for (const message of messages) {
|
|
17
|
-
if (message.role === "system") {
|
|
14
|
+
if (message.role === "system" && inLeadingSystemPrefix) {
|
|
18
15
|
systemContext.push(message.content);
|
|
19
16
|
continue;
|
|
20
17
|
}
|
|
21
18
|
if (message.role === "meta") {
|
|
19
|
+
inLeadingSystemPrefix = false;
|
|
22
20
|
if (message.includeInLlm !== false) {
|
|
23
|
-
|
|
21
|
+
projectedBody.push({
|
|
22
|
+
role: "user",
|
|
23
|
+
content: formatMetaMessage(message),
|
|
24
|
+
});
|
|
24
25
|
}
|
|
25
26
|
continue;
|
|
26
27
|
}
|
|
28
|
+
inLeadingSystemPrefix = false;
|
|
29
|
+
if (message.role === "system") {
|
|
30
|
+
projectedBody.push({
|
|
31
|
+
role: "user",
|
|
32
|
+
content: formatRuntimeSystemMessage(message),
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
27
36
|
if (message.role === "assistant" && isEmptyAssistantMessage(message)) {
|
|
28
37
|
continue;
|
|
29
38
|
}
|
|
@@ -147,9 +156,12 @@ export function repairToolCallChains(messages) {
|
|
|
147
156
|
}
|
|
148
157
|
function isEmptyAssistantMessage(message) {
|
|
149
158
|
const hasContent = message.content.trim().length > 0;
|
|
150
|
-
const hasReasoning = (message.reasoning ?? "").trim().length > 0;
|
|
151
159
|
const hasToolCalls = !!message.toolCalls && message.toolCalls.length > 0;
|
|
152
|
-
|
|
160
|
+
// Reasoning-only assistant messages are not valid ChatCompletions history:
|
|
161
|
+
// providers require assistant history to contain user-visible content or
|
|
162
|
+
// tool_calls. Keep reasoning attached to real assistant/tool-call messages,
|
|
163
|
+
// but drop standalone thinking-only turns before provider projection.
|
|
164
|
+
return !hasContent && !hasToolCalls;
|
|
153
165
|
}
|
|
154
166
|
function formatMetaMessage(message) {
|
|
155
167
|
switch (message.kind) {
|
|
@@ -160,6 +172,9 @@ function formatMetaMessage(message) {
|
|
|
160
172
|
return `Runtime context:\n${message.content}`;
|
|
161
173
|
}
|
|
162
174
|
}
|
|
175
|
+
function formatRuntimeSystemMessage(message) {
|
|
176
|
+
return `Runtime context:\n${message.content}`;
|
|
177
|
+
}
|
|
163
178
|
function cloneMessage(message) {
|
|
164
179
|
if (message.role === "assistant") {
|
|
165
180
|
return {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AgentEvent, Message, ToolResult } from "./types.js";
|
|
2
|
+
export interface DebugTraceContext {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
sessionFile?: string;
|
|
5
|
+
provider?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
renderer?: string;
|
|
8
|
+
surface?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DebugTraceInfo {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
path?: string;
|
|
13
|
+
runId?: string;
|
|
14
|
+
rawEnabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function isDebugTraceEnabled(): boolean;
|
|
17
|
+
export declare function isDebugTraceRawEnabled(): boolean;
|
|
18
|
+
export declare function configureDebugTrace(context: DebugTraceContext): DebugTraceInfo;
|
|
19
|
+
export declare function getDebugTraceInfo(): DebugTraceInfo;
|
|
20
|
+
export declare function traceEvent(phase: string, detail?: Record<string, unknown>, context?: DebugTraceContext): void;
|
|
21
|
+
export declare function summarizeTraceText(value: unknown): Record<string, unknown> | undefined;
|
|
22
|
+
export declare function summarizeTraceValue(value: unknown): Record<string, unknown> | undefined;
|
|
23
|
+
export declare function summarizeTraceMessage(message: Message): Record<string, unknown>;
|
|
24
|
+
export declare function summarizeTraceToolResult(result: ToolResult): Record<string, unknown>;
|
|
25
|
+
export declare function summarizeTraceError(error: unknown): Record<string, unknown>;
|
|
26
|
+
export declare function summarizeAgentEventForTrace(event: AgentEvent): Record<string, unknown>;
|
|
27
|
+
export declare function resetDebugTraceForTests(): void;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFileSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
5
|
+
import { getBubbleHome } from "./bubble-home.js";
|
|
6
|
+
const TRACE_VERSION = 1;
|
|
7
|
+
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
8
|
+
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
|
|
9
|
+
const DEFAULT_MAX_AGE_DAYS = 7;
|
|
10
|
+
const DEFAULT_RAW_MAX_BYTES = 256 * 1024;
|
|
11
|
+
let initialized = false;
|
|
12
|
+
let tracePath;
|
|
13
|
+
let runId;
|
|
14
|
+
let sequence = 0;
|
|
15
|
+
let startedAt = performance.now();
|
|
16
|
+
let baseContext = {};
|
|
17
|
+
let defaultPruneDone = false;
|
|
18
|
+
export function isDebugTraceEnabled() {
|
|
19
|
+
const value = process.env.BUBBLE_TRACE?.trim();
|
|
20
|
+
if (!value)
|
|
21
|
+
return false;
|
|
22
|
+
return !FALSE_VALUES.has(value.toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
export function isDebugTraceRawEnabled() {
|
|
25
|
+
const value = process.env.BUBBLE_TRACE_RAW?.trim().toLowerCase();
|
|
26
|
+
return !!value && !FALSE_VALUES.has(value);
|
|
27
|
+
}
|
|
28
|
+
export function configureDebugTrace(context) {
|
|
29
|
+
baseContext = { ...baseContext, ...dropUndefined(context) };
|
|
30
|
+
const path = ensureTracePath();
|
|
31
|
+
if (!path)
|
|
32
|
+
return { enabled: false, rawEnabled: isDebugTraceRawEnabled() };
|
|
33
|
+
return {
|
|
34
|
+
enabled: true,
|
|
35
|
+
path,
|
|
36
|
+
runId,
|
|
37
|
+
rawEnabled: isDebugTraceRawEnabled(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function getDebugTraceInfo() {
|
|
41
|
+
const path = ensureTracePath();
|
|
42
|
+
if (!path)
|
|
43
|
+
return { enabled: false, rawEnabled: isDebugTraceRawEnabled() };
|
|
44
|
+
return {
|
|
45
|
+
enabled: true,
|
|
46
|
+
path,
|
|
47
|
+
runId,
|
|
48
|
+
rawEnabled: isDebugTraceRawEnabled(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function traceEvent(phase, detail, context) {
|
|
52
|
+
const path = ensureTracePath();
|
|
53
|
+
if (!path)
|
|
54
|
+
return;
|
|
55
|
+
const eventContext = {
|
|
56
|
+
...baseContext,
|
|
57
|
+
...dropUndefined(context ?? {}),
|
|
58
|
+
};
|
|
59
|
+
const line = {
|
|
60
|
+
traceVersion: TRACE_VERSION,
|
|
61
|
+
ts: new Date().toISOString(),
|
|
62
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
63
|
+
seq: ++sequence,
|
|
64
|
+
runId,
|
|
65
|
+
pid: process.pid,
|
|
66
|
+
phase,
|
|
67
|
+
...eventContext,
|
|
68
|
+
...(detail ? { detail: sanitizeTraceValue(detail) } : {}),
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
appendFileSync(path, JSON.stringify(line) + "\n", "utf-8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Debug tracing must never affect normal agent execution.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function summarizeTraceText(value) {
|
|
78
|
+
if (typeof value !== "string")
|
|
79
|
+
return summarizeTraceValue(value);
|
|
80
|
+
return summarizeString(value);
|
|
81
|
+
}
|
|
82
|
+
export function summarizeTraceValue(value) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (value === null)
|
|
86
|
+
return { type: "null" };
|
|
87
|
+
if (typeof value === "string")
|
|
88
|
+
return summarizeString(value);
|
|
89
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
90
|
+
return { type: typeof value, value };
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return {
|
|
94
|
+
type: "array",
|
|
95
|
+
count: value.length,
|
|
96
|
+
json: summarizeJson(value),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === "object") {
|
|
100
|
+
return {
|
|
101
|
+
type: "object",
|
|
102
|
+
keys: Object.keys(value).slice(0, 32),
|
|
103
|
+
json: summarizeJson(value),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { type: typeof value, value: String(value) };
|
|
107
|
+
}
|
|
108
|
+
export function summarizeTraceMessage(message) {
|
|
109
|
+
if (message.role === "assistant") {
|
|
110
|
+
return {
|
|
111
|
+
role: message.role,
|
|
112
|
+
content: summarizeTraceText(message.content),
|
|
113
|
+
reasoning: summarizeTraceText(message.reasoning ?? ""),
|
|
114
|
+
error: message.error,
|
|
115
|
+
toolCalls: message.toolCalls?.map((call) => ({
|
|
116
|
+
id: call.id,
|
|
117
|
+
name: call.name,
|
|
118
|
+
args: summarizeTraceText(call.arguments),
|
|
119
|
+
argsCorrupt: call.argsCorrupt,
|
|
120
|
+
})),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (message.role === "tool") {
|
|
124
|
+
return {
|
|
125
|
+
role: message.role,
|
|
126
|
+
toolCallId: message.toolCallId,
|
|
127
|
+
content: summarizeTraceText(message.content),
|
|
128
|
+
isError: message.isError,
|
|
129
|
+
metadata: message.metadata,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (message.role === "user") {
|
|
133
|
+
return {
|
|
134
|
+
role: message.role,
|
|
135
|
+
content: summarizeTraceValue(message.content),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
role: message.role,
|
|
140
|
+
kind: "kind" in message ? message.kind : undefined,
|
|
141
|
+
content: summarizeTraceText(message.content),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function summarizeTraceToolResult(result) {
|
|
145
|
+
return {
|
|
146
|
+
content: summarizeTraceText(result.content),
|
|
147
|
+
isError: result.isError,
|
|
148
|
+
status: result.status,
|
|
149
|
+
metadata: result.metadata,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function summarizeTraceError(error) {
|
|
153
|
+
if (error instanceof Error) {
|
|
154
|
+
return {
|
|
155
|
+
name: error.name,
|
|
156
|
+
message: error.message,
|
|
157
|
+
stack: truncateString(error.stack ?? "", 4000),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
name: "Error",
|
|
162
|
+
message: String(error),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function summarizeAgentEventForTrace(event) {
|
|
166
|
+
switch (event.type) {
|
|
167
|
+
case "text_delta":
|
|
168
|
+
case "reasoning_delta":
|
|
169
|
+
return { type: event.type, content: summarizeTraceText(event.content) };
|
|
170
|
+
case "tool_call_start":
|
|
171
|
+
return { type: event.type, id: event.id, name: event.name };
|
|
172
|
+
case "tool_call_delta":
|
|
173
|
+
return {
|
|
174
|
+
type: event.type,
|
|
175
|
+
id: event.id,
|
|
176
|
+
name: event.name,
|
|
177
|
+
argumentsDelta: summarizeTraceText(event.argumentsDelta),
|
|
178
|
+
arguments: summarizeTraceText(event.arguments),
|
|
179
|
+
};
|
|
180
|
+
case "tool_call_end":
|
|
181
|
+
return {
|
|
182
|
+
type: event.type,
|
|
183
|
+
id: event.id,
|
|
184
|
+
name: event.name,
|
|
185
|
+
arguments: summarizeTraceText(event.arguments),
|
|
186
|
+
};
|
|
187
|
+
case "tool_start":
|
|
188
|
+
return {
|
|
189
|
+
type: event.type,
|
|
190
|
+
id: event.id,
|
|
191
|
+
name: event.name,
|
|
192
|
+
args: summarizeTraceValue(event.args),
|
|
193
|
+
};
|
|
194
|
+
case "tool_update":
|
|
195
|
+
return {
|
|
196
|
+
type: event.type,
|
|
197
|
+
id: event.id,
|
|
198
|
+
name: event.name,
|
|
199
|
+
update: summarizeTraceValue(event.update),
|
|
200
|
+
};
|
|
201
|
+
case "tool_end":
|
|
202
|
+
return {
|
|
203
|
+
type: event.type,
|
|
204
|
+
id: event.id,
|
|
205
|
+
name: event.name,
|
|
206
|
+
result: summarizeTraceToolResult(event.result),
|
|
207
|
+
};
|
|
208
|
+
case "turn_end":
|
|
209
|
+
return {
|
|
210
|
+
type: event.type,
|
|
211
|
+
usage: event.usage,
|
|
212
|
+
willContinue: event.willContinue,
|
|
213
|
+
};
|
|
214
|
+
case "input_applied":
|
|
215
|
+
case "input_rejected":
|
|
216
|
+
return {
|
|
217
|
+
...event,
|
|
218
|
+
content: summarizeTraceText(event.content),
|
|
219
|
+
};
|
|
220
|
+
case "todos_updated":
|
|
221
|
+
return { type: event.type, count: event.todos.length };
|
|
222
|
+
default:
|
|
223
|
+
return { ...event };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export function resetDebugTraceForTests() {
|
|
227
|
+
initialized = false;
|
|
228
|
+
tracePath = undefined;
|
|
229
|
+
runId = undefined;
|
|
230
|
+
sequence = 0;
|
|
231
|
+
startedAt = performance.now();
|
|
232
|
+
baseContext = {};
|
|
233
|
+
defaultPruneDone = false;
|
|
234
|
+
}
|
|
235
|
+
function ensureTracePath() {
|
|
236
|
+
if (!isDebugTraceEnabled())
|
|
237
|
+
return undefined;
|
|
238
|
+
if (initialized)
|
|
239
|
+
return tracePath;
|
|
240
|
+
initialized = true;
|
|
241
|
+
startedAt = performance.now();
|
|
242
|
+
runId = resolveRunId();
|
|
243
|
+
tracePath = resolveTracePath(runId);
|
|
244
|
+
try {
|
|
245
|
+
mkdirSync(dirname(tracePath), { recursive: true });
|
|
246
|
+
pruneDefaultTraceDirs();
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// The write path may still fail later; tracing remains best-effort.
|
|
250
|
+
}
|
|
251
|
+
return tracePath;
|
|
252
|
+
}
|
|
253
|
+
function resolveRunId() {
|
|
254
|
+
const explicit = process.env.BUBBLE_TRACE_RUN_ID?.trim();
|
|
255
|
+
if (explicit)
|
|
256
|
+
return sanitizeFileSegment(explicit);
|
|
257
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
258
|
+
return `trace-${stamp}-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
259
|
+
}
|
|
260
|
+
function resolveTracePath(id) {
|
|
261
|
+
const explicitPath = process.env.BUBBLE_TRACE_PATH?.trim();
|
|
262
|
+
if (explicitPath)
|
|
263
|
+
return isAbsolute(explicitPath) ? explicitPath : join(process.cwd(), explicitPath);
|
|
264
|
+
const value = process.env.BUBBLE_TRACE?.trim();
|
|
265
|
+
if (value && !TRUE_VALUES.has(value.toLowerCase()) && !FALSE_VALUES.has(value.toLowerCase())) {
|
|
266
|
+
return isAbsolute(value) ? value : join(process.cwd(), value);
|
|
267
|
+
}
|
|
268
|
+
const dateKey = new Date().toISOString().slice(0, 10);
|
|
269
|
+
return join(getBubbleHome(), "debug-runs", dateKey, `${id}.jsonl`);
|
|
270
|
+
}
|
|
271
|
+
function pruneDefaultTraceDirs() {
|
|
272
|
+
if (defaultPruneDone || process.env.BUBBLE_TRACE_PATH?.trim())
|
|
273
|
+
return;
|
|
274
|
+
defaultPruneDone = true;
|
|
275
|
+
const maxAgeDays = Number(process.env.BUBBLE_TRACE_MAX_AGE_DAYS ?? DEFAULT_MAX_AGE_DAYS);
|
|
276
|
+
if (!Number.isFinite(maxAgeDays) || maxAgeDays <= 0)
|
|
277
|
+
return;
|
|
278
|
+
const root = join(getBubbleHome(), "debug-runs");
|
|
279
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
280
|
+
let entries;
|
|
281
|
+
try {
|
|
282
|
+
entries = readdirSync(root);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
const path = join(root, entry);
|
|
289
|
+
try {
|
|
290
|
+
const stat = statSync(path);
|
|
291
|
+
if (stat.mtimeMs < cutoff)
|
|
292
|
+
rmSync(path, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Ignore cleanup failures.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function summarizeString(value) {
|
|
300
|
+
const byteLength = Buffer.byteLength(value, "utf8");
|
|
301
|
+
const summary = {
|
|
302
|
+
type: "string",
|
|
303
|
+
chars: value.length,
|
|
304
|
+
bytes: byteLength,
|
|
305
|
+
hash: hashString(value),
|
|
306
|
+
};
|
|
307
|
+
if (isDebugTraceRawEnabled()) {
|
|
308
|
+
summary.raw = truncateRaw(value, byteLength);
|
|
309
|
+
}
|
|
310
|
+
return summary;
|
|
311
|
+
}
|
|
312
|
+
function summarizeJson(value) {
|
|
313
|
+
const json = safeJsonStringify(value);
|
|
314
|
+
if (json === undefined)
|
|
315
|
+
return { serializable: false };
|
|
316
|
+
const byteLength = Buffer.byteLength(json, "utf8");
|
|
317
|
+
const summary = {
|
|
318
|
+
serializable: true,
|
|
319
|
+
chars: json.length,
|
|
320
|
+
bytes: byteLength,
|
|
321
|
+
hash: hashString(json),
|
|
322
|
+
};
|
|
323
|
+
if (isDebugTraceRawEnabled()) {
|
|
324
|
+
summary.raw = truncateRaw(json, byteLength);
|
|
325
|
+
}
|
|
326
|
+
return summary;
|
|
327
|
+
}
|
|
328
|
+
function sanitizeTraceValue(value) {
|
|
329
|
+
if (value === undefined)
|
|
330
|
+
return undefined;
|
|
331
|
+
if (value === null)
|
|
332
|
+
return null;
|
|
333
|
+
if (typeof value === "string")
|
|
334
|
+
return value;
|
|
335
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
336
|
+
return value;
|
|
337
|
+
if (Array.isArray(value))
|
|
338
|
+
return value.map((item) => sanitizeTraceValue(item));
|
|
339
|
+
if (typeof value === "object") {
|
|
340
|
+
const out = {};
|
|
341
|
+
for (const [key, item] of Object.entries(value)) {
|
|
342
|
+
if (item !== undefined)
|
|
343
|
+
out[key] = sanitizeTraceValue(item);
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
return String(value);
|
|
348
|
+
}
|
|
349
|
+
function truncateRaw(value, byteLength) {
|
|
350
|
+
const limit = Number(process.env.BUBBLE_TRACE_RAW_MAX_BYTES ?? DEFAULT_RAW_MAX_BYTES);
|
|
351
|
+
if (!Number.isFinite(limit) || limit <= 0 || byteLength <= limit)
|
|
352
|
+
return value;
|
|
353
|
+
return {
|
|
354
|
+
value: value.slice(0, Math.max(0, limit)),
|
|
355
|
+
truncated: true,
|
|
356
|
+
bytes: byteLength,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function truncateString(value, maxChars) {
|
|
360
|
+
if (!value)
|
|
361
|
+
return undefined;
|
|
362
|
+
return value.length > maxChars ? `${value.slice(0, maxChars)}...` : value;
|
|
363
|
+
}
|
|
364
|
+
function hashString(value) {
|
|
365
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
366
|
+
}
|
|
367
|
+
function safeJsonStringify(value) {
|
|
368
|
+
try {
|
|
369
|
+
return JSON.stringify(value);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function dropUndefined(value) {
|
|
376
|
+
const out = {};
|
|
377
|
+
for (const [key, item] of Object.entries(value)) {
|
|
378
|
+
if (item !== undefined)
|
|
379
|
+
out[key] = item;
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
function sanitizeFileSegment(value) {
|
|
384
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "trace";
|
|
385
|
+
}
|
|
@@ -32,6 +32,15 @@ export function formatApprovalRequest(req) {
|
|
|
32
32
|
`\n**diff:**\n\`\`\`diff\n${truncate(req.diff, DIFF_PREVIEW_MAX)}\n\`\`\``,
|
|
33
33
|
].join("\n"),
|
|
34
34
|
};
|
|
35
|
+
case "patch":
|
|
36
|
+
return {
|
|
37
|
+
title: "应用补丁",
|
|
38
|
+
body: [
|
|
39
|
+
`**files:** ${req.paths.length}`,
|
|
40
|
+
`**path:** \`${truncate(req.path, PATH_PREVIEW_MAX)}\``,
|
|
41
|
+
`\n**diff:**\n\`\`\`diff\n${truncate(req.diff, DIFF_PREVIEW_MAX)}\n\`\`\``,
|
|
42
|
+
].join("\n"),
|
|
43
|
+
};
|
|
35
44
|
case "lsp":
|
|
36
45
|
return {
|
|
37
46
|
title: `LSP 操作 (${req.operation})`,
|
package/dist/feishu/serve.js
CHANGED
|
@@ -91,7 +91,13 @@ export async function serveFeishu(opts = {}) {
|
|
|
91
91
|
if (mcpLoaded.servers.length > 0) {
|
|
92
92
|
await mcpManager.start();
|
|
93
93
|
}
|
|
94
|
-
const createProvider = (providerId, apiKey, baseURL, promptCacheKey) => createProviderInstance({
|
|
94
|
+
const createProvider = (providerId, apiKey, baseURL, promptCacheKey) => createProviderInstance({
|
|
95
|
+
providerId,
|
|
96
|
+
apiKey,
|
|
97
|
+
baseURL,
|
|
98
|
+
promptCacheKey,
|
|
99
|
+
openAICodexAuth: providerRegistry.createOpenAICodexAuthAdapter(providerId),
|
|
100
|
+
});
|
|
95
101
|
const createProviderForRoute = async (route, promptCacheKey) => {
|
|
96
102
|
const target = providerRegistry.getConfigured().find((p) => p.id === route.providerId);
|
|
97
103
|
if (!target?.apiKey) {
|