@koalaauto/agentchannel 3.11.0 → 3.13.0
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 +1 -1
- package/dist/src/adapters/claude.d.ts +19 -0
- package/dist/src/adapters/claude.js +52 -1
- package/dist/src/adapters/codex.d.ts +11 -0
- package/dist/src/adapters/codex.js +34 -2
- package/dist/src/adapters/fake.d.ts +10 -0
- package/dist/src/adapters/fake.js +9 -0
- package/dist/src/adapters/paths.d.ts +9 -1
- package/dist/src/adapters/paths.js +28 -3
- package/dist/src/adapters/types.d.ts +3 -2
- package/dist/src/args.d.ts +1 -1
- package/dist/src/args.js +18 -9
- package/dist/src/cli.js +104 -15
- package/dist/src/client.d.ts +34 -4
- package/dist/src/client.js +34 -5
- package/dist/src/desknotify.d.ts +2 -2
- package/dist/src/desknotify.js +2 -2
- package/dist/src/envcheck.d.ts +22 -0
- package/dist/src/envcheck.js +98 -0
- package/dist/src/mcp.d.ts +1 -1
- package/dist/src/mcp.js +24 -12
- package/dist/src/names.d.ts +6 -1
- package/dist/src/names.js +15 -2
- package/dist/src/prompts.d.ts +3 -3
- package/dist/src/prompts.js +4 -4
- package/dist/src/redact.d.ts +5 -0
- package/dist/src/redact.js +41 -0
- package/dist/src/runner/runner.d.ts +16 -1
- package/dist/src/runner/runner.js +163 -22
- package/dist/src/sandboxcheck.d.ts +3 -0
- package/dist/src/sandboxcheck.js +5 -1
- package/dist/src/service.d.ts +20 -0
- package/dist/src/service.js +97 -1
- package/dist/src/servicefix.d.ts +41 -0
- package/dist/src/servicefix.js +69 -0
- package/dist/src/trace.d.ts +51 -0
- package/dist/src/trace.js +137 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/dist/src/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
function xml(s) {
|
|
@@ -140,3 +140,99 @@ export function writeServiceFile(f) {
|
|
|
140
140
|
mkdirSync(dir, { recursive: true });
|
|
141
141
|
writeFileSync(f.path, f.content, { mode: 0o644 });
|
|
142
142
|
}
|
|
143
|
+
/** Where this platform keeps the files serviceFile() writes. */
|
|
144
|
+
export function serviceDir(platform, env = process.env) {
|
|
145
|
+
if (platform === 'win32')
|
|
146
|
+
return join(env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
147
|
+
if (platform === 'darwin')
|
|
148
|
+
return join(homedir(), 'Library', 'LaunchAgents');
|
|
149
|
+
return join(homedir(), '.config', 'systemd', 'user');
|
|
150
|
+
}
|
|
151
|
+
const unxml = (s) => s.replace(/"/g, '"').replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&');
|
|
152
|
+
/** The inverse of sd(): words of an ExecStart line, quoted or not. */
|
|
153
|
+
function sdWords(line) {
|
|
154
|
+
const out = [];
|
|
155
|
+
const re = /"((?:[^"\\]|\\.)*)"|(\S+)/g;
|
|
156
|
+
let m;
|
|
157
|
+
while ((m = re.exec(line)))
|
|
158
|
+
out.push(m[1] !== undefined ? m[1].replace(/\\(.)/g, '$1').replace(/\$\$/g, '$').replace(/%%/g, '%') : m[2]);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
const formatOf = (command) => {
|
|
162
|
+
if (!command.length)
|
|
163
|
+
return 'unknown';
|
|
164
|
+
if (/(^|[\\/])npx(\.cmd)?$/.test(command[0]))
|
|
165
|
+
return 'npx';
|
|
166
|
+
return command.slice(1).some((x) => /[\\/]_npx[\\/]/.test(x)) ? 'node-cache' : 'node';
|
|
167
|
+
};
|
|
168
|
+
/** Read a service file this module wrote (launchd plist or systemd unit); null for anything else, a Windows script included. */
|
|
169
|
+
export function parseServiceFile(path, platform, text) {
|
|
170
|
+
let words = [];
|
|
171
|
+
const env = {};
|
|
172
|
+
let logFile = '';
|
|
173
|
+
let agentName = '';
|
|
174
|
+
if (platform === 'darwin') {
|
|
175
|
+
const label = /<key>Label<\/key>\s*<string>([^<]*)<\/string>/.exec(text)?.[1];
|
|
176
|
+
if (!label?.startsWith('io.agentchannel.runner.'))
|
|
177
|
+
return null;
|
|
178
|
+
agentName = unxml(label).slice('io.agentchannel.runner.'.length);
|
|
179
|
+
const arr = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text)?.[1] ?? '';
|
|
180
|
+
words = [...arr.matchAll(/<string>([^<]*)<\/string>/g)].map((m) => unxml(m[1]));
|
|
181
|
+
const dict = /<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/.exec(text)?.[1] ?? '';
|
|
182
|
+
for (const m of dict.matchAll(/<key>([^<]*)<\/key>\s*<string>([^<]*)<\/string>/g))
|
|
183
|
+
env[unxml(m[1])] = unxml(m[2]);
|
|
184
|
+
logFile = unxml(/<key>StandardOutPath<\/key>\s*<string>([^<]*)<\/string>/.exec(text)?.[1] ?? '');
|
|
185
|
+
}
|
|
186
|
+
else if (platform === 'linux') {
|
|
187
|
+
const exec = /^ExecStart=(.*)$/m.exec(text)?.[1];
|
|
188
|
+
const base = path.split(/[\\/]/).pop() ?? '';
|
|
189
|
+
if (!exec || !/^agentchannel-runner-.+\.service$/.test(base))
|
|
190
|
+
return null;
|
|
191
|
+
agentName = base.slice('agentchannel-runner-'.length, -'.service'.length);
|
|
192
|
+
words = sdWords(exec);
|
|
193
|
+
for (const m of text.matchAll(/^Environment=(.*)$/gm)) {
|
|
194
|
+
const kv = sdWords(m[1])[0] ?? '';
|
|
195
|
+
const i = kv.indexOf('=');
|
|
196
|
+
if (i > 0)
|
|
197
|
+
env[kv.slice(0, i)] = kv.slice(i + 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
return null;
|
|
202
|
+
const run = words.indexOf('run');
|
|
203
|
+
if (run < 1)
|
|
204
|
+
return null;
|
|
205
|
+
const command = words.slice(0, run);
|
|
206
|
+
return { path, agentName, command, args: words.slice(run + 1), env, logFile, format: formatOf(command) };
|
|
207
|
+
}
|
|
208
|
+
/** Every runner service of this user on this machine (launchd and systemd). */
|
|
209
|
+
export function listServices(platform, dir, env = process.env) {
|
|
210
|
+
if (platform === 'win32')
|
|
211
|
+
return [];
|
|
212
|
+
const d = dir ?? serviceDir(platform, env);
|
|
213
|
+
let names = [];
|
|
214
|
+
try {
|
|
215
|
+
names = readdirSync(d);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const f of names) {
|
|
222
|
+
if (!(platform === 'darwin' ? /^io\.agentchannel\.runner\..+\.plist$/ : /^agentchannel-runner-.+\.service$/).test(f))
|
|
223
|
+
continue;
|
|
224
|
+
try {
|
|
225
|
+
const s = parseServiceFile(join(d, f), platform, readFileSync(join(d, f), 'utf8'));
|
|
226
|
+
if (s)
|
|
227
|
+
out.push(s);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
/* unreadable: not ours to judge */
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
/** The ServiceSpec that would write this service again (so it can be rewritten with another command). */
|
|
236
|
+
export function specOf(s, platform, command = s.command) {
|
|
237
|
+
return { platform, agentName: s.agentName, command, args: s.args, env: s.env, logFile: s.logFile };
|
|
238
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type FoundService, type ServiceSpec } from './service.js';
|
|
2
|
+
/** The config dir this service's runner uses: its own AGENTCHANNEL_HOME, else the default one. It names the agent (one key per home). */
|
|
3
|
+
export declare function homeOfService(s: FoundService, env?: NodeJS.ProcessEnv): string;
|
|
4
|
+
export interface Retired {
|
|
5
|
+
agentName: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Stop and delete the other services of the agent whose home this is (not the one at `ownPath`). `run` false only deletes the files
|
|
10
|
+
* (the installer was told not to load anything). Returns what was retired.
|
|
11
|
+
*/
|
|
12
|
+
export declare function retireSiblings(o: {
|
|
13
|
+
platform: ServiceSpec['platform'];
|
|
14
|
+
dir?: string;
|
|
15
|
+
env: NodeJS.ProcessEnv;
|
|
16
|
+
ownPath: string;
|
|
17
|
+
ownHome: string;
|
|
18
|
+
run: boolean;
|
|
19
|
+
}): Retired[];
|
|
20
|
+
/** Was this process started by its own service (launchd, systemd)? A runner someone started by hand must not be replaced by a restart. */
|
|
21
|
+
export declare function startedByService(agentName: string, platform: string, env: NodeJS.ProcessEnv): boolean;
|
|
22
|
+
export interface Migration {
|
|
23
|
+
path: string;
|
|
24
|
+
/** Shell commands that make the supervisor load the rewritten file. */
|
|
25
|
+
start: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* This agent's own service, when it is in the old form (node on the npx cache): rewritten to start `npx -y <spec>`. Null when there is
|
|
29
|
+
* none, or it is already fine. Writing only; the caller decides when to reload (that ends this very process).
|
|
30
|
+
*/
|
|
31
|
+
export declare function migrateOldService(o: {
|
|
32
|
+
agentName: string;
|
|
33
|
+
platform: ServiceSpec['platform'];
|
|
34
|
+
dir?: string;
|
|
35
|
+
env: NodeJS.ProcessEnv;
|
|
36
|
+
home: string;
|
|
37
|
+
npx: string;
|
|
38
|
+
spec: string;
|
|
39
|
+
}): Migration | null;
|
|
40
|
+
/** Run the reload so that it outlives this process (the reload stops it): a detached shell. */
|
|
41
|
+
export declare function reloadDetached(start: string[], env: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Keeping an agent's runner service right (issue #4): one service per agent, and a service that starts `npx -y <spec>`.
|
|
2
|
+
// - Installing a service retires the others on this machine that run the same agent (same AGENTCHANNEL_HOME): a renamed agent
|
|
3
|
+
// gets a new service label, and the old one went on running beside it.
|
|
4
|
+
// - A service written by an early release starts node on a file inside the npx cache. A restart (including the self-update's)
|
|
5
|
+
// runs that same cached file, so the runner never moves to a newer release. At start the runner rewrites such a service to
|
|
6
|
+
// `npx -y <spec>` and has the supervisor reload it.
|
|
7
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
8
|
+
import { unlinkSync } from 'node:fs';
|
|
9
|
+
import { achHome } from './config.js';
|
|
10
|
+
import { listServices, serviceFile, serviceLabel, specOf, writeServiceFile } from './service.js';
|
|
11
|
+
/** The config dir this service's runner uses: its own AGENTCHANNEL_HOME, else the default one. It names the agent (one key per home). */
|
|
12
|
+
export function homeOfService(s, env = process.env) {
|
|
13
|
+
return s.env.AGENTCHANNEL_HOME || achHome({ ...env, AGENTCHANNEL_HOME: '' });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Stop and delete the other services of the agent whose home this is (not the one at `ownPath`). `run` false only deletes the files
|
|
17
|
+
* (the installer was told not to load anything). Returns what was retired.
|
|
18
|
+
*/
|
|
19
|
+
export function retireSiblings(o) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const s of listServices(o.platform, o.dir, o.env)) {
|
|
22
|
+
if (s.path === o.ownPath || homeOfService(s, o.env) !== o.ownHome)
|
|
23
|
+
continue;
|
|
24
|
+
if (o.run) {
|
|
25
|
+
for (const c of serviceFile(specOf(s, o.platform), o.dir, o.env).stop) {
|
|
26
|
+
try {
|
|
27
|
+
execFileSync('/bin/sh', ['-c', c], { env: o.env, stdio: 'ignore' });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* it was not loaded: the file still goes */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
unlinkSync(s.path);
|
|
36
|
+
out.push({ agentName: s.agentName, path: s.path });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* cannot delete: left for the person */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Was this process started by its own service (launchd, systemd)? A runner someone started by hand must not be replaced by a restart. */
|
|
45
|
+
export function startedByService(agentName, platform, env) {
|
|
46
|
+
if (platform === 'darwin')
|
|
47
|
+
return env.XPC_SERVICE_NAME === serviceLabel(agentName);
|
|
48
|
+
if (platform === 'linux')
|
|
49
|
+
return !!env.INVOCATION_ID;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* This agent's own service, when it is in the old form (node on the npx cache): rewritten to start `npx -y <spec>`. Null when there is
|
|
54
|
+
* none, or it is already fine. Writing only; the caller decides when to reload (that ends this very process).
|
|
55
|
+
*/
|
|
56
|
+
export function migrateOldService(o) {
|
|
57
|
+
const name = o.agentName.replace(/[^A-Za-z0-9-]/g, '-');
|
|
58
|
+
const own = listServices(o.platform, o.dir, o.env).find((s) => s.agentName === name && homeOfService(s, o.env) === o.home);
|
|
59
|
+
if (!own || own.format !== 'node-cache')
|
|
60
|
+
return null;
|
|
61
|
+
const f = serviceFile(specOf(own, o.platform, [o.npx, '-y', o.spec]), o.dir, o.env);
|
|
62
|
+
writeServiceFile(f);
|
|
63
|
+
return { path: f.path, start: f.start };
|
|
64
|
+
}
|
|
65
|
+
/** Run the reload so that it outlives this process (the reload stops it): a detached shell. */
|
|
66
|
+
export function reloadDetached(start, env) {
|
|
67
|
+
const child = spawn('/bin/sh', ['-c', `sleep 2; ${start.join(' && ')}`], { env, detached: true, stdio: 'ignore' });
|
|
68
|
+
child.unref();
|
|
69
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AdapterEvent } from './adapters/types.js';
|
|
2
|
+
export type StepKind = 'thinking' | 'text' | 'tool' | 'mark';
|
|
3
|
+
export interface TraceStep {
|
|
4
|
+
/** Milliseconds since the run started. */
|
|
5
|
+
t: number;
|
|
6
|
+
kind: StepKind;
|
|
7
|
+
/** tool: the tool's name (Bash, Read, shell, ...); mark: what happened (task_completed, ...). */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** tool: the command, the file, or the arguments, in a line or two. */
|
|
10
|
+
input?: string;
|
|
11
|
+
/** tool: how it ended. */
|
|
12
|
+
is_error?: boolean;
|
|
13
|
+
exit?: number;
|
|
14
|
+
/** tool: what it printed (cut); thinking / text: the words (cut). */
|
|
15
|
+
output?: string;
|
|
16
|
+
/** tool: how long it ran. */
|
|
17
|
+
ms?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface Trace {
|
|
20
|
+
steps: TraceStep[];
|
|
21
|
+
/** Steps dropped from the middle of a long run. */
|
|
22
|
+
omitted: number;
|
|
23
|
+
}
|
|
24
|
+
/** Limits: each text field, the number of steps, and the whole trace as JSON. */
|
|
25
|
+
export declare const FIELD_MAX = 2000;
|
|
26
|
+
export declare const STEPS_MAX = 300;
|
|
27
|
+
export declare const TRACE_BYTES_MAX = 150000;
|
|
28
|
+
/** Cut to `max`, keeping the start and the end (where a command's error usually is). */
|
|
29
|
+
export declare function cut(s: string, max?: number): string;
|
|
30
|
+
/** The command or the file a tool call is about, in a line. */
|
|
31
|
+
export declare function summarizeInput(name: string, input: unknown): string;
|
|
32
|
+
export declare class TraceRecorder {
|
|
33
|
+
private exact;
|
|
34
|
+
private now;
|
|
35
|
+
private start;
|
|
36
|
+
private steps;
|
|
37
|
+
private omitted;
|
|
38
|
+
private open;
|
|
39
|
+
private marked;
|
|
40
|
+
private lastText;
|
|
41
|
+
/** `exact`: secrets this process holds (its key, the run token), masked wherever they appear. */
|
|
42
|
+
constructor(exact?: readonly string[], now?: () => number, start?: number);
|
|
43
|
+
private clean;
|
|
44
|
+
private add;
|
|
45
|
+
/** Feed one adapter event. Events it does not know are ignored. */
|
|
46
|
+
event(e: AdapterEvent): void;
|
|
47
|
+
/** Something happened around the run (the task was completed while the run went on). Once per name. */
|
|
48
|
+
mark(name: string): void;
|
|
49
|
+
/** The trace as it is now, within the size limit. */
|
|
50
|
+
finish(): Trace;
|
|
51
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// The execution trace of one run: what the agent thought (the model's own summary), which commands and files it touched, how each
|
|
2
|
+
// ended and what they printed, and how long each took. Built from the adapter's events, masked and cut down before anything
|
|
3
|
+
// leaves this machine, and uploaded to the hub with the result so both sides of a task can audit it.
|
|
4
|
+
import { isSecretFile, redact } from './redact.js';
|
|
5
|
+
/** Limits: each text field, the number of steps, and the whole trace as JSON. */
|
|
6
|
+
export const FIELD_MAX = 2000;
|
|
7
|
+
export const STEPS_MAX = 300;
|
|
8
|
+
export const TRACE_BYTES_MAX = 150_000;
|
|
9
|
+
/** Cut to `max`, keeping the start and the end (where a command's error usually is). */
|
|
10
|
+
export function cut(s, max = FIELD_MAX) {
|
|
11
|
+
if (s.length <= max)
|
|
12
|
+
return s;
|
|
13
|
+
const head = Math.floor(max * 0.7);
|
|
14
|
+
const tail = max - head;
|
|
15
|
+
return `${s.slice(0, head)}\n…[${s.length - max} characters omitted]…\n${s.slice(s.length - tail)}`;
|
|
16
|
+
}
|
|
17
|
+
const asText = (v) => {
|
|
18
|
+
if (typeof v === 'string')
|
|
19
|
+
return v;
|
|
20
|
+
if (v === undefined || v === null)
|
|
21
|
+
return '';
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(v);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return String(v);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** The command or the file a tool call is about, in a line. */
|
|
30
|
+
export function summarizeInput(name, input) {
|
|
31
|
+
const o = input && typeof input === 'object' ? input : {};
|
|
32
|
+
const pick = (...keys) => keys.map((k) => o[k]).find((v) => typeof v === 'string' && v);
|
|
33
|
+
if (typeof input === 'string')
|
|
34
|
+
return input;
|
|
35
|
+
const cmd = pick('command', 'cmd');
|
|
36
|
+
if (cmd)
|
|
37
|
+
return cmd;
|
|
38
|
+
const file = pick('file_path', 'path', 'notebook_path');
|
|
39
|
+
if (file)
|
|
40
|
+
return file + (typeof o.pattern === 'string' ? ` ${o.pattern}` : '');
|
|
41
|
+
const other = pick('pattern', 'query', 'url', 'prompt', 'description');
|
|
42
|
+
if (other)
|
|
43
|
+
return other;
|
|
44
|
+
return asText(input);
|
|
45
|
+
}
|
|
46
|
+
export class TraceRecorder {
|
|
47
|
+
exact;
|
|
48
|
+
now;
|
|
49
|
+
start;
|
|
50
|
+
steps = [];
|
|
51
|
+
omitted = 0;
|
|
52
|
+
open = new Map();
|
|
53
|
+
marked = new Set();
|
|
54
|
+
lastText = '';
|
|
55
|
+
/** `exact`: secrets this process holds (its key, the run token), masked wherever they appear. */
|
|
56
|
+
constructor(exact = [], now = Date.now, start = now()) {
|
|
57
|
+
this.exact = exact;
|
|
58
|
+
this.now = now;
|
|
59
|
+
this.start = start;
|
|
60
|
+
}
|
|
61
|
+
clean(s, max = FIELD_MAX) {
|
|
62
|
+
// cut first (a command can print megabytes), keeping the start and the end; mask what is left; then the exact limit
|
|
63
|
+
return cut(redact(cut(s, max * 2), this.exact), max);
|
|
64
|
+
}
|
|
65
|
+
add(step) {
|
|
66
|
+
this.steps.push(step);
|
|
67
|
+
// a long run keeps its beginning and its end: drop the step after the first 60% of the budget
|
|
68
|
+
if (this.steps.length > STEPS_MAX) {
|
|
69
|
+
this.steps.splice(Math.floor(STEPS_MAX * 0.6), 1);
|
|
70
|
+
this.omitted++;
|
|
71
|
+
}
|
|
72
|
+
return step;
|
|
73
|
+
}
|
|
74
|
+
/** Feed one adapter event. Events it does not know are ignored. */
|
|
75
|
+
event(e) {
|
|
76
|
+
const d = e.data;
|
|
77
|
+
const t = this.now() - this.start;
|
|
78
|
+
if (e.type === 'thinking') {
|
|
79
|
+
const text = asText(d).trim();
|
|
80
|
+
if (text)
|
|
81
|
+
this.add({ t, kind: 'thinking', output: this.clean(text) });
|
|
82
|
+
}
|
|
83
|
+
else if (e.type === 'text') {
|
|
84
|
+
const text = asText(d).trim();
|
|
85
|
+
// the same words twice in a row (a final message repeated) are one step
|
|
86
|
+
if (text && text !== this.lastText)
|
|
87
|
+
this.add({ t, kind: 'text', output: this.clean(text) });
|
|
88
|
+
this.lastText = text;
|
|
89
|
+
}
|
|
90
|
+
else if (e.type === 'tool_start' && d && typeof d === 'object') {
|
|
91
|
+
const id = String(d.id ?? '');
|
|
92
|
+
const name = String(d.name ?? d.type ?? 'tool');
|
|
93
|
+
const input = summarizeInput(name, d.input);
|
|
94
|
+
const secret = /^(read|cat|view)$/i.test(name) && isSecretFile(String(d.input?.file_path ?? d.input?.path ?? ''));
|
|
95
|
+
const step = this.add({ t, kind: 'tool', name, input: this.clean(input) });
|
|
96
|
+
if (id)
|
|
97
|
+
this.open.set(id, { step, at: this.now(), secret });
|
|
98
|
+
}
|
|
99
|
+
else if (e.type === 'tool_end' && d && typeof d === 'object') {
|
|
100
|
+
const id = String(d.id ?? '');
|
|
101
|
+
const o = this.open.get(id);
|
|
102
|
+
const step = o?.step ?? this.add({ t, kind: 'tool', name: String(d.name ?? 'tool') });
|
|
103
|
+
if (o) {
|
|
104
|
+
step.ms = this.now() - o.at;
|
|
105
|
+
this.open.delete(id);
|
|
106
|
+
}
|
|
107
|
+
if (d.is_error !== undefined)
|
|
108
|
+
step.is_error = !!d.is_error;
|
|
109
|
+
if (typeof d.exit === 'number')
|
|
110
|
+
step.exit = d.exit;
|
|
111
|
+
const out = asText(d.output);
|
|
112
|
+
if (out)
|
|
113
|
+
step.output = o?.secret ? '[contents of a secrets file not recorded]' : this.clean(out);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Something happened around the run (the task was completed while the run went on). Once per name. */
|
|
117
|
+
mark(name) {
|
|
118
|
+
if (this.marked.has(name))
|
|
119
|
+
return;
|
|
120
|
+
this.marked.add(name);
|
|
121
|
+
this.add({ t: this.now() - this.start, kind: 'mark', name });
|
|
122
|
+
}
|
|
123
|
+
/** The trace as it is now, within the size limit. */
|
|
124
|
+
finish() {
|
|
125
|
+
let steps = this.steps.map((s) => ({ ...s }));
|
|
126
|
+
let omitted = this.omitted;
|
|
127
|
+
const size = () => JSON.stringify(steps).length;
|
|
128
|
+
// too big: shorten what each step printed, then drop from the middle
|
|
129
|
+
if (size() > TRACE_BYTES_MAX)
|
|
130
|
+
steps = steps.map((s) => (s.output && s.output.length > 600 ? { ...s, output: cut(s.output, 600) } : s));
|
|
131
|
+
while (size() > TRACE_BYTES_MAX && steps.length > 20) {
|
|
132
|
+
steps.splice(Math.floor(steps.length / 2), 1);
|
|
133
|
+
omitted++;
|
|
134
|
+
}
|
|
135
|
+
return { steps, omitted };
|
|
136
|
+
}
|
|
137
|
+
}
|
package/dist/src/version.d.ts
CHANGED
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koalaauto/agentchannel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Hand work between your team's agents (Claude Code, Codex) and follow it in one panel: the ach CLI, MCP server and runner for AgentChannel v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentchannel",
|