letta-code-acp 0.0.2
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/LICENSE +202 -0
- package/README.md +226 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +569 -0
- package/dist/cli.js.map +1 -0
- package/dist/letta-process.d.ts +57 -0
- package/dist/letta-process.js +151 -0
- package/dist/letta-process.js.map +1 -0
- package/dist/mapping.d.ts +43 -0
- package/dist/mapping.js +444 -0
- package/dist/mapping.js.map +1 -0
- package/dist/wire.d.ts +90 -0
- package/dist/wire.js +16 -0
- package/dist/wire.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { parseWire } from "./wire.js";
|
|
5
|
+
/**
|
|
6
|
+
* Owns a single `letta -p --input-format stream-json --output-format stream-json`
|
|
7
|
+
* subprocess. Emits parsed wire messages and accepts JSON inputs on stdin.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle: construct -> start() -> waits for system/init -> ready.
|
|
10
|
+
* One LettaProcess per ACP session.
|
|
11
|
+
*/
|
|
12
|
+
export class LettaProcess extends EventEmitter {
|
|
13
|
+
opts;
|
|
14
|
+
child = null;
|
|
15
|
+
initMsg = null;
|
|
16
|
+
exited = false;
|
|
17
|
+
stderrBuf = "";
|
|
18
|
+
/** Cap stderr ring-buffer so long-running verbose agents don't leak memory. */
|
|
19
|
+
static STDERR_TAIL_LIMIT = 8 * 1024;
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
super();
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
}
|
|
24
|
+
async start() {
|
|
25
|
+
const bin = this.opts.lettaBin ?? process.env.LETTA_BIN ?? "letta";
|
|
26
|
+
const args = [
|
|
27
|
+
"-p",
|
|
28
|
+
"--input-format",
|
|
29
|
+
"stream-json",
|
|
30
|
+
"--output-format",
|
|
31
|
+
"stream-json",
|
|
32
|
+
"--include-partial-messages",
|
|
33
|
+
];
|
|
34
|
+
// `--conversation <id>` with an explicit id derives the agent from the
|
|
35
|
+
// conversation - headless rejects `--agent` alongside it. For the special
|
|
36
|
+
// "default" virtual id, both flags are allowed (agent is the addressing
|
|
37
|
+
// key; "default" selects its primary chat).
|
|
38
|
+
const conv = this.opts.conversationId;
|
|
39
|
+
const conversationIsExplicit = !!conv && conv !== "default";
|
|
40
|
+
if (this.opts.agentId && !conversationIsExplicit)
|
|
41
|
+
args.push("-a", this.opts.agentId);
|
|
42
|
+
if (this.opts.model)
|
|
43
|
+
args.push("-m", this.opts.model);
|
|
44
|
+
if (this.opts.permissionMode)
|
|
45
|
+
args.push("--permission-mode", this.opts.permissionMode);
|
|
46
|
+
if (conv)
|
|
47
|
+
args.push("--conversation", conv);
|
|
48
|
+
// `--new` is mutually exclusive with `--conversation` in headless.
|
|
49
|
+
if (this.opts.newConversation && !conv)
|
|
50
|
+
args.push("--new");
|
|
51
|
+
if (this.opts.reflectionTrigger) {
|
|
52
|
+
args.push("--reflection-trigger", this.opts.reflectionTrigger);
|
|
53
|
+
}
|
|
54
|
+
if (typeof this.opts.reflectionStepCount === "number") {
|
|
55
|
+
args.push("--reflection-step-count", String(this.opts.reflectionStepCount));
|
|
56
|
+
}
|
|
57
|
+
if (this.opts.extraArgs)
|
|
58
|
+
args.push(...this.opts.extraArgs);
|
|
59
|
+
const child = spawn(bin, args, {
|
|
60
|
+
cwd: this.opts.cwd,
|
|
61
|
+
env: { ...process.env, ...this.opts.env },
|
|
62
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
this.child = child;
|
|
65
|
+
const rl = createInterface({ input: child.stdout });
|
|
66
|
+
rl.on("line", (line) => {
|
|
67
|
+
const msg = parseWire(line);
|
|
68
|
+
if (!msg)
|
|
69
|
+
return;
|
|
70
|
+
if (!this.initMsg && msg.type === "system" && msg.subtype === "init") {
|
|
71
|
+
this.initMsg = msg;
|
|
72
|
+
this.emit("init", this.initMsg);
|
|
73
|
+
}
|
|
74
|
+
this.emit("wire", msg);
|
|
75
|
+
});
|
|
76
|
+
child.stderr.on("data", (chunk) => {
|
|
77
|
+
const text = chunk.toString("utf8");
|
|
78
|
+
this.stderrBuf += text;
|
|
79
|
+
if (this.stderrBuf.length > LettaProcess.STDERR_TAIL_LIMIT) {
|
|
80
|
+
this.stderrBuf = this.stderrBuf.slice(-LettaProcess.STDERR_TAIL_LIMIT);
|
|
81
|
+
}
|
|
82
|
+
this.emit("stderr", text);
|
|
83
|
+
});
|
|
84
|
+
child.on("exit", (code, signal) => {
|
|
85
|
+
this.exited = true;
|
|
86
|
+
this.emit("exit", { code, signal, stderr: this.stderrBuf });
|
|
87
|
+
});
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const onInit = (m) => {
|
|
90
|
+
cleanup();
|
|
91
|
+
resolve(m);
|
|
92
|
+
};
|
|
93
|
+
const onExit = (info) => {
|
|
94
|
+
cleanup();
|
|
95
|
+
reject(new Error(`letta exited before init (code=${info.code}): ${info.stderr.trim()}`));
|
|
96
|
+
};
|
|
97
|
+
const cleanup = () => {
|
|
98
|
+
this.off("init", onInit);
|
|
99
|
+
this.off("exit", onExit);
|
|
100
|
+
};
|
|
101
|
+
this.once("init", onInit);
|
|
102
|
+
this.once("exit", onExit);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Send a user prompt over stdin in the UserInput shape. */
|
|
106
|
+
sendUserPrompt(text) {
|
|
107
|
+
if (!this.child || this.exited)
|
|
108
|
+
throw new Error("letta process not running");
|
|
109
|
+
const input = { type: "user", message: { role: "user", content: text } };
|
|
110
|
+
this.child.stdin.write(JSON.stringify(input) + "\n");
|
|
111
|
+
}
|
|
112
|
+
/** Send a raw control request (e.g. interrupt). */
|
|
113
|
+
sendRaw(obj) {
|
|
114
|
+
if (!this.child || this.exited)
|
|
115
|
+
return;
|
|
116
|
+
this.child.stdin.write(JSON.stringify(obj) + "\n");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Cancel the in-flight turn (if any). Letta's `interrupt` control_request
|
|
120
|
+
* aborts the current operation but keeps the process alive so future
|
|
121
|
+
* prompts on the same session continue to work. We intentionally do NOT
|
|
122
|
+
* fall back to SIGINT - that would kill the subprocess and orphan the
|
|
123
|
+
* adapter's session map.
|
|
124
|
+
*/
|
|
125
|
+
cancel() {
|
|
126
|
+
if (!this.child || this.exited)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
// Letta's ControlRequest wire shape: { type, request_id, request: { subtype } }
|
|
130
|
+
// - subtype lives INSIDE request, not at the envelope level.
|
|
131
|
+
this.sendRaw({
|
|
132
|
+
type: "control_request",
|
|
133
|
+
request_id: `cancel-${Date.now()}`,
|
|
134
|
+
request: { subtype: "interrupt" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
kill() {
|
|
142
|
+
this.child?.kill("SIGTERM");
|
|
143
|
+
}
|
|
144
|
+
get init() {
|
|
145
|
+
return this.initMsg;
|
|
146
|
+
}
|
|
147
|
+
onWire(fn) {
|
|
148
|
+
this.on("wire", fn);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=letta-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"letta-process.js","sourceRoot":"","sources":["../src/letta-process.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SessionNotification } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { WireMessage } from "./wire.js";
|
|
3
|
+
/**
|
|
4
|
+
* Translate a Letta WireMessage into a partial ACP SessionNotification.
|
|
5
|
+
*
|
|
6
|
+
* Returns null for messages that have no direct user-visible mapping
|
|
7
|
+
* (lifecycle, queue events, sync markers). The caller wraps the returned
|
|
8
|
+
* `update` with the ACP sessionId.
|
|
9
|
+
*
|
|
10
|
+
* Heuristic for ToolKind: inferred from tool name. See `inferToolKind`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function wireToSessionUpdate(msg: WireMessage, sessionId: string): SessionNotification | null;
|
|
13
|
+
/**
|
|
14
|
+
* Build a human-readable title from tool name + complete args.
|
|
15
|
+
* Mirrors claude-agent-acp's toolInfoFromToolUse: include file path,
|
|
16
|
+
* pattern, or command in the title so the host UI doesn't show every
|
|
17
|
+
* row as a bare 'Read' / 'Bash' / 'Grep'.
|
|
18
|
+
*/
|
|
19
|
+
export declare function descriptiveTitle(toolName: string, rawInput: any): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* For filesystem edit tools (`Edit`, `Write`, `MultiEdit`, `ApplyPatch`):
|
|
22
|
+
* attach an ACP-style `diff` content block so the host UI renders an actual
|
|
23
|
+
* before/after view instead of the bare tool_return JSON
|
|
24
|
+
* (`{"message":"Successfully replaced 1 occurrence..."}`), which carries no
|
|
25
|
+
* useful information to the user once the change has been made.
|
|
26
|
+
*/
|
|
27
|
+
export declare function decorateFileEditToolCall(baseUpdate: any, toolName: string, rawInput: any): any;
|
|
28
|
+
/**
|
|
29
|
+
* Strip Letta `Read` tool line-number prefixes (` 42→content`) from a return
|
|
30
|
+
* body so the host renders the file contents cleanly. The prefix is purely a
|
|
31
|
+
* decoration for the model and gets in the way of agent-shell's rendering
|
|
32
|
+
* (visual noise + broken indentation).
|
|
33
|
+
*/
|
|
34
|
+
export declare function stripReadLineNumbers(text: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* For `memory`, `memory_apply_patch`, `memory_insert`, `memory_replace`:
|
|
37
|
+
* - prefix title with "memory · " so the host UI flags it visually,
|
|
38
|
+
* - try to build an ACP-style Diff content block from args (old_string/new_string
|
|
39
|
+
* for the unified `memory` tool, or file_text for `create`).
|
|
40
|
+
*
|
|
41
|
+
* Returns the enriched update (still `sessionUpdate: "tool_call"`).
|
|
42
|
+
*/
|
|
43
|
+
export declare function decorateMemoryToolCall(baseUpdate: any, toolName: string, rawInput: any): any;
|
package/dist/mapping.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate a Letta WireMessage into a partial ACP SessionNotification.
|
|
3
|
+
*
|
|
4
|
+
* Returns null for messages that have no direct user-visible mapping
|
|
5
|
+
* (lifecycle, queue events, sync markers). The caller wraps the returned
|
|
6
|
+
* `update` with the ACP sessionId.
|
|
7
|
+
*
|
|
8
|
+
* Heuristic for ToolKind: inferred from tool name. See `inferToolKind`.
|
|
9
|
+
*/
|
|
10
|
+
export function wireToSessionUpdate(msg, sessionId) {
|
|
11
|
+
switch (msg.type) {
|
|
12
|
+
case "message":
|
|
13
|
+
return mapMessage(msg, sessionId);
|
|
14
|
+
case "stream_event":
|
|
15
|
+
return mapStreamEvent(msg, sessionId);
|
|
16
|
+
case "tool_execution_started":
|
|
17
|
+
return toolUpdate(sessionId, String(msg.tool_call_id ?? "?"), {
|
|
18
|
+
status: "in_progress",
|
|
19
|
+
});
|
|
20
|
+
case "tool_execution_finished": {
|
|
21
|
+
const m = msg;
|
|
22
|
+
return toolUpdate(sessionId, String(m.tool_call_id ?? "?"), {
|
|
23
|
+
status: m.status === "error" ? "failed" : "completed",
|
|
24
|
+
content: m.content
|
|
25
|
+
? [{ type: "content", content: { type: "text", text: String(m.content) } }]
|
|
26
|
+
: undefined,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
case "auto_approval": {
|
|
30
|
+
const m = msg;
|
|
31
|
+
const tc = m.tool_call ?? {};
|
|
32
|
+
const id = String(tc.tool_call_id ?? `call-${Date.now()}`);
|
|
33
|
+
const name = String(tc.name ?? "");
|
|
34
|
+
const rawInput = safeParseJson(tc.arguments);
|
|
35
|
+
const base = {
|
|
36
|
+
sessionUpdate: "tool_call_update",
|
|
37
|
+
toolCallId: id,
|
|
38
|
+
status: "in_progress",
|
|
39
|
+
rawInput,
|
|
40
|
+
};
|
|
41
|
+
// Re-run memory decoration with full args (the original tool_call
|
|
42
|
+
// emission only had the tool name; args streamed in later deltas).
|
|
43
|
+
const enriched = decorateFileEditToolCall(decorateMemoryToolCall(base, name, rawInput), name, rawInput);
|
|
44
|
+
return { sessionId, update: enriched };
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function mapMessage(msg, sessionId) {
|
|
51
|
+
const m = msg;
|
|
52
|
+
switch (m.message_type) {
|
|
53
|
+
case "assistant_message":
|
|
54
|
+
return {
|
|
55
|
+
sessionId,
|
|
56
|
+
update: {
|
|
57
|
+
sessionUpdate: "agent_message_chunk",
|
|
58
|
+
content: { type: "text", text: String(m.content ?? "") },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
case "reasoning_message":
|
|
62
|
+
return {
|
|
63
|
+
sessionId,
|
|
64
|
+
update: {
|
|
65
|
+
sessionUpdate: "agent_thought_chunk",
|
|
66
|
+
content: { type: "text", text: String(m.reasoning ?? m.content ?? "") },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
case "tool_call_message": {
|
|
70
|
+
const tc = m.tool_call ?? {};
|
|
71
|
+
const name = String(tc.name ?? "tool");
|
|
72
|
+
return {
|
|
73
|
+
sessionId,
|
|
74
|
+
update: {
|
|
75
|
+
sessionUpdate: "tool_call",
|
|
76
|
+
toolCallId: String(tc.tool_call_id ?? `call-${Date.now()}`),
|
|
77
|
+
title: name,
|
|
78
|
+
kind: inferToolKind(name),
|
|
79
|
+
status: "pending",
|
|
80
|
+
rawInput: safeParseJson(tc.arguments),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
case "tool_return_message":
|
|
85
|
+
return toolUpdate(sessionId, String(m.tool_call_id ?? "?"), {
|
|
86
|
+
status: m.status === "error" ? "failed" : "completed",
|
|
87
|
+
content: m.tool_return
|
|
88
|
+
? [{ type: "content", content: { type: "text", text: String(m.tool_return) } }]
|
|
89
|
+
: undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function mapStreamEvent(msg, sessionId) {
|
|
95
|
+
const ev = msg.event ?? {};
|
|
96
|
+
const mt = ev.message_type;
|
|
97
|
+
// Letta emits assistant text under `event.content`. Reasoning lives under
|
|
98
|
+
// `event.reasoning`. Delta variants share the same field names.
|
|
99
|
+
const assistantText = typeof ev.content === "string"
|
|
100
|
+
? ev.content
|
|
101
|
+
: typeof ev.text === "string"
|
|
102
|
+
? ev.text
|
|
103
|
+
: null;
|
|
104
|
+
const reasoningText = typeof ev.reasoning === "string"
|
|
105
|
+
? ev.reasoning
|
|
106
|
+
: typeof ev.content === "string"
|
|
107
|
+
? ev.content
|
|
108
|
+
: typeof ev.text === "string"
|
|
109
|
+
? ev.text
|
|
110
|
+
: null;
|
|
111
|
+
if (assistantText !== null && (mt === "assistant_message" || mt === "assistant_message_delta")) {
|
|
112
|
+
return {
|
|
113
|
+
sessionId,
|
|
114
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: assistantText } },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (reasoningText !== null && (mt === "reasoning_message" || mt === "reasoning_message_delta")) {
|
|
118
|
+
return {
|
|
119
|
+
sessionId,
|
|
120
|
+
update: { sessionUpdate: "agent_thought_chunk", content: { type: "text", text: reasoningText } },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (mt === "approval_request_message" && ev.tool_call?.name) {
|
|
124
|
+
const tc = ev.tool_call;
|
|
125
|
+
const name = String(tc.name);
|
|
126
|
+
const rawInput = safeParseJson(tc.arguments);
|
|
127
|
+
const base = {
|
|
128
|
+
sessionUpdate: "tool_call",
|
|
129
|
+
toolCallId: String(tc.tool_call_id ?? `call-${Date.now()}`),
|
|
130
|
+
title: name,
|
|
131
|
+
kind: inferToolKind(name),
|
|
132
|
+
status: "pending",
|
|
133
|
+
rawInput,
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
sessionId,
|
|
137
|
+
update: decorateFileEditToolCall(decorateMemoryToolCall(base, name, rawInput), name, rawInput),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (mt === "tool_call_message" && ev.tool_call) {
|
|
141
|
+
const tc = ev.tool_call;
|
|
142
|
+
const name = String(tc.name ?? "tool");
|
|
143
|
+
const rawInput = safeParseJson(tc.arguments);
|
|
144
|
+
const base = {
|
|
145
|
+
sessionUpdate: "tool_call",
|
|
146
|
+
toolCallId: String(tc.tool_call_id ?? tc.id ?? `call-${Date.now()}`),
|
|
147
|
+
title: name,
|
|
148
|
+
kind: inferToolKind(name),
|
|
149
|
+
status: "pending",
|
|
150
|
+
rawInput,
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
sessionId,
|
|
154
|
+
update: decorateFileEditToolCall(decorateMemoryToolCall(base, name, rawInput), name, rawInput),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (mt === "tool_return_message") {
|
|
158
|
+
const text = String(ev.tool_return ?? ev.stdout ?? "");
|
|
159
|
+
// Heuristic: some tools (web_search) return status="success" with an
|
|
160
|
+
// error-shaped JSON body. Detect common patterns and flip to `failed` so
|
|
161
|
+
// the user sees a clear error indicator instead of a green "completed"
|
|
162
|
+
// hiding a 400/auth/quota issue.
|
|
163
|
+
const looksLikeError = ev.status !== "error" &&
|
|
164
|
+
(/^\s*\{\s*"[^"]*"\s*:\s*"[^"]*"\s*,\s*"error"\s*:/.test(text) ||
|
|
165
|
+
/\b(Request failed|status code [45]\d\d|Validation error)\b/i.test(text));
|
|
166
|
+
return toolUpdate(sessionId, String(ev.tool_call_id ?? "?"), {
|
|
167
|
+
status: ev.status === "error" || looksLikeError ? "failed" : "completed",
|
|
168
|
+
content: text
|
|
169
|
+
? [{ type: "content", content: { type: "text", text } }]
|
|
170
|
+
: undefined,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// stop_reason / usage_statistics: lifecycle, no ACP update needed.
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
function toolUpdate(sessionId, toolCallId, fields) {
|
|
177
|
+
return {
|
|
178
|
+
sessionId,
|
|
179
|
+
update: {
|
|
180
|
+
sessionUpdate: "tool_call_update",
|
|
181
|
+
toolCallId,
|
|
182
|
+
...fields,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function safeParseJson(s) {
|
|
187
|
+
if (typeof s !== "string")
|
|
188
|
+
return s ?? null;
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(s);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return s;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Build a human-readable title from tool name + complete args.
|
|
198
|
+
* Mirrors claude-agent-acp's toolInfoFromToolUse: include file path,
|
|
199
|
+
* pattern, or command in the title so the host UI doesn't show every
|
|
200
|
+
* row as a bare 'Read' / 'Bash' / 'Grep'.
|
|
201
|
+
*/
|
|
202
|
+
export function descriptiveTitle(toolName, rawInput) {
|
|
203
|
+
if (!rawInput || typeof rawInput !== "object")
|
|
204
|
+
return null;
|
|
205
|
+
const a = rawInput;
|
|
206
|
+
const fp = typeof a.file_path === "string" ? a.file_path : null;
|
|
207
|
+
switch (toolName) {
|
|
208
|
+
case "Read":
|
|
209
|
+
case "ReadLSP":
|
|
210
|
+
case "ViewImage":
|
|
211
|
+
return fp ? `Read ${relativizePath(fp)}` : null;
|
|
212
|
+
case "Write":
|
|
213
|
+
return fp ? `Write ${relativizePath(fp)}` : null;
|
|
214
|
+
case "Edit":
|
|
215
|
+
case "MultiEdit":
|
|
216
|
+
case "ApplyPatch":
|
|
217
|
+
return fp ? `Edit ${relativizePath(fp)}` : null;
|
|
218
|
+
case "Glob":
|
|
219
|
+
case "GlobGemini": {
|
|
220
|
+
const pattern = typeof a.pattern === "string" ? a.pattern : null;
|
|
221
|
+
const path = typeof a.path === "string" ? a.path : null;
|
|
222
|
+
if (pattern && path)
|
|
223
|
+
return `Glob ${pattern} in ${relativizePath(path)}`;
|
|
224
|
+
if (pattern)
|
|
225
|
+
return `Glob ${pattern}`;
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
case "Grep":
|
|
229
|
+
case "GrepFilesCodex": {
|
|
230
|
+
const pattern = typeof a.pattern === "string" ? a.pattern : null;
|
|
231
|
+
const path = typeof a.path === "string" ? a.path : null;
|
|
232
|
+
if (pattern && path)
|
|
233
|
+
return `Grep "${pattern}" in ${relativizePath(path)}`;
|
|
234
|
+
if (pattern)
|
|
235
|
+
return `Grep "${pattern}"`;
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
case "Bash":
|
|
239
|
+
case "BashOutput":
|
|
240
|
+
case "ShellCommand": {
|
|
241
|
+
const desc = typeof a.description === "string" ? a.description : null;
|
|
242
|
+
const cmd = typeof a.command === "string" ? a.command : null;
|
|
243
|
+
if (desc && cmd)
|
|
244
|
+
return `${desc}`;
|
|
245
|
+
if (cmd)
|
|
246
|
+
return `Bash ${truncate(cmd, 80)}`;
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
case "Task":
|
|
250
|
+
case "Agent": {
|
|
251
|
+
const desc = typeof a.description === "string" ? a.description : null;
|
|
252
|
+
const sub = typeof a.subagent_type === "string" ? a.subagent_type : null;
|
|
253
|
+
if (desc && sub)
|
|
254
|
+
return `Agent[${sub}] ${desc}`;
|
|
255
|
+
if (desc)
|
|
256
|
+
return `Agent ${desc}`;
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
case "TodoWrite": {
|
|
260
|
+
// Letta's TodoWrite mutates an internal task list. The default 'TodoWrite'
|
|
261
|
+
// title looks like generic developer-facing API noise. Surface a more
|
|
262
|
+
// user-visible label and summarize the action.
|
|
263
|
+
const todos = Array.isArray(a.todos) ? a.todos : null;
|
|
264
|
+
if (todos && todos.length > 0) {
|
|
265
|
+
const counts = todos.reduce((acc, t) => {
|
|
266
|
+
const s = String(t?.status ?? "pending");
|
|
267
|
+
acc[s] = (acc[s] ?? 0) + 1;
|
|
268
|
+
return acc;
|
|
269
|
+
}, {});
|
|
270
|
+
const parts = Object.entries(counts)
|
|
271
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
272
|
+
.join(", ");
|
|
273
|
+
return `Plan · ${todos.length} todo${todos.length > 1 ? "s" : ""} (${parts})`;
|
|
274
|
+
}
|
|
275
|
+
return `Plan · update todos`;
|
|
276
|
+
}
|
|
277
|
+
case "UpdatePlan": {
|
|
278
|
+
return `Plan · update`;
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* For filesystem edit tools (`Edit`, `Write`, `MultiEdit`, `ApplyPatch`):
|
|
286
|
+
* attach an ACP-style `diff` content block so the host UI renders an actual
|
|
287
|
+
* before/after view instead of the bare tool_return JSON
|
|
288
|
+
* (`{"message":"Successfully replaced 1 occurrence..."}`), which carries no
|
|
289
|
+
* useful information to the user once the change has been made.
|
|
290
|
+
*/
|
|
291
|
+
export function decorateFileEditToolCall(baseUpdate, toolName, rawInput) {
|
|
292
|
+
if (toolName !== "Edit" &&
|
|
293
|
+
toolName !== "Write" &&
|
|
294
|
+
toolName !== "MultiEdit" &&
|
|
295
|
+
toolName !== "ApplyPatch") {
|
|
296
|
+
return baseUpdate;
|
|
297
|
+
}
|
|
298
|
+
const args = rawInput && typeof rawInput === "object" ? rawInput : {};
|
|
299
|
+
const fp = typeof args.file_path === "string" ? args.file_path : null;
|
|
300
|
+
if (!fp)
|
|
301
|
+
return baseUpdate;
|
|
302
|
+
let before = null;
|
|
303
|
+
let after = null;
|
|
304
|
+
if (toolName === "Edit") {
|
|
305
|
+
before = typeof args.old_string === "string" ? args.old_string : null;
|
|
306
|
+
after = typeof args.new_string === "string" ? args.new_string : null;
|
|
307
|
+
}
|
|
308
|
+
else if (toolName === "Write") {
|
|
309
|
+
before = "";
|
|
310
|
+
after = typeof args.content === "string" ? args.content : null;
|
|
311
|
+
}
|
|
312
|
+
else if (toolName === "MultiEdit" && Array.isArray(args.edits)) {
|
|
313
|
+
const olds = [];
|
|
314
|
+
const news = [];
|
|
315
|
+
for (const e of args.edits) {
|
|
316
|
+
if (typeof e?.old_string === "string")
|
|
317
|
+
olds.push(e.old_string);
|
|
318
|
+
if (typeof e?.new_string === "string")
|
|
319
|
+
news.push(e.new_string);
|
|
320
|
+
}
|
|
321
|
+
if (olds.length || news.length) {
|
|
322
|
+
before = olds.join("\n\n---\n\n");
|
|
323
|
+
after = news.join("\n\n---\n\n");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (before === null || after === null)
|
|
327
|
+
return baseUpdate;
|
|
328
|
+
const update = { ...baseUpdate };
|
|
329
|
+
const existing = Array.isArray(update.content) ? update.content : [];
|
|
330
|
+
const alreadyHasDiff = existing.some((c) => c?.type === "diff");
|
|
331
|
+
if (alreadyHasDiff)
|
|
332
|
+
return update;
|
|
333
|
+
update.content = [...existing, { type: "diff", path: fp, oldText: before, newText: after }];
|
|
334
|
+
return update;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Strip Letta `Read` tool line-number prefixes (` 42→content`) from a return
|
|
338
|
+
* body so the host renders the file contents cleanly. The prefix is purely a
|
|
339
|
+
* decoration for the model and gets in the way of agent-shell's rendering
|
|
340
|
+
* (visual noise + broken indentation).
|
|
341
|
+
*/
|
|
342
|
+
export function stripReadLineNumbers(text) {
|
|
343
|
+
return text.replace(/^[ \t]*\d+→/gm, "");
|
|
344
|
+
}
|
|
345
|
+
function relativizePath(p) {
|
|
346
|
+
const cwd = process.cwd();
|
|
347
|
+
if (p.startsWith(cwd + "/"))
|
|
348
|
+
return p.slice(cwd.length + 1);
|
|
349
|
+
const home = process.env.HOME ?? "";
|
|
350
|
+
if (home && p.startsWith(home + "/"))
|
|
351
|
+
return "~/" + p.slice(home.length + 1);
|
|
352
|
+
return p;
|
|
353
|
+
}
|
|
354
|
+
function truncate(s, n) {
|
|
355
|
+
return s.length <= n ? s : s.slice(0, n - 1) + "…";
|
|
356
|
+
}
|
|
357
|
+
function inferToolKind(name) {
|
|
358
|
+
const n = name.toLowerCase();
|
|
359
|
+
// Memory tools edit agent state (system prompt blocks, MemFS files), so
|
|
360
|
+
// surface them as `edit` even though the file doesn't live in cwd.
|
|
361
|
+
if (n.startsWith("memory"))
|
|
362
|
+
return "edit";
|
|
363
|
+
if (n.includes("read") || n.includes("view") || n.includes("grep") || n.includes("glob"))
|
|
364
|
+
return "read";
|
|
365
|
+
if (n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("apply"))
|
|
366
|
+
return "edit";
|
|
367
|
+
if (n.includes("shell") || n.includes("bash") || n.includes("exec") || n.includes("run"))
|
|
368
|
+
return "execute";
|
|
369
|
+
if (n.includes("search"))
|
|
370
|
+
return "search";
|
|
371
|
+
if (n.includes("plan") || n.includes("todo"))
|
|
372
|
+
return "think";
|
|
373
|
+
return "other";
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* For `memory`, `memory_apply_patch`, `memory_insert`, `memory_replace`:
|
|
377
|
+
* - prefix title with "memory · " so the host UI flags it visually,
|
|
378
|
+
* - try to build an ACP-style Diff content block from args (old_string/new_string
|
|
379
|
+
* for the unified `memory` tool, or file_text for `create`).
|
|
380
|
+
*
|
|
381
|
+
* Returns the enriched update (still `sessionUpdate: "tool_call"`).
|
|
382
|
+
*/
|
|
383
|
+
export function decorateMemoryToolCall(baseUpdate, toolName, rawInput) {
|
|
384
|
+
// Accept either the raw Letta name ("memory_replace") or the already-
|
|
385
|
+
// decorated title ("memory · replace · path") - the refinement path
|
|
386
|
+
// re-runs decoration after the title has been rewritten.
|
|
387
|
+
const lc = toolName.toLowerCase();
|
|
388
|
+
if (!lc.startsWith("memory"))
|
|
389
|
+
return baseUpdate;
|
|
390
|
+
const update = { ...baseUpdate };
|
|
391
|
+
const args = rawInput && typeof rawInput === "object" ? { ...rawInput } : {};
|
|
392
|
+
const command = args.command;
|
|
393
|
+
// Letta memory tools use `file_path` (unified `memory` tool) or `label`
|
|
394
|
+
// (memory_replace / memory_insert targeting a memory block). Treat both
|
|
395
|
+
// as the destination identifier.
|
|
396
|
+
const path = args.file_path ?? args.label ?? args.old_path ?? args.new_path;
|
|
397
|
+
const action = command ? `${command}` : toolName.replace(/^memory_?/, "") || "update";
|
|
398
|
+
update.title = `memory · ${action}${path ? ` · ${path}` : ""}`;
|
|
399
|
+
update.kind = "edit";
|
|
400
|
+
// Compute before/after text from Letta's arg shape.
|
|
401
|
+
const before = typeof args.old_string === "string"
|
|
402
|
+
? args.old_string
|
|
403
|
+
: command === "create"
|
|
404
|
+
? ""
|
|
405
|
+
: null;
|
|
406
|
+
const after = typeof args.new_string === "string"
|
|
407
|
+
? args.new_string
|
|
408
|
+
: typeof args.file_text === "string"
|
|
409
|
+
? args.file_text
|
|
410
|
+
: typeof args.insert_text === "string"
|
|
411
|
+
? args.insert_text
|
|
412
|
+
: null;
|
|
413
|
+
if (before !== null && after !== null && path) {
|
|
414
|
+
// Path 1: emit a 'diff' content block so ACP hosts that read content[]
|
|
415
|
+
// (Zed) render the change.
|
|
416
|
+
const existing = Array.isArray(update.content) ? update.content : [];
|
|
417
|
+
update.content = [
|
|
418
|
+
...existing,
|
|
419
|
+
{
|
|
420
|
+
type: "diff",
|
|
421
|
+
path: `~/.letta/memfs/${path}`,
|
|
422
|
+
oldText: before,
|
|
423
|
+
newText: after,
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
// Path 2: mirror into rawInput under the ACP-standard keys
|
|
427
|
+
// (old_str/new_str/path). agent-shell extracts diffs from rawInput when
|
|
428
|
+
// content[] doesn't include a diff block - that's the path we hit when a
|
|
429
|
+
// tool_call_update later overwrites content with the tool_return text.
|
|
430
|
+
args.old_str = before;
|
|
431
|
+
args.new_str = after;
|
|
432
|
+
args.path = `~/.letta/memfs/${path}`;
|
|
433
|
+
update.rawInput = args;
|
|
434
|
+
}
|
|
435
|
+
else if (typeof args.reason === "string") {
|
|
436
|
+
const existing = Array.isArray(update.content) ? update.content : [];
|
|
437
|
+
update.content = [
|
|
438
|
+
...existing,
|
|
439
|
+
{ type: "content", content: { type: "text", text: `reason: ${args.reason}` } },
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
return update;
|
|
443
|
+
}
|
|
444
|
+
//# sourceMappingURL=mapping.js.map
|