chainlesschain 0.45.64 → 0.45.66
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/package.json +1 -1
- package/src/gateways/ws/session-protocol.js +162 -105
- package/src/gateways/ws/worktree-protocol.js +134 -141
- package/src/lib/agent-core.js +173 -13
- package/src/lib/interaction-adapter.js +45 -0
- package/src/lib/plan-mode.js +3 -1
- package/src/lib/session-manager.js +45 -8
- package/src/lib/web-ui-envelope.js +94 -0
- package/src/lib/web-ui-server.js +13 -1
- package/src/lib/ws-agent-handler.js +8 -1
- package/src/lib/ws-session-manager.js +388 -20
- package/src/runtime/agent-runtime.js +140 -37
- package/src/runtime/coding-agent-contract.js +294 -0
- package/src/runtime/coding-agent-events.cjs +372 -0
- package/src/runtime/coding-agent-managed-tool-policy.cjs +294 -0
- package/src/runtime/coding-agent-policy.cjs +354 -0
- package/src/runtime/coding-agent-shell-policy.cjs +233 -0
- package/src/runtime/contracts/session-record.js +13 -0
- package/src/runtime/index.js +14 -0
- package/src/runtime/runtime-events.js +27 -0
- package/src/tools/index.js +12 -0
- package/src/tools/legacy-agent-tools.js +12 -157
package/src/lib/agent-core.js
CHANGED
|
@@ -18,6 +18,8 @@ import fs from "fs";
|
|
|
18
18
|
import path from "path";
|
|
19
19
|
import { execSync } from "child_process";
|
|
20
20
|
import os from "os";
|
|
21
|
+
import sharedCodingAgentPolicy from "../runtime/coding-agent-policy.cjs";
|
|
22
|
+
import sharedShellPolicy from "../runtime/coding-agent-shell-policy.cjs";
|
|
21
23
|
import { getPlanModeManager } from "./plan-mode.js";
|
|
22
24
|
import { CLISkillLoader } from "./skill-loader.js";
|
|
23
25
|
import { executeHooks, HookEvents } from "./hook-manager.js";
|
|
@@ -32,6 +34,9 @@ import { createToolContext } from "../tools/tool-context.js";
|
|
|
32
34
|
import { createToolTelemetryRecord } from "../tools/tool-telemetry.js";
|
|
33
35
|
import { DEFAULT_TOOL_DESCRIPTORS } from "../tools/registry.js";
|
|
34
36
|
|
|
37
|
+
const { isReadOnlyGitCommand, normalizeGitCommand } = sharedCodingAgentPolicy;
|
|
38
|
+
const { evaluateShellCommandPolicy } = sharedShellPolicy;
|
|
39
|
+
|
|
35
40
|
// ─── Tool definitions ────────────────────────────────────────────────────
|
|
36
41
|
|
|
37
42
|
export const AGENT_TOOLS = [
|
|
@@ -91,7 +96,7 @@ export const AGENT_TOOLS = [
|
|
|
91
96
|
function: {
|
|
92
97
|
name: "run_shell",
|
|
93
98
|
description:
|
|
94
|
-
"Execute a shell command and return the output. Use for running tests,
|
|
99
|
+
"Execute a shell command and return the output. Use for running tests, linting, builds, and other non-git workspace commands.",
|
|
95
100
|
parameters: {
|
|
96
101
|
type: "object",
|
|
97
102
|
properties: {
|
|
@@ -105,6 +110,29 @@ export const AGENT_TOOLS = [
|
|
|
105
110
|
},
|
|
106
111
|
},
|
|
107
112
|
},
|
|
113
|
+
{
|
|
114
|
+
type: "function",
|
|
115
|
+
function: {
|
|
116
|
+
name: "git",
|
|
117
|
+
description:
|
|
118
|
+
"Run a git command inside the workspace. Use this instead of run_shell for git status, diff, log, commit, branch, and related repository operations.",
|
|
119
|
+
parameters: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
command: {
|
|
123
|
+
type: "string",
|
|
124
|
+
description:
|
|
125
|
+
'Git subcommand to execute, for example "status", "diff --stat", or "log --oneline -5"',
|
|
126
|
+
},
|
|
127
|
+
cwd: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Working directory (optional)",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ["command"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
108
136
|
{
|
|
109
137
|
type: "function",
|
|
110
138
|
function: {
|
|
@@ -287,6 +315,11 @@ export function getAgentToolDefinitions({
|
|
|
287
315
|
const disabledNames = new Set(
|
|
288
316
|
Array.isArray(disabledTools) ? disabledTools : [],
|
|
289
317
|
);
|
|
318
|
+
const extraToolNames = new Set(
|
|
319
|
+
(Array.isArray(extraTools) ? extraTools : [])
|
|
320
|
+
.map((tool) => tool?.function?.name)
|
|
321
|
+
.filter(Boolean),
|
|
322
|
+
);
|
|
290
323
|
const allTools = mergeToolDefinitions(
|
|
291
324
|
AGENT_TOOLS,
|
|
292
325
|
Array.isArray(extraTools) ? extraTools : [],
|
|
@@ -295,7 +328,9 @@ export function getAgentToolDefinitions({
|
|
|
295
328
|
return allTools.filter((tool) => {
|
|
296
329
|
const name = tool?.function?.name;
|
|
297
330
|
if (!name) return false;
|
|
298
|
-
if (allowedNames && !allowedNames.has(name))
|
|
331
|
+
if (allowedNames && !allowedNames.has(name) && !extraToolNames.has(name)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
299
334
|
if (disabledNames.has(name)) return false;
|
|
300
335
|
return true;
|
|
301
336
|
});
|
|
@@ -407,6 +442,7 @@ Key behaviors:
|
|
|
407
442
|
- When asked to modify code, read the file first, then edit it
|
|
408
443
|
- When asked to create something, use write_file to create it
|
|
409
444
|
- When asked to run/test something, use run_shell to execute it
|
|
445
|
+
- When asked about git status, diff, log, or other repository operations, use the git tool instead of run_shell
|
|
410
446
|
- When asked about files or code, use read_file and search_files to find information
|
|
411
447
|
- You have multi-layer skills (built-in, marketplace, global, project-level) — use list_skills to discover them and run_skill to execute them
|
|
412
448
|
- Always explain what you're doing and show results
|
|
@@ -575,7 +611,14 @@ export async function executeTool(name, args, context = {}) {
|
|
|
575
611
|
const hookDb = context.hookDb || null;
|
|
576
612
|
const skillLoader = context.skillLoader || _defaultSkillLoader;
|
|
577
613
|
const cwd = context.cwd || process.cwd();
|
|
578
|
-
const
|
|
614
|
+
const planManager = context.planManager || getPlanModeManager();
|
|
615
|
+
const localToolDescriptor =
|
|
616
|
+
context.externalToolDescriptors &&
|
|
617
|
+
typeof context.externalToolDescriptors === "object"
|
|
618
|
+
? context.externalToolDescriptors[name] || null
|
|
619
|
+
: null;
|
|
620
|
+
const runtimeDescriptor =
|
|
621
|
+
getRuntimeToolDescriptor(name) || localToolDescriptor;
|
|
579
622
|
const toolContext = createToolContext({
|
|
580
623
|
toolName: runtimeDescriptor?.name || name,
|
|
581
624
|
cwd,
|
|
@@ -600,7 +643,21 @@ export async function executeTool(name, args, context = {}) {
|
|
|
600
643
|
: null;
|
|
601
644
|
const isExternalHostTool =
|
|
602
645
|
hostToolPolicy && !STATIC_AGENT_TOOL_NAMES.has(name);
|
|
603
|
-
|
|
646
|
+
const isExternalLocalTool =
|
|
647
|
+
localToolDescriptor && !STATIC_AGENT_TOOL_NAMES.has(name);
|
|
648
|
+
const hostPolicyAllowsReadOnlyGit =
|
|
649
|
+
name === "git" &&
|
|
650
|
+
hostToolPolicy?.planModeBehavior === "readonly-conditional" &&
|
|
651
|
+
isReadOnlyGitCommand(args.command);
|
|
652
|
+
const localReadOnlyAllowedInPlanMode =
|
|
653
|
+
isExternalLocalTool &&
|
|
654
|
+
planManager.isActive() &&
|
|
655
|
+
localToolDescriptor?.isReadOnly === true;
|
|
656
|
+
if (
|
|
657
|
+
hostToolPolicy &&
|
|
658
|
+
hostToolPolicy.allowed === false &&
|
|
659
|
+
!hostPolicyAllowsReadOnlyGit
|
|
660
|
+
) {
|
|
604
661
|
return {
|
|
605
662
|
error: `[Host Policy] Tool "${name}" is blocked by desktop host policy. ${hostToolPolicy.reason || "Desktop approval has not been synchronized yet."}`,
|
|
606
663
|
policy: {
|
|
@@ -613,20 +670,24 @@ export async function executeTool(name, args, context = {}) {
|
|
|
613
670
|
}
|
|
614
671
|
|
|
615
672
|
// Plan mode: check if tool is allowed
|
|
616
|
-
const planManager = getPlanModeManager();
|
|
617
673
|
if (
|
|
618
674
|
planManager.isActive() &&
|
|
675
|
+
!(name === "git" && isReadOnlyGitCommand(args.command)) &&
|
|
619
676
|
!planManager.isToolAllowed(name) &&
|
|
620
|
-
!(isExternalHostTool && hostToolPolicy?.allowed === true)
|
|
677
|
+
!(isExternalHostTool && hostToolPolicy?.allowed === true) &&
|
|
678
|
+
!localReadOnlyAllowedInPlanMode
|
|
621
679
|
) {
|
|
622
680
|
planManager.addPlanItem({
|
|
623
681
|
title: `${name}: ${formatToolArgs(name, args)}`,
|
|
624
682
|
tool: name,
|
|
625
683
|
params: args,
|
|
626
684
|
estimatedImpact:
|
|
627
|
-
name === "run_shell" ||
|
|
685
|
+
name === "run_shell" ||
|
|
686
|
+
name === "run_code" ||
|
|
687
|
+
name === "git" ||
|
|
688
|
+
localToolDescriptor?.riskLevel === "high"
|
|
628
689
|
? "high"
|
|
629
|
-
: name === "write_file"
|
|
690
|
+
: name === "write_file" || localToolDescriptor?.riskLevel === "medium"
|
|
630
691
|
? "medium"
|
|
631
692
|
: "low",
|
|
632
693
|
});
|
|
@@ -659,6 +720,9 @@ export async function executeTool(name, args, context = {}) {
|
|
|
659
720
|
parentMessages: context.parentMessages,
|
|
660
721
|
interaction: context.interaction,
|
|
661
722
|
hostManagedToolPolicy: context.hostManagedToolPolicy || null,
|
|
723
|
+
externalToolDescriptors: context.externalToolDescriptors || null,
|
|
724
|
+
externalToolExecutors: context.externalToolExecutors || null,
|
|
725
|
+
mcpClient: context.mcpClient || null,
|
|
662
726
|
});
|
|
663
727
|
} catch (err) {
|
|
664
728
|
if (hookDb) {
|
|
@@ -715,9 +779,23 @@ export async function executeTool(name, args, context = {}) {
|
|
|
715
779
|
async function executeToolInner(
|
|
716
780
|
name,
|
|
717
781
|
args,
|
|
718
|
-
{
|
|
782
|
+
{
|
|
783
|
+
skillLoader,
|
|
784
|
+
cwd,
|
|
785
|
+
parentMessages,
|
|
786
|
+
interaction,
|
|
787
|
+
hostManagedToolPolicy,
|
|
788
|
+
externalToolDescriptors,
|
|
789
|
+
externalToolExecutors,
|
|
790
|
+
mcpClient,
|
|
791
|
+
},
|
|
719
792
|
) {
|
|
720
|
-
const
|
|
793
|
+
const localToolDescriptor =
|
|
794
|
+
externalToolDescriptors && typeof externalToolDescriptors === "object"
|
|
795
|
+
? externalToolDescriptors[name] || null
|
|
796
|
+
: null;
|
|
797
|
+
const runtimeDescriptor =
|
|
798
|
+
getRuntimeToolDescriptor(name) || localToolDescriptor;
|
|
721
799
|
const hostToolPolicies =
|
|
722
800
|
hostManagedToolPolicy?.tools || hostManagedToolPolicy?.toolPolicies || null;
|
|
723
801
|
const hostToolPolicy =
|
|
@@ -754,6 +832,10 @@ async function executeToolInner(
|
|
|
754
832
|
}
|
|
755
833
|
return DEFAULT_TOOL_DESCRIPTOR_MAP.get("shell");
|
|
756
834
|
};
|
|
835
|
+
const localToolExecutor =
|
|
836
|
+
externalToolExecutors && typeof externalToolExecutors === "object"
|
|
837
|
+
? externalToolExecutors[name] || null
|
|
838
|
+
: null;
|
|
757
839
|
switch (name) {
|
|
758
840
|
case "read_file": {
|
|
759
841
|
const filePath = path.resolve(cwd, args.path);
|
|
@@ -799,6 +881,18 @@ async function executeToolInner(
|
|
|
799
881
|
}
|
|
800
882
|
|
|
801
883
|
case "run_shell": {
|
|
884
|
+
const shellPolicy = evaluateShellCommandPolicy(args.command);
|
|
885
|
+
const override = resolveShellDescriptor(args.command);
|
|
886
|
+
if (!shellPolicy.allowed) {
|
|
887
|
+
return attachDescriptor(
|
|
888
|
+
{
|
|
889
|
+
error: `[Shell Policy] ${shellPolicy.reason}`,
|
|
890
|
+
shellCommandPolicy: shellPolicy,
|
|
891
|
+
},
|
|
892
|
+
override || runtimeDescriptor,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
802
896
|
try {
|
|
803
897
|
const output = execSync(args.command, {
|
|
804
898
|
cwd: args.cwd || cwd,
|
|
@@ -806,26 +900,57 @@ async function executeToolInner(
|
|
|
806
900
|
timeout: 60000,
|
|
807
901
|
maxBuffer: 1024 * 1024,
|
|
808
902
|
});
|
|
809
|
-
const override = resolveShellDescriptor(args.command);
|
|
810
903
|
return attachDescriptor(
|
|
811
904
|
{
|
|
812
905
|
stdout: output.substring(0, 30000),
|
|
906
|
+
shellCommandPolicy: shellPolicy,
|
|
813
907
|
},
|
|
814
908
|
override || runtimeDescriptor,
|
|
815
909
|
);
|
|
816
910
|
} catch (err) {
|
|
817
|
-
const override = resolveShellDescriptor(args.command);
|
|
818
911
|
return attachDescriptor(
|
|
819
912
|
{
|
|
820
913
|
error: err.message.substring(0, 2000),
|
|
821
914
|
stderr: (err.stderr || "").substring(0, 2000),
|
|
822
915
|
exitCode: err.status,
|
|
916
|
+
shellCommandPolicy: shellPolicy,
|
|
823
917
|
},
|
|
824
918
|
override || runtimeDescriptor,
|
|
825
919
|
);
|
|
826
920
|
}
|
|
827
921
|
}
|
|
828
922
|
|
|
923
|
+
case "git": {
|
|
924
|
+
const normalizedCommand = normalizeGitCommand(args.command);
|
|
925
|
+
if (!normalizedCommand) {
|
|
926
|
+
return attachDescriptor({
|
|
927
|
+
error: "Git command is required.",
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
const output = execSync(`git ${normalizedCommand}`, {
|
|
933
|
+
cwd: args.cwd || cwd,
|
|
934
|
+
encoding: "utf8",
|
|
935
|
+
timeout: 60000,
|
|
936
|
+
maxBuffer: 1024 * 1024,
|
|
937
|
+
});
|
|
938
|
+
return attachDescriptor({
|
|
939
|
+
stdout: output.substring(0, 30000),
|
|
940
|
+
command: normalizedCommand,
|
|
941
|
+
readOnly: isReadOnlyGitCommand(normalizedCommand),
|
|
942
|
+
});
|
|
943
|
+
} catch (err) {
|
|
944
|
+
return attachDescriptor({
|
|
945
|
+
error: err.message.substring(0, 2000),
|
|
946
|
+
stderr: (err.stderr || "").substring(0, 2000),
|
|
947
|
+
exitCode: err.status,
|
|
948
|
+
command: normalizedCommand,
|
|
949
|
+
readOnly: isReadOnlyGitCommand(normalizedCommand),
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
829
954
|
case "run_code": {
|
|
830
955
|
return attachDescriptor(await _executeRunCode(args, cwd));
|
|
831
956
|
}
|
|
@@ -988,6 +1113,30 @@ async function executeToolInner(
|
|
|
988
1113
|
}
|
|
989
1114
|
|
|
990
1115
|
default:
|
|
1116
|
+
if (localToolExecutor?.kind === "mcp") {
|
|
1117
|
+
if (!mcpClient || typeof mcpClient.callTool !== "function") {
|
|
1118
|
+
return attachDescriptor({
|
|
1119
|
+
error: `MCP client is unavailable for tool: ${name}`,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
const result = await mcpClient.callTool(
|
|
1125
|
+
localToolExecutor.serverName,
|
|
1126
|
+
localToolExecutor.toolName,
|
|
1127
|
+
args || {},
|
|
1128
|
+
);
|
|
1129
|
+
if (result && typeof result === "object") {
|
|
1130
|
+
return attachDescriptor(result);
|
|
1131
|
+
}
|
|
1132
|
+
return attachDescriptor({ result });
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
return attachDescriptor({
|
|
1135
|
+
error: `MCP tool execution failed: ${err.message}`,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
991
1140
|
if (
|
|
992
1141
|
hostToolDefinition &&
|
|
993
1142
|
interaction &&
|
|
@@ -1332,8 +1481,12 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1332
1481
|
|
|
1333
1482
|
const persona = _loadProjectPersona(options.cwd);
|
|
1334
1483
|
const tools = getAgentToolDefinitions({
|
|
1484
|
+
names: options.enabledToolNames,
|
|
1335
1485
|
disabledTools: persona?.toolsDisabled,
|
|
1336
|
-
extraTools:
|
|
1486
|
+
extraTools: [
|
|
1487
|
+
...(options.hostManagedToolPolicy?.toolDefinitions || []),
|
|
1488
|
+
...(options.extraToolDefinitions || []),
|
|
1489
|
+
],
|
|
1337
1490
|
});
|
|
1338
1491
|
|
|
1339
1492
|
const lastUserMsg = [...rawMessages].reverse().find((m) => m.role === "user");
|
|
@@ -1518,7 +1671,12 @@ export async function* agentLoop(messages, options) {
|
|
|
1518
1671
|
hookDb: options.hookDb || null,
|
|
1519
1672
|
skillLoader: options.skillLoader || _defaultSkillLoader,
|
|
1520
1673
|
cwd: options.cwd || process.cwd(),
|
|
1674
|
+
planManager: options.planManager || null,
|
|
1675
|
+
sessionId: options.sessionId || null,
|
|
1521
1676
|
hostManagedToolPolicy: options.hostManagedToolPolicy || null,
|
|
1677
|
+
externalToolDescriptors: options.externalToolDescriptors || null,
|
|
1678
|
+
externalToolExecutors: options.externalToolExecutors || null,
|
|
1679
|
+
mcpClient: options.mcpClient || null,
|
|
1522
1680
|
parentMessages: messages, // pass parent messages for sub-agent auto-condensation
|
|
1523
1681
|
interaction: options.interaction || null,
|
|
1524
1682
|
};
|
|
@@ -1646,6 +1804,8 @@ export function formatToolArgs(name, args) {
|
|
|
1646
1804
|
return args.path;
|
|
1647
1805
|
case "run_shell":
|
|
1648
1806
|
return args.command;
|
|
1807
|
+
case "git":
|
|
1808
|
+
return args.command;
|
|
1649
1809
|
case "search_files":
|
|
1650
1810
|
return args.pattern;
|
|
1651
1811
|
case "list_dir":
|
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createHash } from "crypto";
|
|
10
|
+
import {
|
|
11
|
+
createCodingAgentEvent,
|
|
12
|
+
CodingAgentSequenceTracker,
|
|
13
|
+
CODING_AGENT_EVENT_TYPES,
|
|
14
|
+
LEGACY_TO_UNIFIED_TYPE,
|
|
15
|
+
} from "../runtime/runtime-events.js";
|
|
16
|
+
|
|
17
|
+
// Whitelist of event types the CLI runtime should emit as unified envelopes
|
|
18
|
+
// (with source: "cli-runtime"). Anything not in this set keeps the legacy
|
|
19
|
+
// raw shape so non-coding-agent transports (host-tool callbacks, generic
|
|
20
|
+
// progress events, etc.) are unaffected.
|
|
21
|
+
const CODING_AGENT_EVENT_TYPE_SET = new Set([
|
|
22
|
+
...Object.values(CODING_AGENT_EVENT_TYPES),
|
|
23
|
+
...Object.keys(LEGACY_TO_UNIFIED_TYPE),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function isCodingAgentEventType(type) {
|
|
27
|
+
return typeof type === "string" && CODING_AGENT_EVENT_TYPE_SET.has(type);
|
|
28
|
+
}
|
|
10
29
|
|
|
11
30
|
/**
|
|
12
31
|
* Base class — subclasses must implement askInput, askSelect, askConfirm, emit.
|
|
@@ -87,6 +106,10 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
87
106
|
this.sessionId = sessionId;
|
|
88
107
|
/** @type {Map<string, {resolve: Function, reject: Function}>} */
|
|
89
108
|
this._pending = new Map();
|
|
109
|
+
// Per-instance sequence tracker so monotonic sequences are scoped to
|
|
110
|
+
// this WS session instead of leaking across sessions via the process-
|
|
111
|
+
// global default tracker.
|
|
112
|
+
this._sequenceTracker = new CodingAgentSequenceTracker();
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
/** Generate a unique request id */
|
|
@@ -184,6 +207,28 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
184
207
|
}
|
|
185
208
|
|
|
186
209
|
emit(eventType, data) {
|
|
210
|
+
// Coding-agent events flow as the unified envelope so the Desktop bridge
|
|
211
|
+
// (and any other consumer) sees a single canonical shape with
|
|
212
|
+
// source: "cli-runtime" baked in — no Bridge-layer translation needed.
|
|
213
|
+
if (isCodingAgentEventType(eventType)) {
|
|
214
|
+
const payload = data && typeof data === "object" ? { ...data } : {};
|
|
215
|
+
const requestId = payload.requestId || null;
|
|
216
|
+
const sessionId = payload.sessionId || this.sessionId || null;
|
|
217
|
+
delete payload.requestId;
|
|
218
|
+
delete payload.sessionId;
|
|
219
|
+
|
|
220
|
+
const envelope = createCodingAgentEvent(eventType, payload, {
|
|
221
|
+
sessionId,
|
|
222
|
+
requestId,
|
|
223
|
+
source: "cli-runtime",
|
|
224
|
+
tracker: this._sequenceTracker,
|
|
225
|
+
});
|
|
226
|
+
this._sendWs(envelope);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Non-coding-agent events (host-tool callbacks, generic progress, etc.)
|
|
231
|
+
// keep the legacy raw shape — they are not part of the v1.0 protocol.
|
|
187
232
|
this._sendWs({
|
|
188
233
|
type: eventType,
|
|
189
234
|
sessionId: this.sessionId,
|
package/src/lib/plan-mode.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Plan Mode for CLI Agent REPL
|
|
3
3
|
*
|
|
4
4
|
* During plan mode, the AI can only use read-only tools (read_file, search_files, list_dir, list_skills).
|
|
5
|
-
* Write/execute tools (write_file, edit_file, run_shell, run_skill) are blocked until the plan is approved.
|
|
5
|
+
* Write/execute tools (write_file, edit_file, run_shell, git, run_skill) are blocked until the plan is approved.
|
|
6
6
|
*
|
|
7
7
|
* Lightweight port of desktop-app-vue/src/main/ai-engine/plan-mode/index.js
|
|
8
8
|
*/
|
|
@@ -48,6 +48,7 @@ const WRITE_TOOLS = new Set([
|
|
|
48
48
|
"write_file",
|
|
49
49
|
"edit_file",
|
|
50
50
|
"run_shell",
|
|
51
|
+
"git",
|
|
51
52
|
"run_skill",
|
|
52
53
|
]);
|
|
53
54
|
|
|
@@ -66,6 +67,7 @@ const TOOL_RISK_WEIGHTS = {
|
|
|
66
67
|
edit_file: 2,
|
|
67
68
|
run_skill: 2,
|
|
68
69
|
run_shell: 3,
|
|
70
|
+
git: 3,
|
|
69
71
|
};
|
|
70
72
|
|
|
71
73
|
const IMPACT_MULTIPLIERS = {
|
|
@@ -16,11 +16,18 @@ function ensureSessionsTable(db) {
|
|
|
16
16
|
model TEXT DEFAULT '',
|
|
17
17
|
message_count INTEGER DEFAULT 0,
|
|
18
18
|
messages TEXT DEFAULT '[]',
|
|
19
|
+
metadata TEXT DEFAULT '{}',
|
|
19
20
|
summary TEXT DEFAULT '',
|
|
20
21
|
created_at TEXT DEFAULT (datetime('now')),
|
|
21
22
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
22
23
|
)
|
|
23
24
|
`);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
db.exec("ALTER TABLE llm_sessions ADD COLUMN metadata TEXT DEFAULT '{}'");
|
|
28
|
+
} catch (_error) {
|
|
29
|
+
// Column already exists or ALTER TABLE is unsupported by the mock DB.
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
@@ -34,13 +41,14 @@ export function createSession(db, options = {}) {
|
|
|
34
41
|
`session-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
|
|
35
42
|
|
|
36
43
|
db.prepare(
|
|
37
|
-
`INSERT INTO llm_sessions (id, title, provider, model, messages) VALUES (?, ?, ?, ?, ?)`,
|
|
44
|
+
`INSERT INTO llm_sessions (id, title, provider, model, messages, metadata) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
38
45
|
).run(
|
|
39
46
|
id,
|
|
40
47
|
options.title || "Untitled",
|
|
41
48
|
options.provider || "",
|
|
42
49
|
options.model || "",
|
|
43
50
|
JSON.stringify(options.messages || []),
|
|
51
|
+
JSON.stringify(options.metadata || {}),
|
|
44
52
|
);
|
|
45
53
|
|
|
46
54
|
return { id, title: options.title || "Untitled" };
|
|
@@ -71,14 +79,26 @@ export function addMessage(db, sessionId, role, content) {
|
|
|
71
79
|
/**
|
|
72
80
|
* Save all messages at once (batch update)
|
|
73
81
|
*/
|
|
74
|
-
export function saveMessages(db, sessionId, messages) {
|
|
82
|
+
export function saveMessages(db, sessionId, messages, metadata) {
|
|
75
83
|
ensureSessionsTable(db);
|
|
76
84
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
const hasMetadata = metadata !== undefined;
|
|
86
|
+
const result = hasMetadata
|
|
87
|
+
? db
|
|
88
|
+
.prepare(
|
|
89
|
+
`UPDATE llm_sessions SET messages = ?, metadata = ?, message_count = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
90
|
+
)
|
|
91
|
+
.run(
|
|
92
|
+
JSON.stringify(messages),
|
|
93
|
+
JSON.stringify(metadata || {}),
|
|
94
|
+
messages.length,
|
|
95
|
+
sessionId,
|
|
96
|
+
)
|
|
97
|
+
: db
|
|
98
|
+
.prepare(
|
|
99
|
+
`UPDATE llm_sessions SET messages = ?, message_count = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
100
|
+
)
|
|
101
|
+
.run(JSON.stringify(messages), messages.length, sessionId);
|
|
82
102
|
|
|
83
103
|
return { messageCount: messages.length, updated: result.changes > 0 };
|
|
84
104
|
}
|
|
@@ -105,6 +125,10 @@ export function getSession(db, sessionId) {
|
|
|
105
125
|
return {
|
|
106
126
|
...session,
|
|
107
127
|
messages: JSON.parse(session.messages || "[]"),
|
|
128
|
+
metadata:
|
|
129
|
+
typeof session.metadata === "string"
|
|
130
|
+
? JSON.parse(session.metadata || "{}")
|
|
131
|
+
: session.metadata || {},
|
|
108
132
|
};
|
|
109
133
|
}
|
|
110
134
|
|
|
@@ -119,11 +143,19 @@ export function listSessions(db, options = {}) {
|
|
|
119
143
|
return db
|
|
120
144
|
.prepare(
|
|
121
145
|
`SELECT id, title, provider, model, message_count, summary, created_at, updated_at
|
|
146
|
+
, metadata
|
|
122
147
|
FROM llm_sessions
|
|
123
148
|
ORDER BY updated_at DESC
|
|
124
149
|
LIMIT ?`,
|
|
125
150
|
)
|
|
126
|
-
.all(limit)
|
|
151
|
+
.all(limit)
|
|
152
|
+
.map((session) => ({
|
|
153
|
+
...session,
|
|
154
|
+
metadata:
|
|
155
|
+
typeof session.metadata === "string"
|
|
156
|
+
? JSON.parse(session.metadata || "{}")
|
|
157
|
+
: session.metadata || {},
|
|
158
|
+
}));
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
/**
|
|
@@ -142,6 +174,11 @@ export function updateSession(db, sessionId, updates) {
|
|
|
142
174
|
"UPDATE llm_sessions SET summary = ?, updated_at = datetime('now') WHERE id = ?",
|
|
143
175
|
).run(updates.summary, sessionId);
|
|
144
176
|
}
|
|
177
|
+
if (updates.metadata !== undefined) {
|
|
178
|
+
db.prepare(
|
|
179
|
+
"UPDATE llm_sessions SET metadata = ?, updated_at = datetime('now') WHERE id = ?",
|
|
180
|
+
).run(JSON.stringify(updates.metadata || {}), sessionId);
|
|
181
|
+
}
|
|
145
182
|
}
|
|
146
183
|
|
|
147
184
|
/**
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web UI envelope unwrap helper.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the unified Coding Agent envelope → legacy
|
|
5
|
+
* kebab-case adapter consumed by the browser bundle in `web-ui-server.js`.
|
|
6
|
+
*
|
|
7
|
+
* The same code runs in two contexts:
|
|
8
|
+
* 1. Node.js unit tests — imported as ESM/CJS and executed directly.
|
|
9
|
+
* 2. Browser — inlined into the HTML payload as a `<script>` block via
|
|
10
|
+
* `getInlineSource()`. The function body must therefore stay
|
|
11
|
+
* ES5-friendly (no spread, no const) so it parses in older runtimes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const UNIFIED_TO_LEGACY = Object.freeze({
|
|
15
|
+
"session.started": "session-created",
|
|
16
|
+
"session.resumed": "session-resumed",
|
|
17
|
+
"session.list": "session-list-result",
|
|
18
|
+
"session.closed": "session-closed",
|
|
19
|
+
"assistant.delta": "response-token",
|
|
20
|
+
"assistant.final": "response-complete",
|
|
21
|
+
"assistant.message": "response-message",
|
|
22
|
+
"assistant.thought-summary": "thought-summary",
|
|
23
|
+
"tool.call.started": "tool-executing",
|
|
24
|
+
"tool.call.completed": "tool-result",
|
|
25
|
+
"tool.call.failed": "tool-error",
|
|
26
|
+
"tool.call.skipped": "tool-skipped",
|
|
27
|
+
"plan.started": "plan-started",
|
|
28
|
+
"plan.updated": "plan-updated",
|
|
29
|
+
"plan.approval_required": "plan-ready",
|
|
30
|
+
"plan.approved": "plan-approved",
|
|
31
|
+
"plan.rejected": "plan-rejected",
|
|
32
|
+
"request.accepted": "request-accepted",
|
|
33
|
+
"request.rejected": "request-rejected",
|
|
34
|
+
"approval.requested": "approval-requested",
|
|
35
|
+
"approval.granted": "approval-granted",
|
|
36
|
+
"approval.denied": "approval-denied",
|
|
37
|
+
"model.switch": "model-switch",
|
|
38
|
+
"command.response": "command-response",
|
|
39
|
+
"slot.filling": "slot-filling",
|
|
40
|
+
"context.compaction.completed": "compression-applied",
|
|
41
|
+
"worktree.list": "worktree-list",
|
|
42
|
+
"worktree.diff": "worktree-diff",
|
|
43
|
+
"worktree.merged": "worktree-merged",
|
|
44
|
+
"worktree.merge-preview": "worktree-merge-preview",
|
|
45
|
+
"worktree.automation-applied": "worktree-automation-applied",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detect a unified envelope and unwrap its payload into a flat shape that
|
|
50
|
+
* matches the legacy kebab-case message structure.
|
|
51
|
+
*
|
|
52
|
+
* Returns the message unchanged if it is not a recognised envelope so that
|
|
53
|
+
* non-envelope traffic (auth-result, server pings, etc.) keeps working.
|
|
54
|
+
*/
|
|
55
|
+
export function unwrapEnvelope(msg) {
|
|
56
|
+
if (!msg || typeof msg !== "object") return msg;
|
|
57
|
+
if (msg.version !== "1.0") return msg;
|
|
58
|
+
if (typeof msg.eventId !== "string") return msg;
|
|
59
|
+
if (!msg.payload || typeof msg.payload !== "object") return msg;
|
|
60
|
+
const legacyType = UNIFIED_TO_LEGACY[msg.type] || msg.type;
|
|
61
|
+
const flat = Object.assign({}, msg.payload);
|
|
62
|
+
flat.type = legacyType;
|
|
63
|
+
if (msg.sessionId != null) flat.sessionId = msg.sessionId;
|
|
64
|
+
if (msg.requestId != null) flat.requestId = msg.requestId;
|
|
65
|
+
return flat;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render the helper as a `var` + `function` declaration string suitable
|
|
70
|
+
* for inlining inside the browser bundle returned by `buildHtml`.
|
|
71
|
+
*
|
|
72
|
+
* The output is intentionally ES5-compatible: it uses `var` rather than
|
|
73
|
+
* `const`, and inlines the map literal so the browser does not need to
|
|
74
|
+
* import any module.
|
|
75
|
+
*/
|
|
76
|
+
export function getInlineSource() {
|
|
77
|
+
return (
|
|
78
|
+
"var UNIFIED_TO_LEGACY = " +
|
|
79
|
+
JSON.stringify(UNIFIED_TO_LEGACY, null, 2) +
|
|
80
|
+
";\n" +
|
|
81
|
+
"function unwrapEnvelope(msg) {\n" +
|
|
82
|
+
" if (!msg || typeof msg !== 'object') return msg;\n" +
|
|
83
|
+
" if (msg.version !== '1.0') return msg;\n" +
|
|
84
|
+
" if (typeof msg.eventId !== 'string') return msg;\n" +
|
|
85
|
+
" if (!msg.payload || typeof msg.payload !== 'object') return msg;\n" +
|
|
86
|
+
" var legacyType = UNIFIED_TO_LEGACY[msg.type] || msg.type;\n" +
|
|
87
|
+
" var flat = Object.assign({}, msg.payload);\n" +
|
|
88
|
+
" flat.type = legacyType;\n" +
|
|
89
|
+
" if (msg.sessionId != null) flat.sessionId = msg.sessionId;\n" +
|
|
90
|
+
" if (msg.requestId != null) flat.requestId = msg.requestId;\n" +
|
|
91
|
+
" return flat;\n" +
|
|
92
|
+
" }"
|
|
93
|
+
);
|
|
94
|
+
}
|
package/src/lib/web-ui-server.js
CHANGED
|
@@ -12,6 +12,7 @@ import http from "http";
|
|
|
12
12
|
import fs from "fs";
|
|
13
13
|
import path from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
|
+
import { getInlineSource as getEnvelopeInlineSource } from "./web-ui-envelope.js";
|
|
15
16
|
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
|
|
@@ -654,7 +655,16 @@ function buildHtml({
|
|
|
654
655
|
send({ type: 'session-list' });
|
|
655
656
|
}
|
|
656
657
|
|
|
657
|
-
|
|
658
|
+
// Map unified envelope dot-case types back to legacy kebab-case so the
|
|
659
|
+
// existing switch table below keeps working without per-case rewrites.
|
|
660
|
+
// The CLI runtime wraps every coding-agent event in a v1.0 envelope.
|
|
661
|
+
// Source of truth lives in lib/web-ui-envelope.js — inlined here at
|
|
662
|
+
// build time so the browser bundle stays self-contained and the same
|
|
663
|
+
// unwrap logic is unit-testable in Node.
|
|
664
|
+
${getEnvelopeInlineSource()}
|
|
665
|
+
|
|
666
|
+
function handleMessage(rawMsg) {
|
|
667
|
+
var msg = unwrapEnvelope(rawMsg);
|
|
658
668
|
switch (msg.type) {
|
|
659
669
|
case 'auth-result':
|
|
660
670
|
if (msg.success) {
|
|
@@ -798,6 +808,8 @@ function buildHtml({
|
|
|
798
808
|
ws.onmessage = ev => {
|
|
799
809
|
let msg;
|
|
800
810
|
try { msg = JSON.parse(ev.data); } catch { return; }
|
|
811
|
+
// Unwrap unified envelopes so the type compare below still matches.
|
|
812
|
+
msg = unwrapEnvelope(msg);
|
|
801
813
|
if (msg.type === 'session-created' && msg.sessionId) {
|
|
802
814
|
// Replace temp id
|
|
803
815
|
sessions.delete(tempId);
|