@anhlp/alp-runtime 0.0.0-beta.1 → 0.0.0-beta.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/dist/acp-transport.d.ts +16 -0
- package/dist/acp-transport.js +40 -7
- package/dist/antigravity-transport.d.ts +2 -0
- package/dist/antigravity-transport.js +12 -2
- package/dist/claude-transport.d.ts +14 -0
- package/dist/claude-transport.js +21 -9
- package/dist/contracts.d.ts +8 -2
- package/dist/contracts.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/dist/resolve.js +3 -3
- package/dist/runtime.js +54 -24
- package/dist/session-state.d.ts +3 -0
- package/dist/tool-detail.d.ts +81 -0
- package/dist/tool-detail.js +329 -0
- package/dist/usage.d.ts +10 -0
- package/dist/usage.js +26 -1
- package/package.json +3 -3
- package/src/acp-transport.ts +44 -9
- package/src/antigravity-transport.ts +9 -0
- package/src/claude-transport.ts +24 -8
- package/src/contracts.ts +8 -2
- package/src/index.ts +2 -0
- package/src/resolve.ts +3 -3
- package/src/runtime.ts +65 -25
- package/src/session-state.ts +3 -0
- package/src/tool-detail.ts +394 -0
- package/src/usage.ts +37 -0
package/dist/session-state.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { MailEvent } from './mailbox.js';
|
|
|
4
4
|
import type { Worktree, WorktreeChange, Copy } from './workspace.js';
|
|
5
5
|
import type { Verification } from '@anhlp/alp-core/verify';
|
|
6
6
|
import type { Handoff } from './handoff.js';
|
|
7
|
+
import type { SessionUsage } from './events.js';
|
|
7
8
|
export type Session = {
|
|
8
9
|
runtimeKind: RuntimeKind;
|
|
9
10
|
runtime: RuntimeTransport;
|
|
@@ -49,6 +50,8 @@ export type Session = {
|
|
|
49
50
|
progressAt?: number;
|
|
50
51
|
/** A digest of the instructions the session runs with. */
|
|
51
52
|
instructionsSha?: string;
|
|
53
|
+
/** The latest usage its runtime reported, shown in viewers' context meters (ALPD §57). */
|
|
54
|
+
usage?: SessionUsage;
|
|
52
55
|
/** The context advisory was given since the context last emptied (ALPD §57). */
|
|
53
56
|
contextLevel?: number;
|
|
54
57
|
/** An assignment's brief as its requester gave it, repeated after a compaction. */
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ToolCallDetail } from '@anhlp/alp-protocol/events';
|
|
2
|
+
/**
|
|
3
|
+
* Typed tool-call details (ALPD §4.2): what each runtime reports of a tool call, as the
|
|
4
|
+
* provider-neutral shapes of Paseo's ToolCallDetail. Pure functions; a transport attaches the
|
|
5
|
+
* detail to the commandExecution it reports as before, and the runtime emits it.
|
|
6
|
+
*
|
|
7
|
+
* A function returns undefined for a call that stays `shell` as today (Claude's Bash, ACP's
|
|
8
|
+
* execute). A tool ALP cannot type gets `unknown` with its raw input and output.
|
|
9
|
+
*/
|
|
10
|
+
export type { ToolCallDetail };
|
|
11
|
+
/** The bounds of a typed detail (not shell, not unknown), as the protocol states them. */
|
|
12
|
+
export declare const DETAIL_TEXT_CHARS = 65536;
|
|
13
|
+
export declare const DETAIL_FILE_PATHS = 200;
|
|
14
|
+
export declare const DETAIL_WEB_RESULTS = 50;
|
|
15
|
+
export declare const ERROR_CHARS = 2000;
|
|
16
|
+
export declare const TOOL_FAILED = "Tool call failed";
|
|
17
|
+
/** The detail types of protocol v1; any other is a later one. */
|
|
18
|
+
export declare const DETAIL_TYPES: readonly ["shell", "read", "edit", "write", "search", "fetch", "sub_agent", "plan", "plain_text", "unknown"];
|
|
19
|
+
/** Text kept to `max` characters: its head, then how much was left out. */
|
|
20
|
+
export declare function clipText(text: string, max?: number): string;
|
|
21
|
+
/** A typed detail within the protocol's bounds; shell and unknown details pass unchanged. */
|
|
22
|
+
export declare function boundDetail(detail: ToolCallDetail): ToolCallDetail;
|
|
23
|
+
/** The error a failed call carries: the tool's own text, clipped, else a fixed one. */
|
|
24
|
+
export declare function toolError(text: unknown): string;
|
|
25
|
+
/** The text of a tool result: a string, or the text blocks of a content array. */
|
|
26
|
+
export declare function resultText(content: unknown): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Claude's Read output is `cat -n` style (` 12\tcode` or ` 12→code`); this is the raw text
|
|
29
|
+
* and the first line's number, or undefined unless every non-empty line carries the gutter with
|
|
30
|
+
* consecutive numbers. A reminder Claude appends after the content is dropped first.
|
|
31
|
+
*/
|
|
32
|
+
export declare function stripReadGutter(text: string): {
|
|
33
|
+
content: string;
|
|
34
|
+
startLine: number;
|
|
35
|
+
} | undefined;
|
|
36
|
+
/** What Claude's result for a call holds: the tool_result text and the SDK's structured output. */
|
|
37
|
+
export type ClaudeResult = {
|
|
38
|
+
text?: string;
|
|
39
|
+
structured?: unknown;
|
|
40
|
+
error?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The detail of a Claude Code tool call, from its name and input and, once done, its result.
|
|
44
|
+
* Undefined for Bash, which stays a shell command.
|
|
45
|
+
*/
|
|
46
|
+
export declare function claudeToolDetail(name: string, rawInput: unknown, result?: ClaudeResult): ToolCallDetail | undefined;
|
|
47
|
+
/** What ALP holds of an ACP tool call, merged from its tool_call and tool_call_update notifications. */
|
|
48
|
+
export type AcpCall = {
|
|
49
|
+
kind?: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
rawInput?: unknown;
|
|
52
|
+
rawOutput?: unknown;
|
|
53
|
+
locations?: unknown;
|
|
54
|
+
content?: unknown;
|
|
55
|
+
};
|
|
56
|
+
/** The text parts of an ACP tool call's content. */
|
|
57
|
+
export declare function acpText(content: unknown): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The detail of an ACP tool call by its kind (read, edit, delete, move, search, fetch, think,
|
|
60
|
+
* other): its rawInput, rawOutput, locations and content, including diffs. Undefined for execute,
|
|
61
|
+
* which stays a shell command.
|
|
62
|
+
*/
|
|
63
|
+
export declare function acpToolDetail(call: AcpCall): ToolCallDetail | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* The detail of an Antigravity tool step from its tool_info (name, parameters, output, error):
|
|
66
|
+
* run_command is a shell command, view_file a read, write_to_file a write, the replace tools
|
|
67
|
+
* edits; any other is unknown.
|
|
68
|
+
*/
|
|
69
|
+
export declare function antigravityToolDetail(info: unknown): ToolCallDetail;
|
|
70
|
+
/** A Codex native item ALP shows as a tool call. */
|
|
71
|
+
export type CodexToolCall = {
|
|
72
|
+
name: string;
|
|
73
|
+
status: 'running' | 'completed' | 'failed';
|
|
74
|
+
detail: ToolCallDetail;
|
|
75
|
+
error?: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Codex's native fileChange, webSearch, mcpToolCall and collabAgentToolCall items as tool calls,
|
|
79
|
+
* `done` when Codex reported the item completed; undefined for any other item.
|
|
80
|
+
*/
|
|
81
|
+
export declare function codexToolCall(item: any, done: boolean): CodexToolCall | undefined;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// packages/runtime/src/tool-detail.ts
|
|
2
|
+
var DETAIL_TEXT_CHARS = 65536;
|
|
3
|
+
var DETAIL_FILE_PATHS = 200;
|
|
4
|
+
var DETAIL_WEB_RESULTS = 50;
|
|
5
|
+
var ERROR_CHARS = 2e3;
|
|
6
|
+
var TOOL_FAILED = "Tool call failed";
|
|
7
|
+
var DETAIL_TYPES = ["shell", "read", "edit", "write", "search", "fetch", "sub_agent", "plan", "plain_text", "unknown"];
|
|
8
|
+
function clipText(text, max = DETAIL_TEXT_CHARS) {
|
|
9
|
+
if (text.length <= max) return text;
|
|
10
|
+
const marker = /\n… \(\d+ more characters\)$/.exec(text);
|
|
11
|
+
if (marker && marker.index <= max) return text;
|
|
12
|
+
const end = /[\uD800-\uDBFF]/.test(text[max - 1] ?? "") ? max - 1 : max;
|
|
13
|
+
return `${text.slice(0, end)}
|
|
14
|
+
\u2026 (${text.length - end} more characters)`;
|
|
15
|
+
}
|
|
16
|
+
function boundDetail(detail) {
|
|
17
|
+
if (detail.type === "shell" || detail.type === "unknown") return detail;
|
|
18
|
+
const bounded = {};
|
|
19
|
+
let cut = false;
|
|
20
|
+
for (const [key, value] of Object.entries(detail)) {
|
|
21
|
+
if (value === void 0) continue;
|
|
22
|
+
if (typeof value === "string") bounded[key] = key === "type" ? value : clipText(value);
|
|
23
|
+
else if (key === "filePaths" && Array.isArray(value)) {
|
|
24
|
+
bounded[key] = value.slice(0, DETAIL_FILE_PATHS).map((entry) => clipText(String(entry)));
|
|
25
|
+
cut = value.length > DETAIL_FILE_PATHS;
|
|
26
|
+
} else if (key === "webResults" && Array.isArray(value)) {
|
|
27
|
+
bounded[key] = value.slice(0, DETAIL_WEB_RESULTS).map((entry) => ({ title: clipText(entry.title), url: clipText(entry.url) }));
|
|
28
|
+
} else bounded[key] = value;
|
|
29
|
+
}
|
|
30
|
+
if (cut) bounded.truncated = true;
|
|
31
|
+
return bounded;
|
|
32
|
+
}
|
|
33
|
+
function toolError(text) {
|
|
34
|
+
const value = typeof text === "string" ? text.trim().replace(/^<tool_use_error>([\s\S]*)<\/tool_use_error>$/, "$1").trim() : "";
|
|
35
|
+
return value ? clipText(value, ERROR_CHARS) : TOOL_FAILED;
|
|
36
|
+
}
|
|
37
|
+
var str = (value) => typeof value === "string" && value.trim() ? value : void 0;
|
|
38
|
+
var int = (value) => typeof value === "number" && Number.isInteger(value) ? value : void 0;
|
|
39
|
+
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
40
|
+
var pathOf = (input) => str(input?.file_path) ?? str(input?.filePath) ?? str(input?.path) ?? str(input?.notebook_path) ?? str(input?.AbsolutePath) ?? str(input?.TargetFile);
|
|
41
|
+
var defined = (value) => Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
42
|
+
var typed = (detail) => boundDetail(defined(detail));
|
|
43
|
+
function resultText(content) {
|
|
44
|
+
if (typeof content === "string") return content;
|
|
45
|
+
if (Array.isArray(content)) {
|
|
46
|
+
const parts = content.map((part) => typeof part === "string" ? part : typeof part?.text === "string" ? part.text : "").filter(Boolean);
|
|
47
|
+
return parts.length ? parts.join("\n") : void 0;
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
var READ_GUTTER = /^\s*(\d+)(?:\t|→)(.*)$/;
|
|
52
|
+
function stripReadGutter(text) {
|
|
53
|
+
const body = text.replace(/\r\n/g, "\n").replace(/\n*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/g, "");
|
|
54
|
+
const out = [];
|
|
55
|
+
let start;
|
|
56
|
+
let previous;
|
|
57
|
+
for (const line of body.split("\n")) {
|
|
58
|
+
if (!line) {
|
|
59
|
+
out.push(line);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const match = READ_GUTTER.exec(line);
|
|
63
|
+
if (!match) return void 0;
|
|
64
|
+
const number = Number.parseInt(match[1], 10);
|
|
65
|
+
if (previous !== void 0 && number !== previous + 1) return void 0;
|
|
66
|
+
start ??= number;
|
|
67
|
+
previous = number;
|
|
68
|
+
out.push(match[2]);
|
|
69
|
+
}
|
|
70
|
+
return start === void 0 ? void 0 : { content: out.join("\n"), startLine: start };
|
|
71
|
+
}
|
|
72
|
+
function structuredPatch(patch) {
|
|
73
|
+
if (!Array.isArray(patch) || !patch.length) return void 0;
|
|
74
|
+
const hunks = patch.map((hunk) => {
|
|
75
|
+
const lines = Array.isArray(hunk?.lines) ? hunk.lines.map(String) : [];
|
|
76
|
+
return [`@@ -${hunk?.oldStart ?? 0},${hunk?.oldLines ?? 0} +${hunk?.newStart ?? 0},${hunk?.newLines ?? 0} @@`, ...lines].join("\n");
|
|
77
|
+
});
|
|
78
|
+
return `${hunks.join("\n")}
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
function replacementsDiff(edits) {
|
|
82
|
+
const side = (text, mark) => String(text ?? "").split("\n").map((line) => `${mark}${line}`);
|
|
83
|
+
return `${edits.map((edit) => ["@@", ...side(edit.old_string, "-"), ...side(edit.new_string, "+")].join("\n")).join("\n")}
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
var TODO_MARK = { completed: "[x]", in_progress: "[~]", pending: "[ ]" };
|
|
87
|
+
function claudeToolDetail(name, rawInput, result) {
|
|
88
|
+
if (name === "Bash") return void 0;
|
|
89
|
+
const input = record(rawInput) ?? {};
|
|
90
|
+
const ok = result && !result.error;
|
|
91
|
+
const text = ok ? result.text : void 0;
|
|
92
|
+
const out = ok ? record(result.structured) : void 0;
|
|
93
|
+
const unknown = () => ({ type: "unknown", input: rawInput ?? null, output: result?.text ?? null });
|
|
94
|
+
const filePath = pathOf(input);
|
|
95
|
+
switch (name) {
|
|
96
|
+
case "Read": {
|
|
97
|
+
if (!filePath) return unknown();
|
|
98
|
+
const file = out?.type === "text" ? record(out.file) : void 0;
|
|
99
|
+
const stripped = file ? void 0 : text !== void 0 ? stripReadGutter(text) : void 0;
|
|
100
|
+
const content = typeof file?.content === "string" ? file.content : stripped?.content ?? text;
|
|
101
|
+
return typed({ type: "read", filePath, content, offset: int(file?.startLine) ?? stripped?.startLine ?? int(input.offset), limit: int(input.limit) });
|
|
102
|
+
}
|
|
103
|
+
case "Write":
|
|
104
|
+
if (!filePath) return unknown();
|
|
105
|
+
return typed({ type: "write", filePath, content: typeof input.content === "string" ? input.content : void 0 });
|
|
106
|
+
case "Edit":
|
|
107
|
+
if (!filePath) return unknown();
|
|
108
|
+
return typed({ type: "edit", filePath, oldString: typeof input.old_string === "string" ? input.old_string : void 0, newString: typeof input.new_string === "string" ? input.new_string : void 0, unifiedDiff: structuredPatch(out?.structuredPatch) });
|
|
109
|
+
case "MultiEdit": {
|
|
110
|
+
if (!filePath) return unknown();
|
|
111
|
+
const edits = Array.isArray(input.edits) ? input.edits.filter(record) : [];
|
|
112
|
+
if (edits.length === 1) return typed({ type: "edit", filePath, oldString: str(edits[0].old_string) ?? void 0, newString: typeof edits[0].new_string === "string" ? edits[0].new_string : void 0, unifiedDiff: structuredPatch(out?.structuredPatch) });
|
|
113
|
+
return typed({ type: "edit", filePath, unifiedDiff: structuredPatch(out?.structuredPatch) ?? (edits.length ? replacementsDiff(edits) : void 0) });
|
|
114
|
+
}
|
|
115
|
+
case "NotebookEdit":
|
|
116
|
+
if (!filePath) return unknown();
|
|
117
|
+
return typed({ type: "edit", filePath, oldString: typeof out?.old_source === "string" ? out.old_source : void 0, newString: input.edit_mode === "delete" ? void 0 : typeof input.new_source === "string" ? input.new_source : void 0 });
|
|
118
|
+
case "Grep": {
|
|
119
|
+
const query = str(input.pattern);
|
|
120
|
+
if (!query) return unknown();
|
|
121
|
+
const structured = out && Array.isArray(out.filenames);
|
|
122
|
+
return typed({
|
|
123
|
+
type: "search",
|
|
124
|
+
query,
|
|
125
|
+
toolName: "grep",
|
|
126
|
+
content: structured ? typeof out.content === "string" ? out.content : void 0 : text,
|
|
127
|
+
filePaths: structured && out.filenames.length ? out.filenames.map(String) : void 0,
|
|
128
|
+
numFiles: structured ? int(out.numFiles) : void 0,
|
|
129
|
+
numMatches: structured ? int(out.numMatches) : void 0
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
case "Glob": {
|
|
133
|
+
const query = str(input.pattern);
|
|
134
|
+
if (!query) return unknown();
|
|
135
|
+
const structured = out && Array.isArray(out.filenames);
|
|
136
|
+
return typed({
|
|
137
|
+
type: "search",
|
|
138
|
+
query,
|
|
139
|
+
toolName: "glob",
|
|
140
|
+
content: structured ? void 0 : text,
|
|
141
|
+
filePaths: structured && out.filenames.length ? out.filenames.map(String) : void 0,
|
|
142
|
+
numFiles: structured ? int(out.numFiles) : void 0,
|
|
143
|
+
truncated: structured && typeof out.truncated === "boolean" ? out.truncated : void 0
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
case "WebFetch": {
|
|
147
|
+
const url = str(input.url) ?? str(out?.url);
|
|
148
|
+
if (!url) return unknown();
|
|
149
|
+
return typed({ type: "fetch", url, prompt: str(input.prompt), result: typeof out?.result === "string" ? out.result : text, code: int(out?.code) });
|
|
150
|
+
}
|
|
151
|
+
case "WebSearch": {
|
|
152
|
+
const query = str(input.query) ?? str(out?.query);
|
|
153
|
+
if (!query) return unknown();
|
|
154
|
+
const entries = Array.isArray(out?.results) ? out.results : [];
|
|
155
|
+
const webResults = entries.flatMap((entry) => Array.isArray(record(entry)?.content) ? entry.content : []).filter((hit) => typeof hit?.title === "string" && typeof hit?.url === "string").map((hit) => ({ title: hit.title, url: hit.url }));
|
|
156
|
+
const notes = entries.filter((entry) => typeof entry === "string");
|
|
157
|
+
return typed({ type: "search", query, toolName: "web_search", webResults: webResults.length ? webResults : void 0, content: out ? notes.length ? notes.join("\n") : void 0 : text });
|
|
158
|
+
}
|
|
159
|
+
case "Task":
|
|
160
|
+
case "Agent": {
|
|
161
|
+
const report = out && Array.isArray(out.content) ? resultText(out.content) : text;
|
|
162
|
+
return typed({ type: "sub_agent", subAgentType: str(input.subagent_type), description: str(input.description) ?? str(input.prompt), log: report });
|
|
163
|
+
}
|
|
164
|
+
case "ExitPlanMode":
|
|
165
|
+
return str(input.plan) ? typed({ type: "plan", text: input.plan }) : unknown();
|
|
166
|
+
case "Skill":
|
|
167
|
+
return str(input.skill) ? typed({ type: "plain_text", label: input.skill, text }) : unknown();
|
|
168
|
+
case "TodoWrite": {
|
|
169
|
+
const todos = Array.isArray(input.todos) ? input.todos.filter(record) : [];
|
|
170
|
+
if (!todos.length) return unknown();
|
|
171
|
+
return typed({ type: "plain_text", label: "Todos", text: todos.map((todo) => `${TODO_MARK[todo.status] ?? "[ ]"} ${String(todo.content ?? todo.activeForm ?? "")}`).join("\n") });
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
return unknown();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function acpText(content) {
|
|
178
|
+
const parts = (Array.isArray(content) ? content : []).map((part) => part?.type === "content" && part.content?.type === "text" ? part.content.text : "").filter(Boolean);
|
|
179
|
+
return parts.length ? parts.join("\n") : void 0;
|
|
180
|
+
}
|
|
181
|
+
function acpToolDetail(call) {
|
|
182
|
+
const kind = call.kind ?? "other";
|
|
183
|
+
if (kind === "execute") return void 0;
|
|
184
|
+
const input = record(call.rawInput);
|
|
185
|
+
const output = record(call.rawOutput);
|
|
186
|
+
const locations = (Array.isArray(call.locations) ? call.locations : []).map(record).filter((entry) => Boolean(str(entry?.path)));
|
|
187
|
+
const diffs = (Array.isArray(call.content) ? call.content : []).map(record).filter((part) => part?.type === "diff" && Boolean(str(part.path)));
|
|
188
|
+
const text = acpText(call.content) ?? (typeof call.rawOutput === "string" ? call.rawOutput : str(output?.output) ?? str(output?.content));
|
|
189
|
+
const title = str(call.title);
|
|
190
|
+
const filePath = locations[0]?.path ?? pathOf(input);
|
|
191
|
+
const unknown = () => ({ type: "unknown", input: call.rawInput ?? null, output: call.rawOutput ?? text ?? null });
|
|
192
|
+
switch (kind) {
|
|
193
|
+
case "read":
|
|
194
|
+
if (!filePath) return unknown();
|
|
195
|
+
return typed({ type: "read", filePath, content: text, offset: int(input?.offset) ?? int(input?.line) ?? int(locations[0]?.line), limit: int(input?.limit) });
|
|
196
|
+
case "edit":
|
|
197
|
+
case "delete": {
|
|
198
|
+
const diff = diffs[0];
|
|
199
|
+
if (diff) {
|
|
200
|
+
const oldText = typeof diff.oldText === "string" ? diff.oldText : void 0;
|
|
201
|
+
const newText = typeof diff.newText === "string" ? diff.newText : void 0;
|
|
202
|
+
if (oldText === void 0 && newText !== void 0 && kind === "edit") return typed({ type: "write", filePath: diff.path, content: newText });
|
|
203
|
+
return typed({ type: "edit", filePath: diff.path, oldString: oldText, newString: kind === "delete" ? void 0 : newText });
|
|
204
|
+
}
|
|
205
|
+
if (!filePath) return unknown();
|
|
206
|
+
const oldString = str(input?.old_string) ?? str(input?.oldText) ?? str(input?.old_str);
|
|
207
|
+
const newString = str(input?.new_string) ?? str(input?.newText) ?? str(input?.new_str);
|
|
208
|
+
const unifiedDiff = str(input?.diff) ?? str(input?.patch) ?? str(input?.unified_diff);
|
|
209
|
+
if (kind === "edit" && !oldString && !unifiedDiff && typeof input?.content === "string") return typed({ type: "write", filePath, content: input.content });
|
|
210
|
+
return typed({ type: "edit", filePath, oldString, newString, unifiedDiff });
|
|
211
|
+
}
|
|
212
|
+
case "move": {
|
|
213
|
+
const from = filePath ?? str(input?.from) ?? str(input?.source);
|
|
214
|
+
const to = locations[1]?.path ?? str(input?.to) ?? str(input?.destination) ?? str(input?.new_path);
|
|
215
|
+
if (!from) return unknown();
|
|
216
|
+
return typed({ type: "plain_text", label: title ?? "Move", text: to ? `${from} \u2192 ${to}` : from });
|
|
217
|
+
}
|
|
218
|
+
case "search": {
|
|
219
|
+
const query = str(input?.query) ?? str(input?.pattern) ?? str(input?.q) ?? title;
|
|
220
|
+
if (!query) return unknown();
|
|
221
|
+
const paths = locations.map((entry) => entry.path);
|
|
222
|
+
return typed({ type: "search", query, toolName: "search", content: text, filePaths: paths.length ? paths : void 0 });
|
|
223
|
+
}
|
|
224
|
+
case "fetch": {
|
|
225
|
+
const url = str(input?.url);
|
|
226
|
+
if (url) return typed({ type: "fetch", url, prompt: str(input?.prompt), result: text });
|
|
227
|
+
const query = str(input?.query) ?? str(input?.q);
|
|
228
|
+
if (query) return typed({ type: "search", query, toolName: "web_search", content: text });
|
|
229
|
+
return unknown();
|
|
230
|
+
}
|
|
231
|
+
case "think":
|
|
232
|
+
return typed({ type: "plain_text", label: title ?? "Thinking", text: text ?? str(input?.thought) ?? str(input?.text) });
|
|
233
|
+
default:
|
|
234
|
+
return unknown();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function antigravityToolDetail(info) {
|
|
238
|
+
const tool = record(info);
|
|
239
|
+
const parameters = record(tool?.parameters) ?? {};
|
|
240
|
+
const output = typeof tool?.output === "string" && tool.output ? tool.output : void 0;
|
|
241
|
+
const filePath = str(parameters.AbsolutePath) ?? str(parameters.TargetFile);
|
|
242
|
+
switch (tool?.name) {
|
|
243
|
+
case "run_command":
|
|
244
|
+
if (typeof parameters.CommandLine === "string") return { type: "shell", command: parameters.CommandLine, ...str(parameters.Cwd) ? { cwd: parameters.Cwd } : {}, output: output ?? "" };
|
|
245
|
+
break;
|
|
246
|
+
case "view_file":
|
|
247
|
+
if (filePath) return typed({ type: "read", filePath, content: output, offset: int(parameters.StartLine), limit: int(parameters.EndLine) !== void 0 && int(parameters.StartLine) !== void 0 ? parameters.EndLine - parameters.StartLine + 1 : void 0 });
|
|
248
|
+
break;
|
|
249
|
+
case "write_to_file":
|
|
250
|
+
if (filePath) return typed({ type: "write", filePath, content: typeof parameters.CodeContent === "string" ? parameters.CodeContent : void 0 });
|
|
251
|
+
break;
|
|
252
|
+
case "replace_file_content":
|
|
253
|
+
case "multi_replace_file_content":
|
|
254
|
+
if (filePath) return typed({ type: "edit", filePath, oldString: typeof parameters.TargetContent === "string" ? parameters.TargetContent : void 0, newString: typeof parameters.ReplacementContent === "string" ? parameters.ReplacementContent : void 0 });
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
return { type: "unknown", input: tool?.parameters ?? null, output: tool?.error?.message ?? output ?? null };
|
|
258
|
+
}
|
|
259
|
+
function withHeaders(path, diff) {
|
|
260
|
+
return /^(--- |diff --git )/.test(diff) ? diff : `--- a/${path}
|
|
261
|
+
+++ b/${path}
|
|
262
|
+
${diff}`;
|
|
263
|
+
}
|
|
264
|
+
var looksLikeDiff = (text) => /^(@@ |--- |\+\+\+ |diff --git )/.test(text);
|
|
265
|
+
function changeDiff(kind, diff) {
|
|
266
|
+
if (kind !== "add" && kind !== "delete" || !diff || looksLikeDiff(diff)) return diff;
|
|
267
|
+
const lines = diff.replace(/\r\n?/g, "\n").replace(/\n$/, "").split("\n");
|
|
268
|
+
const n = lines.length;
|
|
269
|
+
return kind === "add" ? `@@ -0,0 +1,${n} @@
|
|
270
|
+
${lines.map((line) => `+${line}`).join("\n")}
|
|
271
|
+
` : `@@ -1,${n} +0,0 @@
|
|
272
|
+
${lines.map((line) => `-${line}`).join("\n")}
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
function codexToolCall(item, done) {
|
|
276
|
+
const raw = String(item?.status ?? "");
|
|
277
|
+
const status = raw === "inProgress" || !raw && !done ? "running" : raw === "failed" || raw === "declined" ? "failed" : "completed";
|
|
278
|
+
const failed = (error) => status === "failed" ? { error: toolError(error) } : {};
|
|
279
|
+
if (item?.type === "fileChange") {
|
|
280
|
+
const changes = (Array.isArray(item.changes) ? item.changes : []).filter((change) => str(change?.path));
|
|
281
|
+
if (!changes.length) return { name: "apply_patch", status, detail: { type: "unknown", input: item.changes ?? null, output: null }, ...failed() };
|
|
282
|
+
let detail;
|
|
283
|
+
if (changes.length === 1) {
|
|
284
|
+
const [change] = changes;
|
|
285
|
+
const diff = typeof change.diff === "string" ? change.diff : "";
|
|
286
|
+
const kind = change.kind?.type ?? change.kind;
|
|
287
|
+
detail = kind === "add" && !looksLikeDiff(diff) ? typed({ type: "write", filePath: change.path, content: diff || void 0 }) : typed({ type: "edit", filePath: change.path, unifiedDiff: changeDiff(kind, diff) || void 0 });
|
|
288
|
+
} else {
|
|
289
|
+
const diff = changes.map((change) => {
|
|
290
|
+
const text = typeof change.diff === "string" ? changeDiff(change.kind?.type ?? change.kind, change.diff) : "";
|
|
291
|
+
return text ? withHeaders(change.path, text.endsWith("\n") ? text : `${text}
|
|
292
|
+
`) : `--- a/${change.path}
|
|
293
|
+
+++ b/${change.path}
|
|
294
|
+
`;
|
|
295
|
+
}).join("");
|
|
296
|
+
detail = typed({ type: "edit", filePath: changes[0].path, unifiedDiff: diff });
|
|
297
|
+
}
|
|
298
|
+
return { name: "apply_patch", status, detail, ...failed() };
|
|
299
|
+
}
|
|
300
|
+
if (item?.type === "webSearch") {
|
|
301
|
+
const query = str(item.query) ?? str(item.action?.query) ?? (Array.isArray(item.action?.queries) ? str(item.action.queries[0]) : void 0) ?? "";
|
|
302
|
+
return { name: "web_search", status, detail: typed({ type: "search", query, toolName: "web_search" }), ...failed() };
|
|
303
|
+
}
|
|
304
|
+
if (item?.type === "mcpToolCall") {
|
|
305
|
+
return { name: `mcp__${String(item.server ?? "")}__${String(item.tool ?? "")}`, status, detail: { type: "unknown", input: item.arguments ?? null, output: item.result ?? null }, ...failed(item.error?.message) };
|
|
306
|
+
}
|
|
307
|
+
if (item?.type === "collabAgentToolCall") {
|
|
308
|
+
return { name: "Sub-agent", status, detail: typed({ type: "sub_agent", subAgentType: "Sub-agent", description: str(item.prompt) }), ...failed(item.error?.message) };
|
|
309
|
+
}
|
|
310
|
+
return void 0;
|
|
311
|
+
}
|
|
312
|
+
export {
|
|
313
|
+
DETAIL_FILE_PATHS,
|
|
314
|
+
DETAIL_TEXT_CHARS,
|
|
315
|
+
DETAIL_TYPES,
|
|
316
|
+
DETAIL_WEB_RESULTS,
|
|
317
|
+
ERROR_CHARS,
|
|
318
|
+
TOOL_FAILED,
|
|
319
|
+
acpText,
|
|
320
|
+
acpToolDetail,
|
|
321
|
+
antigravityToolDetail,
|
|
322
|
+
boundDetail,
|
|
323
|
+
claudeToolDetail,
|
|
324
|
+
clipText,
|
|
325
|
+
codexToolCall,
|
|
326
|
+
resultText,
|
|
327
|
+
stripReadGutter,
|
|
328
|
+
toolError
|
|
329
|
+
};
|
package/dist/usage.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SessionUsage } from './events.js';
|
|
1
2
|
/** Account-free usage data shown in ALP's viewers. */
|
|
2
3
|
export type UsageWindow = {
|
|
3
4
|
name: string;
|
|
@@ -13,3 +14,12 @@ export type ProviderUsage = {
|
|
|
13
14
|
windows: UsageWindow[];
|
|
14
15
|
};
|
|
15
16
|
export declare function providerUsage(runtime: ProviderUsage['runtime'], usage: any, checkedAt?: string): ProviderUsage;
|
|
17
|
+
/**
|
|
18
|
+
* A session's usage after one token report (ALPD §57), as Paseo's AgentUsage. Every transport
|
|
19
|
+
* reports as Codex's thread/tokenUsage/updated does: `last` the latest model call, of which
|
|
20
|
+
* `totalTokens` is what the context holds now; `modelContextWindow` the window once known;
|
|
21
|
+
* `totalCostUsd` the session's cost where the runtime counts it. The fields a report carries
|
|
22
|
+
* replace the session's; the others are kept, except that the latest response's input, cached and
|
|
23
|
+
* output counts are replaced together. Undefined when the report changes nothing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sessionUsage(previous: SessionUsage | undefined, report: any): SessionUsage | undefined;
|
package/dist/usage.js
CHANGED
|
@@ -30,6 +30,31 @@ function providerUsage(runtime, usage, checkedAt = (/* @__PURE__ */ new Date()).
|
|
|
30
30
|
windows
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
var tokens = (value, least = 0) => typeof value === "number" && Number.isInteger(value) && value >= least ? value : void 0;
|
|
34
|
+
function sessionUsage(previous, report) {
|
|
35
|
+
if (!report || typeof report !== "object") return void 0;
|
|
36
|
+
const last = report.last && typeof report.last === "object" ? report.last : {};
|
|
37
|
+
const cost = report.totalCostUsd;
|
|
38
|
+
const fields = {
|
|
39
|
+
contextWindowUsedTokens: tokens(last.totalTokens),
|
|
40
|
+
contextWindowMaxTokens: tokens(report.modelContextWindow, 1),
|
|
41
|
+
inputTokens: tokens(last.inputTokens),
|
|
42
|
+
cachedInputTokens: tokens(last.cachedInputTokens ?? report.cachedInputTokens),
|
|
43
|
+
outputTokens: tokens(last.outputTokens),
|
|
44
|
+
totalCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : void 0
|
|
45
|
+
};
|
|
46
|
+
const next = { ...previous };
|
|
47
|
+
if (fields.inputTokens !== void 0 || fields.cachedInputTokens !== void 0 || fields.outputTokens !== void 0) {
|
|
48
|
+
delete next.inputTokens;
|
|
49
|
+
delete next.cachedInputTokens;
|
|
50
|
+
delete next.outputTokens;
|
|
51
|
+
}
|
|
52
|
+
for (const [key, value] of Object.entries(fields)) if (value !== void 0) next[key] = value;
|
|
53
|
+
const keys = Object.keys(next);
|
|
54
|
+
if (keys.length === Object.keys(previous ?? {}).length && keys.every((key) => previous?.[key] === next[key])) return void 0;
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
33
57
|
export {
|
|
34
|
-
providerUsage
|
|
58
|
+
providerUsage,
|
|
59
|
+
sessionUsage
|
|
35
60
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhlp/alp-runtime",
|
|
3
|
-
"version": "0.0.0-beta.
|
|
3
|
+
"version": "0.0.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ALP runtime module",
|
|
6
6
|
"files": [
|
|
@@ -222,8 +222,8 @@
|
|
|
222
222
|
"node": ">=22.12"
|
|
223
223
|
},
|
|
224
224
|
"dependencies": {
|
|
225
|
-
"@anhlp/alp-core": "0.0.0-beta.
|
|
226
|
-
"@anhlp/alp-protocol": "0.0.0-beta.
|
|
225
|
+
"@anhlp/alp-core": "0.0.0-beta.2",
|
|
226
|
+
"@anhlp/alp-protocol": "0.0.0-beta.2",
|
|
227
227
|
"@anthropic-ai/claude-agent-sdk": "^0.3.292",
|
|
228
228
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
229
229
|
"@agentclientprotocol/sdk": "1.7.0",
|
package/src/acp-transport.ts
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
9
9
|
import { commandDecision, unwrapShell, type PermissionProfile } from '@anhlp/alp-core/permissions';
|
|
10
10
|
import { launchCommand } from '@anhlp/alp-core/providers';
|
|
11
|
+
import { acpText, acpToolDetail, type AcpCall } from './tool-detail.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* ACP agents (ALPD §46): a generic transport for any agent that speaks the Agent Client
|
|
@@ -146,6 +147,13 @@ function acpServers(servers: Record<string, any>, http: boolean) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
/** Maps an ACP agent over stdio to the normalized runtime protocol. */
|
|
150
|
+
/** One turn-input part as ACP's: text, or a data-URL image as an ACP image block; anything else is dropped. */
|
|
151
|
+
export function acpContent(part: any) {
|
|
152
|
+
if (typeof part?.text === 'string') return { type: 'text', text: part.text };
|
|
153
|
+
const image = part?.type === 'image' && typeof part.url === 'string' ? /^data:(image\/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(part.url) : null;
|
|
154
|
+
return image ? { type: 'image', mimeType: image[1], data: image[2] } : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
149
157
|
export class AcpTransport {
|
|
150
158
|
private child?: ChildProcessWithoutNullStreams;
|
|
151
159
|
private exit: Promise<void> = Promise.resolve();
|
|
@@ -156,6 +164,8 @@ export class AcpTransport {
|
|
|
156
164
|
private requestHandler?: RequestHandler;
|
|
157
165
|
private closed = false;
|
|
158
166
|
private capabilities: any = {};
|
|
167
|
+
/** ACP agents say in initialize whether a prompt may hold images (promptCapabilities.image). */
|
|
168
|
+
takesImages() { return this.capabilities?.promptCapabilities?.image === true; }
|
|
159
169
|
private sessionId = '';
|
|
160
170
|
private config: any;
|
|
161
171
|
/** Instructions go with the first prompt of a new session; a loaded one has them already. */
|
|
@@ -167,7 +177,7 @@ export class AcpTransport {
|
|
|
167
177
|
/** Text of the running turn's current message; a tool call starts the next one. */
|
|
168
178
|
private message?: { id: string; text: string };
|
|
169
179
|
private messages = 0;
|
|
170
|
-
private toolCalls = new Map<string, { command: string; alp: boolean }>();
|
|
180
|
+
private toolCalls = new Map<string, { command: string; alp: boolean; state: AcpCall; done?: boolean }>();
|
|
171
181
|
/** Permission questions of the running turn, answered cancelled when it is cancelled. */
|
|
172
182
|
private questions = new Set<(value: unknown) => void>();
|
|
173
183
|
private bridge?: { server: net.Server; socket: string; token: string; connections: Set<net.Socket> };
|
|
@@ -230,13 +240,14 @@ export class AcpTransport {
|
|
|
230
240
|
const turnId = randomUUID();
|
|
231
241
|
this.activeTurn = turnId;
|
|
232
242
|
this.message = undefined;
|
|
233
|
-
const prompt = (params.input ?? []).
|
|
243
|
+
const prompt = (params.input ?? []).map(acpContent).filter(Boolean) as Array<Record<string, unknown>>;
|
|
244
|
+
if (prompt.some(part => part.type === 'image') && !this.takesImages()) throw new Error(`${this.provider.label} takes no images`);
|
|
234
245
|
if (!this.instructionsSent && this.config?.developerInstructions) {
|
|
235
246
|
prompt.unshift({ type: 'text', text: `Instructions from ALP, which runs you. Follow them for this whole session.\n\n${this.config.developerInstructions}` });
|
|
236
247
|
this.instructionsSent = true;
|
|
237
248
|
}
|
|
238
249
|
this.prompting = this.call('session/prompt', { sessionId: this.sessionId, prompt }).then(
|
|
239
|
-
result => this.endTurn(turnId, result?.stopReason),
|
|
250
|
+
result => { this.turnUsage(turnId, result?.usage); this.endTurn(turnId, result?.stopReason); },
|
|
240
251
|
error => this.endTurn(turnId, 'error', error instanceof Error ? error.message : String(error)),
|
|
241
252
|
).finally(() => { if (this.activeTurn === undefined || this.activeTurn === turnId) this.prompting = undefined; });
|
|
242
253
|
return { turn: { id: turnId } };
|
|
@@ -272,6 +283,13 @@ export class AcpTransport {
|
|
|
272
283
|
await this.call('session/set_model', { sessionId: this.sessionId, modelId: wanted }, 30_000);
|
|
273
284
|
}
|
|
274
285
|
|
|
286
|
+
/** The tokens of the turn the prompt response says (ACP's unstable usage), before its end. */
|
|
287
|
+
private turnUsage(turnId: string, usage: any) {
|
|
288
|
+
if (this.closed || this.activeTurn !== turnId || !usage || typeof usage !== 'object') return;
|
|
289
|
+
const last = { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, cachedInputTokens: usage.cachedReadTokens };
|
|
290
|
+
this.emit('thread/tokenUsage/updated', { threadId: this.sessionId, turnId, tokenUsage: { last } });
|
|
291
|
+
}
|
|
292
|
+
|
|
275
293
|
private endTurn(turnId: string, stopReason: string, error?: string) {
|
|
276
294
|
if (this.closed || this.activeTurn !== turnId) return;
|
|
277
295
|
this.closeMessage();
|
|
@@ -304,25 +322,42 @@ export class AcpTransport {
|
|
|
304
322
|
const id = String(update.toolCallId ?? '');
|
|
305
323
|
if (!id) return;
|
|
306
324
|
let call = this.toolCalls.get(id);
|
|
325
|
+
const first = !call;
|
|
326
|
+
// A call reported done stays done: a late update must not reopen it or replace its detail.
|
|
327
|
+
if (call?.done) return;
|
|
307
328
|
if (!call) {
|
|
308
329
|
const request = acpPermission(update, this.tools.map(tool => tool.name));
|
|
309
|
-
call = { command: request.command ?? `${request.kind}: ${request.title}`, alp: request.alpTool };
|
|
330
|
+
call = { command: request.command ?? `${request.kind}: ${request.title}`, alp: request.alpTool, state: {} };
|
|
310
331
|
this.toolCalls.set(id, call);
|
|
311
332
|
this.closeMessage();
|
|
312
|
-
// ALP's own tools show as ALP tool calls; the agent's call to the bridge would repeat them.
|
|
313
|
-
if (!call.alp) this.emit('item/started', { threadId: this.sessionId, item: { type: 'commandExecution', id, command: call.command, cwd: this.cwd, status: 'inProgress', aggregatedOutput: '' } });
|
|
314
333
|
}
|
|
315
|
-
|
|
334
|
+
// An update carries only what changed; the call's detail is built from all of it.
|
|
335
|
+
for (const field of ['kind', 'title', 'rawInput', 'rawOutput', 'locations', 'content'] as const) if (update[field] !== undefined && update[field] !== null) call.state[field] = update[field];
|
|
336
|
+
if (call.alp) return;
|
|
337
|
+
// The command stays what skill use, copy escapes and the journal read; the typed detail rides along.
|
|
338
|
+
const detail = acpToolDetail(call.state);
|
|
339
|
+
const typed = detail ? { tool: call.state.title || call.state.kind || 'tool', detail } : {};
|
|
340
|
+
const done = update.status === 'completed' || update.status === 'failed';
|
|
341
|
+
// ALP's own tools show as ALP tool calls; the agent's call to the bridge would repeat them.
|
|
342
|
+
if (first) this.emit('item/started', { threadId: this.sessionId, item: { type: 'commandExecution', id, command: call.command, cwd: this.cwd, status: 'inProgress', aggregatedOutput: '', ...typed } });
|
|
343
|
+
else if (!done && detail) this.emit('item/updated', { threadId: this.sessionId, item: { type: 'commandExecution', id, command: call.command, cwd: this.cwd, status: 'inProgress', aggregatedOutput: '', ...typed } });
|
|
344
|
+
if (done) {
|
|
345
|
+
// What the call held is in its detail now; a long turn need not keep every result.
|
|
346
|
+
call.state = {};
|
|
347
|
+
call.done = true;
|
|
316
348
|
const output = (Array.isArray(update.content) ? update.content : [])
|
|
317
349
|
.map((part: any) => part?.type === 'content' && part.content?.type === 'text' ? part.content.text : part?.type === 'diff' ? `diff ${part.path}` : '')
|
|
318
350
|
.filter(Boolean).join('\n');
|
|
319
|
-
|
|
351
|
+
const error = update.status === 'failed' && detail ? { error: acpText(update.content) ?? (typeof update.rawOutput === 'string' ? update.rawOutput : undefined) } : {};
|
|
352
|
+
this.emit('item/completed', { threadId: this.sessionId, item: { type: 'commandExecution', id, command: call.command, cwd: this.cwd, status: update.status, exitCode: update.status === 'failed' ? 1 : 0, aggregatedOutput: output, ...typed, ...error } });
|
|
320
353
|
}
|
|
321
354
|
return;
|
|
322
355
|
}
|
|
323
356
|
if (kind === 'usage_update' && typeof update.used === 'number') {
|
|
324
357
|
const last = { totalTokens: update.used };
|
|
325
|
-
|
|
358
|
+
// ACP's cost is the session's, in its currency; only US dollars are shown.
|
|
359
|
+
const cost = update.cost?.currency === 'USD' && typeof update.cost.amount === 'number' ? { totalCostUsd: update.cost.amount } : {};
|
|
360
|
+
this.emit('thread/tokenUsage/updated', { threadId: this.sessionId, turnId: this.activeTurn, tokenUsage: { last, total: last, ...(typeof update.size === 'number' ? { modelContextWindow: update.size } : {}), ...cost } });
|
|
326
361
|
}
|
|
327
362
|
}
|
|
328
363
|
|