@span-io/agent-link 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/index.js +18 -3
- package/dist/process-runner.js +55 -4
- package/dist/prompt-compact.js +175 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,12 @@ Configuration is stored in `~/.config/remote-agent/client.json`.
|
|
|
72
72
|
**Environment Variables:**
|
|
73
73
|
* `CODEX_BIN`, `GEMINI_BIN`, `CLAUDE_BIN`: Override the path to specific agent binaries.
|
|
74
74
|
* `CODEX_CWD`: Set the working directory for the agent (defaults to the directory where you ran the client).
|
|
75
|
+
* `AGENT_LINK_PROMPT_CHAR_LIMIT` (or `CODEX_PROMPT_CHAR_LIMIT`): Hard cap for prompt length in characters (default `200000`).
|
|
76
|
+
* `AGENT_LINK_PROMPT_CHAR_THRESHOLD` (or `CODEX_PROMPT_CHAR_THRESHOLD`): Length at which compaction kicks in (default `90%` of limit).
|
|
77
|
+
* `AGENT_LINK_PROMPT_CHAR_TARGET` (or `CODEX_PROMPT_CHAR_TARGET`): Target length after compaction (default `85%` of limit).
|
|
78
|
+
* `AGENT_LINK_PROMPT_COMPACT` (or `CODEX_PROMPT_COMPACT`): `auto`, `summary`, `truncate`, or `off` (default `auto`).
|
|
79
|
+
* `AGENT_LINK_PROMPT_SUMMARY_LINES` (or `CODEX_PROMPT_SUMMARY_LINES`): Max lines to include in heuristic summary (default `20`).
|
|
75
80
|
|
|
76
81
|
## License
|
|
77
82
|
|
|
78
|
-
MIT
|
|
83
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loadConfig, saveConfig } from "./config.js";
|
|
|
5
5
|
import { findAgentsOnPath, resolveAgentBinary } from "./agent.js";
|
|
6
6
|
import { spawnAgentProcess as spawnAgentProcessAdvanced } from "./process-runner.js";
|
|
7
7
|
import { LogBuffer } from "./log-buffer.js";
|
|
8
|
+
import { compactPrompt, resolvePromptCompactionPolicy } from "./prompt-compact.js";
|
|
8
9
|
import { NoopTransport, WebSocketTransport } from "./transport.js";
|
|
9
10
|
const args = parseArgs(process.argv.slice(2));
|
|
10
11
|
if (args.list) {
|
|
@@ -43,6 +44,7 @@ if (args.pairingCode) {
|
|
|
43
44
|
const logBuffer = new LogBuffer();
|
|
44
45
|
// Update map to hold SpawnedProcess which includes the child
|
|
45
46
|
const activeAgents = new Map();
|
|
47
|
+
const promptPolicy = resolvePromptCompactionPolicy();
|
|
46
48
|
let transport;
|
|
47
49
|
try {
|
|
48
50
|
transport = await createTransport({
|
|
@@ -67,7 +69,11 @@ function handleControl(message) {
|
|
|
67
69
|
let agentProc = activeAgents.get(agentId);
|
|
68
70
|
if (agentProc) {
|
|
69
71
|
if (payload?.prompt && agentProc.child.stdin) {
|
|
70
|
-
|
|
72
|
+
const normalized = compactPrompt(payload.prompt, promptPolicy);
|
|
73
|
+
if (normalized.action !== "none") {
|
|
74
|
+
console.warn(`Prompt ${normalized.action} (${normalized.reason ?? "threshold"}): ${normalized.originalLength} -> ${normalized.finalLength} chars.`);
|
|
75
|
+
}
|
|
76
|
+
agentProc.child.stdin.write(`${normalized.prompt}\n`);
|
|
71
77
|
}
|
|
72
78
|
return;
|
|
73
79
|
}
|
|
@@ -110,13 +116,18 @@ function handleControl(message) {
|
|
|
110
116
|
return;
|
|
111
117
|
}
|
|
112
118
|
const optionsArgs = [...(payload?.args || args.agentArgs)];
|
|
119
|
+
const rawPrompt = payload?.prompt || "";
|
|
120
|
+
const normalized = compactPrompt(rawPrompt, promptPolicy);
|
|
121
|
+
if (normalized.action !== "none") {
|
|
122
|
+
console.warn(`Prompt ${normalized.action} (${normalized.reason ?? "threshold"}): ${normalized.originalLength} -> ${normalized.finalLength} chars.`);
|
|
123
|
+
}
|
|
113
124
|
const proc = spawnAgentProcessAdvanced({
|
|
114
125
|
agent: {
|
|
115
126
|
id: agentId,
|
|
116
127
|
name: agentCandidate.name,
|
|
117
128
|
model: targetModel || "codex-cli"
|
|
118
129
|
},
|
|
119
|
-
prompt:
|
|
130
|
+
prompt: normalized.prompt,
|
|
120
131
|
optionsArgs,
|
|
121
132
|
executablePath: agentCandidate.path
|
|
122
133
|
});
|
|
@@ -139,7 +150,11 @@ function handleControl(message) {
|
|
|
139
150
|
const proc = activeAgents.get(agentId);
|
|
140
151
|
const data = message.data || payload?.prompt;
|
|
141
152
|
if (proc && data && proc.child.stdin) {
|
|
142
|
-
|
|
153
|
+
const normalized = compactPrompt(data, promptPolicy);
|
|
154
|
+
if (normalized.action !== "none") {
|
|
155
|
+
console.warn(`Prompt ${normalized.action} (${normalized.reason ?? "threshold"}): ${normalized.originalLength} -> ${normalized.finalLength} chars.`);
|
|
156
|
+
}
|
|
157
|
+
proc.child.stdin.write(normalized.prompt.endsWith("\n") ? normalized.prompt : `${normalized.prompt}\n`);
|
|
143
158
|
}
|
|
144
159
|
break;
|
|
145
160
|
}
|
package/dist/process-runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdirSync } from "fs";
|
|
1
2
|
import { spawn } from "child_process";
|
|
2
3
|
const DEFAULT_CODEX_ARGS = "exec --skip-git-repo-check";
|
|
3
4
|
const DEFAULT_GEMINI_ARGS = "";
|
|
@@ -12,6 +13,50 @@ function shellQuote(value) {
|
|
|
12
13
|
// Simple single-quote wrapping for display purposes
|
|
13
14
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
14
15
|
}
|
|
16
|
+
function collectDirectoriesFromArgs(args) {
|
|
17
|
+
const directories = [];
|
|
18
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
const next = args[i + 1];
|
|
21
|
+
if ((arg === "--cd" || arg === "-C" || arg === "--add-dir" || arg === "--include-directories") && typeof next === "string") {
|
|
22
|
+
directories.push(next);
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg.startsWith("--cd=")) {
|
|
27
|
+
directories.push(arg.slice("--cd=".length));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg.startsWith("--add-dir=")) {
|
|
31
|
+
directories.push(arg.slice("--add-dir=".length));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg.startsWith("--include-directories=")) {
|
|
35
|
+
const raw = arg.slice("--include-directories=".length);
|
|
36
|
+
raw
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((entry) => entry.trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.forEach((entry) => directories.push(entry));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return directories;
|
|
45
|
+
}
|
|
46
|
+
function ensureDirectoriesExist(rawDirs) {
|
|
47
|
+
const deduped = new Set(rawDirs
|
|
48
|
+
.map((entry) => entry.trim())
|
|
49
|
+
.filter((entry) => entry.length > 0));
|
|
50
|
+
for (const dir of deduped) {
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
throw new Error(`Failed to create working directory '${dir}': ${message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
15
60
|
export function buildCommand({ agent, prompt, optionsArgs = [], executablePath }, promptModeOverride) {
|
|
16
61
|
// 1. Gemini Models
|
|
17
62
|
if (agent.model.startsWith("gemini-")) {
|
|
@@ -34,7 +79,13 @@ export function buildCommand({ agent, prompt, optionsArgs = [], executablePath }
|
|
|
34
79
|
// 2. Claude Models
|
|
35
80
|
if (agent.model.startsWith("claude-")) {
|
|
36
81
|
const command = executablePath ?? process.env.CLAUDE_BIN ?? "claude";
|
|
37
|
-
const args = ["-p", prompt
|
|
82
|
+
const args = ["-p", prompt];
|
|
83
|
+
if (optionsArgs.length > 0) {
|
|
84
|
+
args.push(...optionsArgs);
|
|
85
|
+
}
|
|
86
|
+
if (!args.includes("--model") && !args.includes("-m")) {
|
|
87
|
+
args.push("--model", agent.model);
|
|
88
|
+
}
|
|
38
89
|
return { command, args, promptMode: "args" };
|
|
39
90
|
}
|
|
40
91
|
// 3. Generic Codex/Other Models
|
|
@@ -81,11 +132,11 @@ export function spawnAgentProcess(config) {
|
|
|
81
132
|
const { agent, optionsArgs = [] } = config;
|
|
82
133
|
const startedAt = new Date().toISOString();
|
|
83
134
|
const isCodexModel = !agent.model.startsWith("gemini-") && !agent.model.startsWith("claude-");
|
|
84
|
-
const sanitizedOptions =
|
|
85
|
-
? []
|
|
86
|
-
: optionsArgs;
|
|
135
|
+
const sanitizedOptions = optionsArgs;
|
|
87
136
|
const spawnWithMode = (promptModeOverride) => {
|
|
88
137
|
const { command, args, promptMode } = buildCommand({ ...config, optionsArgs: sanitizedOptions }, promptModeOverride);
|
|
138
|
+
// Ensure requested project/working directories exist before launching CLI.
|
|
139
|
+
ensureDirectoriesExist(collectDirectoriesFromArgs(args));
|
|
89
140
|
const ttyMode = process.env.CODEX_TTY_MODE ?? DEFAULT_TTY_MODE;
|
|
90
141
|
const hasExec = args.includes("exec");
|
|
91
142
|
const useScriptWrapper = ttyMode === "script" || (ttyMode === "auto" && hasExec);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const DEFAULT_MAX_CHARS = 200_000;
|
|
2
|
+
const DEFAULT_THRESHOLD_RATIO = 0.9;
|
|
3
|
+
const DEFAULT_TARGET_RATIO = 0.85;
|
|
4
|
+
const DEFAULT_SUMMARY_MAX_LINES = 20;
|
|
5
|
+
function parseEnvInt(value) {
|
|
6
|
+
if (!value)
|
|
7
|
+
return null;
|
|
8
|
+
const parsed = Number.parseInt(value, 10);
|
|
9
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
10
|
+
}
|
|
11
|
+
function parseEnvMode(value) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return null;
|
|
14
|
+
const normalized = value.trim().toLowerCase();
|
|
15
|
+
if (normalized === "auto" ||
|
|
16
|
+
normalized === "summary" ||
|
|
17
|
+
normalized === "truncate" ||
|
|
18
|
+
normalized === "off") {
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function clamp(value, min, max) {
|
|
24
|
+
return Math.max(min, Math.min(max, value));
|
|
25
|
+
}
|
|
26
|
+
function resolveEnvValue(env, keys) {
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const value = env[key];
|
|
29
|
+
if (value !== undefined)
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export function resolvePromptCompactionPolicy(env = process.env) {
|
|
35
|
+
const maxChars = parseEnvInt(resolveEnvValue(env, ["AGENT_LINK_PROMPT_CHAR_LIMIT", "CODEX_PROMPT_CHAR_LIMIT"])) ??
|
|
36
|
+
DEFAULT_MAX_CHARS;
|
|
37
|
+
const thresholdCharsEnv = parseEnvInt(resolveEnvValue(env, ["AGENT_LINK_PROMPT_CHAR_THRESHOLD", "CODEX_PROMPT_CHAR_THRESHOLD"]));
|
|
38
|
+
const targetCharsEnv = parseEnvInt(resolveEnvValue(env, ["AGENT_LINK_PROMPT_CHAR_TARGET", "CODEX_PROMPT_CHAR_TARGET"]));
|
|
39
|
+
const thresholdChars = thresholdCharsEnv ?? Math.floor(maxChars * DEFAULT_THRESHOLD_RATIO);
|
|
40
|
+
const targetChars = targetCharsEnv ?? Math.floor(maxChars * DEFAULT_TARGET_RATIO);
|
|
41
|
+
const mode = parseEnvMode(resolveEnvValue(env, ["AGENT_LINK_PROMPT_COMPACT", "CODEX_PROMPT_COMPACT"])) ??
|
|
42
|
+
"auto";
|
|
43
|
+
const summaryMaxLines = parseEnvInt(resolveEnvValue(env, ["AGENT_LINK_PROMPT_SUMMARY_LINES", "CODEX_PROMPT_SUMMARY_LINES"])) ??
|
|
44
|
+
DEFAULT_SUMMARY_MAX_LINES;
|
|
45
|
+
return {
|
|
46
|
+
maxChars,
|
|
47
|
+
thresholdChars: clamp(thresholdChars, 1, maxChars),
|
|
48
|
+
targetChars: clamp(targetChars, 1, maxChars),
|
|
49
|
+
mode,
|
|
50
|
+
summaryMaxLines,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function truncateByPreservingEdges(prompt, targetChars) {
|
|
54
|
+
if (prompt.length <= targetChars)
|
|
55
|
+
return prompt;
|
|
56
|
+
const marker = "\n\n[...prompt truncated...]\n\n";
|
|
57
|
+
if (targetChars <= marker.length + 1) {
|
|
58
|
+
return prompt.slice(0, targetChars);
|
|
59
|
+
}
|
|
60
|
+
const available = targetChars - marker.length;
|
|
61
|
+
const headLen = Math.floor(available * 0.6);
|
|
62
|
+
const tailLen = available - headLen;
|
|
63
|
+
return `${prompt.slice(0, headLen)}${marker}${prompt.slice(prompt.length - tailLen)}`;
|
|
64
|
+
}
|
|
65
|
+
function extractKeyLines(text, maxLines) {
|
|
66
|
+
const lines = text.split(/\r?\n/);
|
|
67
|
+
const picked = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const rawLine of lines) {
|
|
70
|
+
const line = rawLine.trim();
|
|
71
|
+
if (line.length === 0)
|
|
72
|
+
continue;
|
|
73
|
+
const isHeading = /^(#+\s+|[*-]\s+|[A-Z][\w\s-]{0,40}:)/.test(line);
|
|
74
|
+
const isSignal = /(ERROR|WARN|WARNING|TODO|FIXME|NOTE|IMPORTANT)/i.test(line);
|
|
75
|
+
if (!isHeading && !isSignal)
|
|
76
|
+
continue;
|
|
77
|
+
if (line.length > 160)
|
|
78
|
+
continue;
|
|
79
|
+
if (seen.has(line))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(line);
|
|
82
|
+
picked.push(line);
|
|
83
|
+
if (picked.length >= maxLines)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return picked;
|
|
87
|
+
}
|
|
88
|
+
function buildSummary(removed, maxChars, maxLines) {
|
|
89
|
+
if (maxChars <= 0)
|
|
90
|
+
return "";
|
|
91
|
+
const header = "SUMMARY OF REMOVED CONTENT:\n";
|
|
92
|
+
const lines = extractKeyLines(removed, maxLines);
|
|
93
|
+
const body = lines.length > 0
|
|
94
|
+
? lines.map((line) => `- ${line}`).join("\n")
|
|
95
|
+
: "Summary unavailable; content removed.";
|
|
96
|
+
let summary = `${header}${body}`;
|
|
97
|
+
if (summary.length > maxChars) {
|
|
98
|
+
summary = summary.slice(0, Math.max(0, maxChars - 3)).trimEnd() + "...";
|
|
99
|
+
}
|
|
100
|
+
return summary;
|
|
101
|
+
}
|
|
102
|
+
function compactWithSummary(prompt, targetChars, maxLines) {
|
|
103
|
+
if (prompt.length <= targetChars)
|
|
104
|
+
return prompt;
|
|
105
|
+
const marker = "\n\n[...prompt compacted; middle summarized...]\n\n";
|
|
106
|
+
const spacer = "\n\n";
|
|
107
|
+
let headBudget = Math.floor(targetChars * 0.3);
|
|
108
|
+
let tailBudget = Math.floor(targetChars * 0.3);
|
|
109
|
+
let summaryBudget = targetChars - headBudget - tailBudget - marker.length - spacer.length;
|
|
110
|
+
if (summaryBudget < 50) {
|
|
111
|
+
return truncateByPreservingEdges(prompt, targetChars);
|
|
112
|
+
}
|
|
113
|
+
const head = prompt.slice(0, headBudget);
|
|
114
|
+
const tail = prompt.slice(prompt.length - tailBudget);
|
|
115
|
+
const removed = prompt.slice(headBudget, prompt.length - tailBudget);
|
|
116
|
+
const summary = buildSummary(removed, summaryBudget, maxLines);
|
|
117
|
+
const compacted = `${head}${marker}${summary}${spacer}${tail}`;
|
|
118
|
+
if (compacted.length <= targetChars) {
|
|
119
|
+
return compacted;
|
|
120
|
+
}
|
|
121
|
+
return truncateByPreservingEdges(compacted, targetChars);
|
|
122
|
+
}
|
|
123
|
+
export function compactPrompt(prompt, policy) {
|
|
124
|
+
const originalLength = prompt.length;
|
|
125
|
+
if (originalLength === 0) {
|
|
126
|
+
return { prompt, originalLength, finalLength: 0, action: "none" };
|
|
127
|
+
}
|
|
128
|
+
const maxChars = Math.max(1, policy.maxChars);
|
|
129
|
+
const thresholdChars = clamp(policy.thresholdChars, 1, maxChars);
|
|
130
|
+
const targetChars = clamp(policy.targetChars, 1, maxChars);
|
|
131
|
+
if (originalLength <= thresholdChars) {
|
|
132
|
+
return { prompt, originalLength, finalLength: originalLength, action: "none" };
|
|
133
|
+
}
|
|
134
|
+
if (policy.mode === "off") {
|
|
135
|
+
if (originalLength <= maxChars) {
|
|
136
|
+
return { prompt, originalLength, finalLength: originalLength, action: "none" };
|
|
137
|
+
}
|
|
138
|
+
const truncated = truncateByPreservingEdges(prompt, maxChars);
|
|
139
|
+
return {
|
|
140
|
+
prompt: truncated,
|
|
141
|
+
originalLength,
|
|
142
|
+
finalLength: truncated.length,
|
|
143
|
+
action: "truncate",
|
|
144
|
+
reason: "hard-limit",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (policy.mode === "truncate") {
|
|
148
|
+
const truncated = truncateByPreservingEdges(prompt, targetChars);
|
|
149
|
+
return {
|
|
150
|
+
prompt: truncated,
|
|
151
|
+
originalLength,
|
|
152
|
+
finalLength: truncated.length,
|
|
153
|
+
action: "truncate",
|
|
154
|
+
reason: "policy-truncate",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const summarized = compactWithSummary(prompt, targetChars, policy.summaryMaxLines);
|
|
158
|
+
if (summarized.length <= targetChars) {
|
|
159
|
+
return {
|
|
160
|
+
prompt: summarized,
|
|
161
|
+
originalLength,
|
|
162
|
+
finalLength: summarized.length,
|
|
163
|
+
action: "summary",
|
|
164
|
+
reason: "policy-summary",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const truncated = truncateByPreservingEdges(summarized, targetChars);
|
|
168
|
+
return {
|
|
169
|
+
prompt: truncated,
|
|
170
|
+
originalLength,
|
|
171
|
+
finalLength: truncated.length,
|
|
172
|
+
action: "truncate",
|
|
173
|
+
reason: "summary-overflow",
|
|
174
|
+
};
|
|
175
|
+
}
|