agent.libx.js 0.94.22 → 0.94.24
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 +3 -3
- package/dist/{Agent-DmsB5hzp.d.ts → Agent-BA-rueWn.d.ts} +7 -2
- package/dist/cli.d.ts +7 -3
- package/dist/cli.js +820 -269
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +36 -6
- package/dist/index.js +96 -29
- package/dist/index.js.map +1 -1
- package/dist/{mcp-D00OuccC.d.ts → mcp-CnzmQ8JE.d.ts} +1 -1
- package/dist/mcp.client.d.ts +2 -2
- package/dist/{tools-9AUK6SG2.d.ts → tools-DtpN8Agv.d.ts} +2 -0
- package/dist/tools.shell.d.ts +1 -1
- package/dist/tools.shell.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { a as AgentOptions, H as Hooks, h as RunResult, A as Agent } from './Agent-
|
|
2
|
-
export { C as ChatFragment, D as DEFAULT_MUTATING, b as Decision, P as PermissionOptions, c as PermissionPolicy, d as PermissionRule, e as PreToolUseDecision, R as ReasoningEffort, f as RecordingHooks, g as RecordingLifecycle, T as ToolUse, i as ToolUseMeta, j as composeHooks, p as planMode, r as reasoningToChatFragment } from './Agent-
|
|
1
|
+
import { a as AgentOptions, H as Hooks, h as RunResult, A as Agent } from './Agent-BA-rueWn.js';
|
|
2
|
+
export { C as ChatFragment, D as DEFAULT_MUTATING, b as Decision, P as PermissionOptions, c as PermissionPolicy, d as PermissionRule, e as PreToolUseDecision, R as ReasoningEffort, f as RecordingHooks, g as RecordingLifecycle, T as ToolUse, i as ToolUseMeta, j as composeHooks, p as planMode, r as reasoningToChatFragment } from './Agent-BA-rueWn.js';
|
|
3
3
|
import { IFilesystem, FileMetadata } from '@livx.cc/wcli/core';
|
|
4
4
|
export { CommandExecutor, FileMetadata, IFilesystem, IndexedDbFilesystem, MemFilesystem, registerHeadlessCommands } from '@livx.cc/wcli/core';
|
|
5
5
|
import { BodDB } from '@bod.ee/db';
|
|
6
|
-
import { A as AgentTool, C as ChatLike, a as ChatOptions, b as ChatResponse, h as ToolCall, H as HostBridge, U as UserQuestion, e as MessageContent } from './tools-
|
|
7
|
-
export { c as ContentPart, d as HostEvent, M as Message, R as Role, S as SandboxJobRegistry, f as StreamChunk, T as TodoItem, g as Tool, i as ToolContext, j as bashTool, k as contentText, l as defaultTools, m as editTool, n as exitSessionTool, o as imagePart, p as makeContext, q as makeJobTools, r as readTool, t as toWireTools, s as todoWriteTool, u as toolRegistry, v as toolsByName } from './tools-
|
|
8
|
-
export { M as McpCall, a as McpImage, b as McpRoute, c as McpRouteResolver, d as McpToolResult, e as McpToolSearchOptions, f as McpToolSpec, g as MountedMcpLike, h as buildMcpCatalog, m as makeLazyMcpToolSearch, i as makeMcpToolSearch, j as makeMcpToolSearchFromMounted, k as mcpToolToAgentTool, l as mcpToolsToAgentTools } from './mcp-
|
|
6
|
+
import { A as AgentTool, C as ChatLike, a as ChatOptions, b as ChatResponse, h as ToolCall, H as HostBridge, U as UserQuestion, e as MessageContent } from './tools-DtpN8Agv.js';
|
|
7
|
+
export { c as ContentPart, d as HostEvent, M as Message, R as Role, S as SandboxJobRegistry, f as StreamChunk, T as TodoItem, g as Tool, i as ToolContext, j as bashTool, k as contentText, l as defaultTools, m as editTool, n as exitSessionTool, o as imagePart, p as makeContext, q as makeJobTools, r as readTool, t as toWireTools, s as todoWriteTool, u as toolRegistry, v as toolsByName } from './tools-DtpN8Agv.js';
|
|
8
|
+
export { M as McpCall, a as McpImage, b as McpRoute, c as McpRouteResolver, d as McpToolResult, e as McpToolSearchOptions, f as McpToolSpec, g as MountedMcpLike, h as buildMcpCatalog, m as makeLazyMcpToolSearch, i as makeMcpToolSearch, j as makeMcpToolSearchFromMounted, k as mcpToolToAgentTool, l as mcpToolsToAgentTools } from './mcp-CnzmQ8JE.js';
|
|
9
9
|
import * as libx_js_src_modules_log from 'libx.js/src/modules/log';
|
|
10
10
|
export { log } from 'libx.js/src/modules/log';
|
|
11
11
|
|
|
@@ -25,6 +25,8 @@ declare class NodeDiskFilesystem implements IFilesystem {
|
|
|
25
25
|
/** Ensure the root dir exists. */
|
|
26
26
|
init(): Promise<void>;
|
|
27
27
|
private real;
|
|
28
|
+
private verified;
|
|
29
|
+
private static VERIFY_TTL_MS;
|
|
28
30
|
/** Throw if any existing component of `real` is a symlink (escape vector). */
|
|
29
31
|
private assertNoSymlink;
|
|
30
32
|
resolvePath(path: string, cwd?: string): string;
|
|
@@ -384,7 +386,32 @@ declare class Scheduler {
|
|
|
384
386
|
destroy(): void;
|
|
385
387
|
}
|
|
386
388
|
|
|
387
|
-
|
|
389
|
+
/** Narrow seam to an OS-level scheduler backend (jobs that survive quit/reboot). Node hosts wire
|
|
390
|
+
* the CLI's `OsScheduler` (launchd/cron/at); absent => everything stays in-process. Edge-safe:
|
|
391
|
+
* this module only sees the interface. */
|
|
392
|
+
interface OsBackend {
|
|
393
|
+
sessionId: string;
|
|
394
|
+
cwd: string;
|
|
395
|
+
/** 'os' when the trigger should outlive the session (per hint/heuristic). */
|
|
396
|
+
route(trigger: Trigger, backendHint?: string): 'session' | 'os';
|
|
397
|
+
/** Register with the OS. Returns a mechanism description (e.g. 'launchd:…'). Throws on failure. */
|
|
398
|
+
schedule(spec: {
|
|
399
|
+
id: string;
|
|
400
|
+
prompt: string;
|
|
401
|
+
sessionId: string;
|
|
402
|
+
cwd: string;
|
|
403
|
+
trigger: Trigger;
|
|
404
|
+
label?: string;
|
|
405
|
+
}): string;
|
|
406
|
+
cancel(id: string): boolean;
|
|
407
|
+
list(): Array<{
|
|
408
|
+
id: string;
|
|
409
|
+
label?: string;
|
|
410
|
+
mechanism: string;
|
|
411
|
+
trigger: Trigger;
|
|
412
|
+
}>;
|
|
413
|
+
}
|
|
414
|
+
declare function makeScheduleTools(scheduler: Scheduler, os?: OsBackend): AgentTool[];
|
|
388
415
|
|
|
389
416
|
/** Sandbox mode: an in-memory VFS — the real disk is never read or written. Edge/browser/test/dry-run. */
|
|
390
417
|
declare function sandboxAgentOptions(opts?: Partial<AgentOptions>): Partial<AgentOptions>;
|
|
@@ -829,6 +856,9 @@ declare class DuplexAgentOptions {
|
|
|
829
856
|
askRelay: boolean;
|
|
830
857
|
/** Parked questions auto-resolve empty after this long (callers map '' to deny/best-judgment). */
|
|
831
858
|
askTimeoutMs: number;
|
|
859
|
+
/** Max retained task records: oldest SETTLED tasks (and their activity tails) are evicted past this,
|
|
860
|
+
* bounding memory over a long-lived session. Running tasks are never evicted. */
|
|
861
|
+
maxTaskRecords: number;
|
|
832
862
|
/** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
|
|
833
863
|
* (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
|
|
834
864
|
quickLook?: Record<string, (path?: string) => string | Promise<string>>;
|
package/dist/index.js
CHANGED
|
@@ -253,12 +253,14 @@ var init_tools_structured = __esm({
|
|
|
253
253
|
const ctxN = Math.max(0, Number(context ?? 0));
|
|
254
254
|
const out = [];
|
|
255
255
|
const matched = [];
|
|
256
|
+
let skipped = 0;
|
|
256
257
|
for (const path of files) {
|
|
257
258
|
ckAbort(ctx.signal);
|
|
258
259
|
let content;
|
|
259
260
|
try {
|
|
260
261
|
content = await ctx.fs.readFile(path);
|
|
261
262
|
} catch {
|
|
263
|
+
skipped++;
|
|
262
264
|
continue;
|
|
263
265
|
}
|
|
264
266
|
const lines = content.split("\n");
|
|
@@ -273,8 +275,10 @@ var init_tools_structured = __esm({
|
|
|
273
275
|
}
|
|
274
276
|
if (fileHit) matched.push(path);
|
|
275
277
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
const note = skipped ? `
|
|
279
|
+
[skipped ${skipped} unreadable file${skipped === 1 ? "" : "s"}]` : "";
|
|
280
|
+
if (filesOnly) return (matched.length ? matched.join("\n") : "(no matches)") + note;
|
|
281
|
+
return (out.length ? out.join("\n") : "(no matches)") + note;
|
|
278
282
|
}
|
|
279
283
|
};
|
|
280
284
|
SIG_RE = /^\s*(export\b|(?:export\s+)?(?:async\s+)?function\s+\*?\w|(?:export\s+)?(?:abstract\s+)?class\s+\w|(?:export\s+)?interface\s+\w|(?:export\s+)?type\s+\w|(?:export\s+)?enum\s+\w)/;
|
|
@@ -963,7 +967,7 @@ var init_tools = __esm({
|
|
|
963
967
|
IMG_MIME = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
964
968
|
readTool = {
|
|
965
969
|
name: "Read",
|
|
966
|
-
description: "Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. Always Read a file before Editing it.",
|
|
970
|
+
description: "Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. PDFs return their extracted text. Always Read a file before Editing it.",
|
|
967
971
|
parameters: {
|
|
968
972
|
type: "object",
|
|
969
973
|
required: ["path"],
|
|
@@ -975,6 +979,12 @@ var init_tools = __esm({
|
|
|
975
979
|
},
|
|
976
980
|
async run({ path, offset, limit }, ctx) {
|
|
977
981
|
const ext = String(path).toLowerCase().split(".").pop() ?? "";
|
|
982
|
+
if (ext === "pdf") {
|
|
983
|
+
if (!ctx.pdfText) return `[${path} is a PDF \u2014 text extraction isn't available in this environment (install poppler's pdftotext and run on disk).]`;
|
|
984
|
+
if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
|
|
985
|
+
const text = (await ctx.pdfText(ctx.fs.resolvePath(path))).trim();
|
|
986
|
+
return text ? numberLines(text, Math.max(0, offset ?? 0), limit) : `[${path}: no extractable text (scanned/image-only PDF?)]`;
|
|
987
|
+
}
|
|
978
988
|
if (IMG_MIME[ext]) {
|
|
979
989
|
const fs = ctx.fs;
|
|
980
990
|
if (typeof fs.readFileBytes !== "function") {
|
|
@@ -1052,7 +1062,7 @@ var NodeDiskFilesystem;
|
|
|
1052
1062
|
var init_NodeDiskFilesystem = __esm({
|
|
1053
1063
|
"src/NodeDiskFilesystem.ts"() {
|
|
1054
1064
|
"use strict";
|
|
1055
|
-
NodeDiskFilesystem = class {
|
|
1065
|
+
NodeDiskFilesystem = class _NodeDiskFilesystem {
|
|
1056
1066
|
constructor(baseDir, opts = {}) {
|
|
1057
1067
|
this.baseDir = baseDir;
|
|
1058
1068
|
this.opts = { denySymlinks: true, ...opts };
|
|
@@ -1067,14 +1077,24 @@ var init_NodeDiskFilesystem = __esm({
|
|
|
1067
1077
|
real(vpath) {
|
|
1068
1078
|
return np.join(this.baseDir, "." + vpath);
|
|
1069
1079
|
}
|
|
1080
|
+
// Verified non-symlink DIRECTORY components, with a short TTL: tree walks (Glob/Grep) hit the same
|
|
1081
|
+
// parents thousands of times; re-lstat'ing each per op is the dominant syscall cost. The 1s window
|
|
1082
|
+
// is an accepted race only against an out-of-band symlink swap mid-walk (this FS can't create
|
|
1083
|
+
// symlinks itself); leaf components are never cached.
|
|
1084
|
+
verified = /* @__PURE__ */ new Map();
|
|
1085
|
+
static VERIFY_TTL_MS = 1e3;
|
|
1070
1086
|
/** Throw if any existing component of `real` is a symlink (escape vector). */
|
|
1071
1087
|
async assertNoSymlink(real) {
|
|
1072
1088
|
if (!this.opts.denySymlinks) return;
|
|
1073
1089
|
const rel = np.relative(this.baseDir, real);
|
|
1074
1090
|
if (rel === "" || rel.startsWith("..")) return;
|
|
1091
|
+
const parts = rel.split(np.sep);
|
|
1075
1092
|
let cur = this.baseDir;
|
|
1076
|
-
|
|
1077
|
-
|
|
1093
|
+
const now4 = Date.now();
|
|
1094
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1095
|
+
cur = np.join(cur, parts[i]);
|
|
1096
|
+
const isLeaf = i === parts.length - 1;
|
|
1097
|
+
if (!isLeaf && (this.verified.get(cur) ?? 0) > now4) continue;
|
|
1078
1098
|
let st;
|
|
1079
1099
|
try {
|
|
1080
1100
|
st = await fsp.lstat(cur);
|
|
@@ -1082,6 +1102,10 @@ var init_NodeDiskFilesystem = __esm({
|
|
|
1082
1102
|
return;
|
|
1083
1103
|
}
|
|
1084
1104
|
if (st.isSymbolicLink()) throw new Error("File not found: symlink not permitted");
|
|
1105
|
+
if (!isLeaf) {
|
|
1106
|
+
if (this.verified.size > 1e4) this.verified.clear();
|
|
1107
|
+
this.verified.set(cur, now4 + _NodeDiskFilesystem.VERIFY_TTL_MS);
|
|
1108
|
+
}
|
|
1085
1109
|
}
|
|
1086
1110
|
}
|
|
1087
1111
|
resolvePath(path, cwd) {
|
|
@@ -2835,6 +2859,8 @@ var AgentOptions = class {
|
|
|
2835
2859
|
permissions;
|
|
2836
2860
|
/** Opt-in syntax guardrail: refuse to persist a syntactically-broken code-file write/edit. Default off. */
|
|
2837
2861
|
lintOnWrite;
|
|
2862
|
+
/** Optional PDF text extraction for Read on .pdf files (node hosts wire pdftotext); absent => Read explains. */
|
|
2863
|
+
pdfText;
|
|
2838
2864
|
/** Opt-in: after a write-class tool runs, run `command` over the VFS and append any failure to the tool result.
|
|
2839
2865
|
* `tools` defaults to ['Write','Edit','MultiEdit','ApplyEdits']. */
|
|
2840
2866
|
autoTest;
|
|
@@ -2885,8 +2911,12 @@ var Agent = class _Agent {
|
|
|
2885
2911
|
reprepare() {
|
|
2886
2912
|
this.prepared = false;
|
|
2887
2913
|
}
|
|
2888
|
-
/**
|
|
2914
|
+
/** Tools injected via addTools(); kept separate from options.tools so prepare() rebuilds don't drop them. */
|
|
2915
|
+
injectedTools = [];
|
|
2916
|
+
/** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn
|
|
2917
|
+
* and survives prepare() rebuilds (reprepare(), new conversations). */
|
|
2889
2918
|
addTools(tools) {
|
|
2919
|
+
this.injectedTools.push(...tools);
|
|
2890
2920
|
this.activeTools.push(...tools);
|
|
2891
2921
|
}
|
|
2892
2922
|
/** Remove tools by name from a running agent. Returns the count removed. */
|
|
@@ -2894,6 +2924,7 @@ var Agent = class _Agent {
|
|
|
2894
2924
|
const s = names instanceof Set ? names : new Set(names);
|
|
2895
2925
|
const before = this.activeTools.length;
|
|
2896
2926
|
this.activeTools = this.activeTools.filter((t) => !s.has(t.name));
|
|
2927
|
+
this.injectedTools = this.injectedTools.filter((t) => !s.has(t.name));
|
|
2897
2928
|
return before - this.activeTools.length;
|
|
2898
2929
|
}
|
|
2899
2930
|
constructor(options) {
|
|
@@ -2905,6 +2936,7 @@ var Agent = class _Agent {
|
|
|
2905
2936
|
this.ctx = makeContext(this.options.fs, this.options.host);
|
|
2906
2937
|
this.ctx.signal = this.options.signal;
|
|
2907
2938
|
if (this.options.lintOnWrite) this.ctx.lint = checkSyntax;
|
|
2939
|
+
if (this.options.pdfText) this.ctx.pdfText = this.options.pdfText;
|
|
2908
2940
|
this.ctx.ai = this.options.ai;
|
|
2909
2941
|
this.ctx.model = this.options.model;
|
|
2910
2942
|
this.ctx.parkHuman = (p) => this.park(p);
|
|
@@ -2977,7 +3009,7 @@ var Agent = class _Agent {
|
|
|
2977
3009
|
const plan = o.planMode ? planMode({ host: o.host }) : void 0;
|
|
2978
3010
|
if (plan) tools = [...tools, plan.tool];
|
|
2979
3011
|
this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
|
|
2980
|
-
this.activeTools = tools;
|
|
3012
|
+
this.activeTools = [...tools, ...this.injectedTools];
|
|
2981
3013
|
this.systemPromptCache = systemPrompt;
|
|
2982
3014
|
this.prepared = true;
|
|
2983
3015
|
return systemPrompt;
|
|
@@ -3112,7 +3144,15 @@ var Agent = class _Agent {
|
|
|
3112
3144
|
} catch (err) {
|
|
3113
3145
|
if (err?.code === "budget") return kill("budget");
|
|
3114
3146
|
if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
|
|
3115
|
-
|
|
3147
|
+
const body = err?.body ?? err?.response?.data ?? err?.error;
|
|
3148
|
+
let bodyStr;
|
|
3149
|
+
try {
|
|
3150
|
+
bodyStr = body && typeof body !== "string" ? JSON.stringify(body).slice(0, 2e3) : body;
|
|
3151
|
+
} catch {
|
|
3152
|
+
bodyStr = void 0;
|
|
3153
|
+
}
|
|
3154
|
+
if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
|
|
3155
|
+
log3.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, err);
|
|
3116
3156
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
|
|
3117
3157
|
}
|
|
3118
3158
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -3343,20 +3383,28 @@ function stubOldToolResults(messages, keep) {
|
|
|
3343
3383
|
return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
|
|
3344
3384
|
});
|
|
3345
3385
|
}
|
|
3346
|
-
var
|
|
3386
|
+
var callIdSet = (msgs) => {
|
|
3387
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3388
|
+
for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
|
|
3389
|
+
return ids;
|
|
3390
|
+
};
|
|
3347
3391
|
function dropOrphanToolResults(messages) {
|
|
3348
|
-
const
|
|
3392
|
+
const ids = callIdSet(messages);
|
|
3393
|
+
const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
|
|
3349
3394
|
return messages.every(ok) ? messages : messages.filter(ok);
|
|
3350
3395
|
}
|
|
3351
3396
|
function fitTokenBudget(messages, maxTokens) {
|
|
3352
|
-
|
|
3397
|
+
const per = messages.map((x) => estimateTokens([x]));
|
|
3398
|
+
let total = per.reduce((a, b) => a + b, 0);
|
|
3399
|
+
if (total <= maxTokens) return messages;
|
|
3353
3400
|
const head = messages[0]?.role === "system" ? [messages[0]] : [];
|
|
3354
|
-
let
|
|
3355
|
-
while (
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3401
|
+
let from = head.length;
|
|
3402
|
+
while (from < messages.length && total > maxTokens) total -= per[from++];
|
|
3403
|
+
const ids = callIdSet(messages.slice(from));
|
|
3404
|
+
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
3405
|
+
if (total > maxTokens)
|
|
3406
|
+
log3.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
3407
|
+
return [...head, ...messages.slice(from)];
|
|
3360
3408
|
}
|
|
3361
3409
|
function compact(m, max, focus) {
|
|
3362
3410
|
const hasSystem = m[0]?.role === "system";
|
|
@@ -3899,11 +3947,11 @@ var Scheduler = class {
|
|
|
3899
3947
|
this.jobs.clear();
|
|
3900
3948
|
}
|
|
3901
3949
|
};
|
|
3902
|
-
function makeScheduleTools(scheduler) {
|
|
3950
|
+
function makeScheduleTools(scheduler, os) {
|
|
3903
3951
|
return [
|
|
3904
3952
|
{
|
|
3905
3953
|
name: "ScheduleTask",
|
|
3906
|
-
description: 'Schedule a prompt to fire automatically
|
|
3954
|
+
description: 'Schedule a prompt to fire automatically.\nModes:\n \u2022 One-off: {at: <epoch_ms>} \u2014 fires once at that time, then done.\n \u2022 Interval: {everyMs: <ms>} \u2014 fires repeatedly (\u22651s).\n \u2022 Cron: {cron: "min hr dom mon dow"} \u2014 standard 5-field cron.\nBackend: "session" fires while this CLI session is alive (default for recurring + near one-offs); "os" registers with the OS scheduler so the job survives quitting \u2014 it headless-resumes this session when it fires (auto-chosen for one-offs \u226530min out when available). Pass backend:"os" explicitly for recurring jobs that must outlive the session.',
|
|
3907
3955
|
parameters: {
|
|
3908
3956
|
type: "object",
|
|
3909
3957
|
required: ["prompt", "trigger"],
|
|
@@ -3918,15 +3966,22 @@ function makeScheduleTools(scheduler) {
|
|
|
3918
3966
|
cron: { type: "string" }
|
|
3919
3967
|
}
|
|
3920
3968
|
},
|
|
3921
|
-
label: { type: "string", description: "Short label for display (optional)." }
|
|
3969
|
+
label: { type: "string", description: "Short label for display (optional)." },
|
|
3970
|
+
backend: { type: "string", enum: ["auto", "session", "os"], description: "Where the job lives (default auto)." }
|
|
3922
3971
|
}
|
|
3923
3972
|
},
|
|
3924
|
-
async run({ prompt, trigger, label }) {
|
|
3973
|
+
async run({ prompt, trigger, label, backend }) {
|
|
3925
3974
|
try {
|
|
3975
|
+
if (os && os.route(trigger, backend) === "os") {
|
|
3976
|
+
const id2 = `os-${Date.now().toString(36)}`;
|
|
3977
|
+
const mechanism = os.schedule({ id: id2, prompt, sessionId: os.sessionId, cwd: os.cwd, trigger, label });
|
|
3978
|
+
return `Scheduled ${id2}${label ? ` (${label})` : ""} on the OS scheduler (${mechanism}) \u2014 survives quitting; fires \`agentx --resume ${os.sessionId}\` headless.`;
|
|
3979
|
+
}
|
|
3980
|
+
if (backend === "os") return "Error: no OS scheduler available on this platform \u2014 job not created (use the default in-session backend).";
|
|
3926
3981
|
const id = scheduler.add({ prompt, trigger, label });
|
|
3927
3982
|
const job = scheduler.get(id);
|
|
3928
3983
|
const next = scheduler.nextFire(job);
|
|
3929
|
-
return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}
|
|
3984
|
+
return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}. (In-session: does not survive quitting.)`;
|
|
3930
3985
|
} catch (e) {
|
|
3931
3986
|
return `Error: ${e?.message ?? e}`;
|
|
3932
3987
|
}
|
|
@@ -3934,24 +3989,28 @@ function makeScheduleTools(scheduler) {
|
|
|
3934
3989
|
},
|
|
3935
3990
|
{
|
|
3936
3991
|
name: "ScheduleList",
|
|
3937
|
-
description: "List all scheduled jobs and their next fire time.",
|
|
3992
|
+
description: "List all scheduled jobs (in-session + OS-backed) and their next fire time.",
|
|
3938
3993
|
parameters: { type: "object", properties: {} },
|
|
3939
3994
|
async run() {
|
|
3995
|
+
const osJobs = os?.list() ?? [];
|
|
3996
|
+
const osLines = osJobs.map((j) => `${j.id} os ${j.mechanism}${j.label ? " " + j.label : ""}`);
|
|
3940
3997
|
const jobs = scheduler.list();
|
|
3941
|
-
if (!jobs.length) return "(no scheduled jobs)";
|
|
3942
|
-
return jobs.map((j) => {
|
|
3998
|
+
if (!jobs.length && !osLines.length) return "(no scheduled jobs)";
|
|
3999
|
+
return [...osLines, ...jobs.map((j) => {
|
|
3943
4000
|
const next = scheduler.nextFire(j);
|
|
3944
4001
|
const trig = "at" in j.trigger ? `once @ ${new Date(j.trigger.at).toLocaleString()}` : "everyMs" in j.trigger ? `every ${(j.trigger.everyMs / 1e3).toFixed(0)}s` : `cron: ${j.trigger.cron}`;
|
|
3945
4002
|
return `${j.id} ${j.status} ${trig} runs:${j.runs} next:${next ? new Date(next).toLocaleTimeString() : "\u2014"}${j.label ? " " + j.label : ""}`;
|
|
3946
|
-
}).join("\n");
|
|
4003
|
+
})].join("\n");
|
|
3947
4004
|
}
|
|
3948
4005
|
},
|
|
3949
4006
|
{
|
|
3950
4007
|
name: "ScheduleCancel",
|
|
3951
|
-
description: "Cancel a scheduled job by id.",
|
|
4008
|
+
description: "Cancel a scheduled job by id (in-session or OS-backed).",
|
|
3952
4009
|
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
3953
4010
|
async run({ id }) {
|
|
3954
|
-
|
|
4011
|
+
const key = String(id);
|
|
4012
|
+
if (key.startsWith("os-")) return os?.cancel(key) ? `Cancelled ${key} (OS job removed).` : `Error: no OS job '${key}'.`;
|
|
4013
|
+
return scheduler.cancel(key) ? `Cancelled ${key}.` : `Error: no scheduled job '${key}'. Use ScheduleList to see jobs.`;
|
|
3955
4014
|
}
|
|
3956
4015
|
},
|
|
3957
4016
|
{
|
|
@@ -4262,6 +4321,9 @@ var DuplexAgentOptions = class {
|
|
|
4262
4321
|
askRelay = false;
|
|
4263
4322
|
/** Parked questions auto-resolve empty after this long (callers map '' to deny/best-judgment). */
|
|
4264
4323
|
askTimeoutMs = 12e4;
|
|
4324
|
+
/** Max retained task records: oldest SETTLED tasks (and their activity tails) are evicted past this,
|
|
4325
|
+
* bounding memory over a long-lived session. Running tasks are never evicted. */
|
|
4326
|
+
maxTaskRecords = 50;
|
|
4265
4327
|
/** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
|
|
4266
4328
|
* (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
|
|
4267
4329
|
quickLook;
|
|
@@ -4552,6 +4614,11 @@ ${recent}` : brief) + verify;
|
|
|
4552
4614
|
};
|
|
4553
4615
|
const promise = new Agent(agentOpts).run(briefText).then((res) => this.maybeVerify(id, briefText, res, tier, agentOpts)).then((res) => this.onWorkerSettled(id, res)).catch((err) => this.onWorkerFailed(id, err));
|
|
4554
4616
|
this.tasks.set(id, { id, label, status: "running", controller, promise, tail });
|
|
4617
|
+
if (this.tasks.size > this.options.maxTaskRecords)
|
|
4618
|
+
for (const [tid, rec] of this.tasks) {
|
|
4619
|
+
if (this.tasks.size <= this.options.maxTaskRecords) break;
|
|
4620
|
+
if (rec.status !== "running") this.tasks.delete(tid);
|
|
4621
|
+
}
|
|
4555
4622
|
}
|
|
4556
4623
|
/** Fresh-context check of a successful Act task: a NEW agent (same model/fs/tools, but NO shared
|
|
4557
4624
|
* conversation context) re-reads the file state against the brief and fixes any gap. The fix lands
|