chainlesschain 0.45.81 → 0.47.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 +10 -0
- package/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{Analytics-C1AnPdMx.js → Analytics-DgypYeUB.js} +2 -2
- package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +1 -0
- package/src/assets/web-panel/assets/{Backup-D31iZX3l.js → Backup-Ba9UybpT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DiXJ3TuK.js → Chat-BwXskT21.js} +1 -1
- package/src/assets/web-panel/assets/Cowork-CXuhlHew.css +1 -0
- package/src/assets/web-panel/assets/Cowork-UmOe7qvE.js +7 -0
- package/src/assets/web-panel/assets/{Cron-DBt1ueXh.js → Cron-JHS-rc-4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-HPh9FcPt.js → Dashboard-B95cMCO7.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{Git-hwQ1oZHj.js → Git-CSYO0_zk.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-4D9p6PRM.js → Logs-Hxw_K0km.js} +2 -2
- package/src/assets/web-panel/assets/{McpTools-CyAUjbbs.js → McpTools-DIE75TrB.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-BMqOR7S-.js → Memory-C4KVnLlp.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-Cmas8i4E.js → Notes-DuzrHMAk.js} +2 -2
- package/src/assets/web-panel/assets/{Organization-DnSa58Tl.js → Organization-DTq6uF82.js} +4 -4
- package/src/assets/web-panel/assets/{P2P-BxksIBWs.js → P2P-C0hjlhsR.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-Bq5Qn2s3.js → Permissions-Ec0NH-xC.js} +4 -4
- package/src/assets/web-panel/assets/{Projects-B7EM0uPg.js → Projects-U8D0asCS.js} +2 -2
- package/src/assets/web-panel/assets/{Providers-DAwgG5KV.js → Providers-BngtTLvJ.js} +2 -2
- package/src/assets/web-panel/assets/{RssFeed-HSZoRXvS.js → RssFeed-B9NbwCKM.js} +3 -3
- package/src/assets/web-panel/assets/{Security-Cz17qBny.js → Security-BL5Rkr1T.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D2EsLq-v.js → Services-D4MJzLld.js} +2 -2
- package/src/assets/web-panel/assets/{Skills-C9v-f3vZ.js → Skills-CQTOMDwF.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-yMEcU0n7.js → Tasks-DepbJMnL.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-l7SvlKuB.js → Templates-C24PVZPu.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BHWhLWn9.js → Wallet-PQoSpN_P.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-kWhFYaUK.js → WebAuthn-BcuyQ4Lr.js} +4 -4
- package/src/assets/web-panel/assets/WorkflowEditor-C-SvXbHW.js +1 -0
- package/src/assets/web-panel/assets/WorkflowEditor-D5bX6woe.css +1 -0
- package/src/assets/web-panel/assets/{antd-D6h4fDFf.js → antd-DEjZPGMj.js} +82 -82
- package/src/assets/web-panel/assets/index-CwvzTTw_.js +2 -0
- package/src/assets/web-panel/assets/{markdown-BZsB-Dsv.js → markdown-CusdXFxb.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/cowork.js +867 -0
- package/src/gateways/ws/action-protocol.js +182 -2
- package/src/gateways/ws/message-dispatcher.js +5 -0
- package/src/gateways/ws/ws-server.js +21 -0
- package/src/lib/cowork-cron.js +474 -0
- package/src/lib/cowork-evomap-adapter.js +121 -0
- package/src/lib/cowork-learning.js +438 -0
- package/src/lib/cowork-mcp-tools.js +182 -0
- package/src/lib/cowork-observe-html.js +108 -0
- package/src/lib/cowork-observe.js +160 -0
- package/src/lib/cowork-share.js +322 -0
- package/src/lib/cowork-task-runner.js +317 -3
- package/src/lib/cowork-task-templates.js +101 -13
- package/src/lib/cowork-template-marketplace.js +205 -0
- package/src/lib/cowork-workflow.js +571 -0
- package/src/lib/provider-options.js +133 -0
- package/src/lib/skill-loader.js +65 -0
- package/src/lib/sub-agent-context.js +54 -2
- package/src/lib/sub-agent-profiles.js +164 -0
- package/src/lib/todo-manager.js +108 -0
- package/src/lib/turn-context.js +95 -0
- package/src/lib/web-fetch.js +224 -0
- package/src/lib/workflow-expr.js +318 -0
- package/src/repl/agent-repl.js +4 -0
- package/src/runtime/agent-core.js +135 -3
- package/src/runtime/coding-agent-contract-shared.cjs +131 -0
- package/src/runtime/coding-agent-policy.cjs +30 -0
- package/src/assets/web-panel/assets/AppLayout-YdvJBMHH.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +0 -1
- package/src/assets/web-panel/assets/Cowork-BnrHWwZw.js +0 -7
- package/src/assets/web-panel/assets/Cowork-CcSoS3eX.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
- package/src/assets/web-panel/assets/index-ByUk2Wmr.js +0 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Fetch — agent-safe HTTP(S) GET with allowlist + size/timeout limits.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by open-agents web_fetch tool. Guards against SSRF by rejecting
|
|
5
|
+
* private/loopback hosts unless explicitly allowlisted.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import http from "http";
|
|
9
|
+
import https from "https";
|
|
10
|
+
import { URL } from "url";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MAX_BYTES = 2_000_000;
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
// RFC 1918 + loopback + link-local. Blocked by default unless config.allowPrivateHosts.
|
|
16
|
+
const PRIVATE_HOST_PATTERNS = [
|
|
17
|
+
/^127\./,
|
|
18
|
+
/^10\./,
|
|
19
|
+
/^192\.168\./,
|
|
20
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
21
|
+
/^169\.254\./,
|
|
22
|
+
/^0\.0\.0\.0$/,
|
|
23
|
+
/^::1$/,
|
|
24
|
+
/^localhost$/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function isPrivateHost(host) {
|
|
28
|
+
if (!host) return true;
|
|
29
|
+
return PRIVATE_HOST_PATTERNS.some((re) => re.test(host));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function checkAllowed(urlStr, config = {}) {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = new URL(urlStr);
|
|
36
|
+
} catch {
|
|
37
|
+
return { allowed: false, reason: "invalid URL" };
|
|
38
|
+
}
|
|
39
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
reason: `unsupported protocol: ${parsed.protocol}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (isPrivateHost(parsed.hostname) && !config.allowPrivateHosts) {
|
|
46
|
+
return {
|
|
47
|
+
allowed: false,
|
|
48
|
+
reason: `private/loopback host blocked: ${parsed.hostname}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const allowed = Array.isArray(config.allowedDomains)
|
|
52
|
+
? config.allowedDomains
|
|
53
|
+
: ["*"];
|
|
54
|
+
if (!allowed.includes("*")) {
|
|
55
|
+
const match = allowed.some((pattern) => {
|
|
56
|
+
if (pattern.startsWith("*.")) {
|
|
57
|
+
return parsed.hostname.endsWith(pattern.slice(1));
|
|
58
|
+
}
|
|
59
|
+
return parsed.hostname === pattern;
|
|
60
|
+
});
|
|
61
|
+
if (!match) {
|
|
62
|
+
return {
|
|
63
|
+
allowed: false,
|
|
64
|
+
reason: `host not in allowedDomains: ${parsed.hostname}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { allowed: true, url: parsed };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function htmlToMarkdown(html) {
|
|
72
|
+
if (!html) return "";
|
|
73
|
+
let text = String(html);
|
|
74
|
+
// Strip scripts/styles entirely
|
|
75
|
+
text = text.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
76
|
+
text = text.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
77
|
+
text = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
78
|
+
// Convert headings
|
|
79
|
+
text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, n, inner) => {
|
|
80
|
+
return `\n\n${"#".repeat(Number(n))} ${inner.trim()}\n\n`;
|
|
81
|
+
});
|
|
82
|
+
// Paragraphs / breaks
|
|
83
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
84
|
+
text = text.replace(/<\/p>/gi, "\n\n");
|
|
85
|
+
// Links: [text](href)
|
|
86
|
+
text = text.replace(
|
|
87
|
+
/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,
|
|
88
|
+
(_, href, inner) => `[${stripTags(inner).trim()}](${href})`,
|
|
89
|
+
);
|
|
90
|
+
// List items
|
|
91
|
+
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, inner) => {
|
|
92
|
+
return `- ${stripTags(inner).trim()}\n`;
|
|
93
|
+
});
|
|
94
|
+
// Strip remaining tags
|
|
95
|
+
text = stripTags(text);
|
|
96
|
+
// Decode minimal HTML entities
|
|
97
|
+
text = text
|
|
98
|
+
.replace(/ /g, " ")
|
|
99
|
+
.replace(/&/g, "&")
|
|
100
|
+
.replace(/</g, "<")
|
|
101
|
+
.replace(/>/g, ">")
|
|
102
|
+
.replace(/"/g, '"')
|
|
103
|
+
.replace(/'/g, "'");
|
|
104
|
+
// Collapse whitespace
|
|
105
|
+
text = text.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+/g, " ");
|
|
106
|
+
return text.trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stripTags(html) {
|
|
110
|
+
return String(html).replace(/<[^>]+>/g, "");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function _doRequest(parsed, { maxBytes, timeout, headers }) {
|
|
114
|
+
const lib = parsed.protocol === "https:" ? https : http;
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const req = lib.request(
|
|
117
|
+
{
|
|
118
|
+
method: "GET",
|
|
119
|
+
protocol: parsed.protocol,
|
|
120
|
+
hostname: parsed.hostname,
|
|
121
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
122
|
+
path: parsed.pathname + parsed.search,
|
|
123
|
+
headers: {
|
|
124
|
+
"User-Agent": "ChainlessChain-Agent/1.0",
|
|
125
|
+
Accept: "*/*",
|
|
126
|
+
...headers,
|
|
127
|
+
},
|
|
128
|
+
timeout,
|
|
129
|
+
},
|
|
130
|
+
(res) => {
|
|
131
|
+
if (
|
|
132
|
+
res.statusCode >= 300 &&
|
|
133
|
+
res.statusCode < 400 &&
|
|
134
|
+
res.headers.location
|
|
135
|
+
) {
|
|
136
|
+
res.resume();
|
|
137
|
+
return resolve({ redirect: res.headers.location });
|
|
138
|
+
}
|
|
139
|
+
const chunks = [];
|
|
140
|
+
let size = 0;
|
|
141
|
+
res.on("data", (chunk) => {
|
|
142
|
+
size += chunk.length;
|
|
143
|
+
if (size > maxBytes) {
|
|
144
|
+
req.destroy(new Error(`response exceeds maxBytes (${maxBytes})`));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
chunks.push(chunk);
|
|
148
|
+
});
|
|
149
|
+
res.on("end", () => {
|
|
150
|
+
resolve({
|
|
151
|
+
statusCode: res.statusCode,
|
|
152
|
+
headers: res.headers,
|
|
153
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
res.on("error", reject);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
req.on("error", reject);
|
|
160
|
+
req.on("timeout", () => req.destroy(new Error("request timeout")));
|
|
161
|
+
req.end();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function webFetch(url, options = {}) {
|
|
166
|
+
const {
|
|
167
|
+
format = "markdown",
|
|
168
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
169
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
170
|
+
config = {},
|
|
171
|
+
headers = {},
|
|
172
|
+
maxRedirects = 3,
|
|
173
|
+
} = options;
|
|
174
|
+
|
|
175
|
+
const check = checkAllowed(url, config);
|
|
176
|
+
if (!check.allowed) {
|
|
177
|
+
return { error: `web_fetch blocked: ${check.reason}` };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let parsed = check.url;
|
|
181
|
+
let redirects = 0;
|
|
182
|
+
let response;
|
|
183
|
+
while (true) {
|
|
184
|
+
response = await _doRequest(parsed, { maxBytes, timeout, headers });
|
|
185
|
+
if (!response.redirect) break;
|
|
186
|
+
if (++redirects > maxRedirects) {
|
|
187
|
+
return { error: "too many redirects" };
|
|
188
|
+
}
|
|
189
|
+
const next = new URL(response.redirect, parsed);
|
|
190
|
+
const nextCheck = checkAllowed(next.toString(), config);
|
|
191
|
+
if (!nextCheck.allowed) {
|
|
192
|
+
return { error: `redirect blocked: ${nextCheck.reason}` };
|
|
193
|
+
}
|
|
194
|
+
parsed = nextCheck.url;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { statusCode, headers: respHeaders, body } = response;
|
|
198
|
+
const contentType = String(respHeaders["content-type"] || "");
|
|
199
|
+
|
|
200
|
+
let output = body;
|
|
201
|
+
let outputFormat = format;
|
|
202
|
+
if (format === "markdown") {
|
|
203
|
+
output = /html/i.test(contentType) ? htmlToMarkdown(body) : body;
|
|
204
|
+
} else if (format === "text") {
|
|
205
|
+
output = /html/i.test(contentType) ? stripTags(body) : body;
|
|
206
|
+
} else if (format === "json") {
|
|
207
|
+
try {
|
|
208
|
+
output = JSON.parse(body);
|
|
209
|
+
} catch {
|
|
210
|
+
return { error: "response is not valid JSON", statusCode, body };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
url: parsed.toString(),
|
|
216
|
+
statusCode,
|
|
217
|
+
contentType,
|
|
218
|
+
format: outputFormat,
|
|
219
|
+
bytes: Buffer.byteLength(body, "utf8"),
|
|
220
|
+
content: output,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const _deps = { http, https };
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow expression sandbox — evaluate `when` conditions and resolve
|
|
3
|
+
* typed step-result references for `forEach` expansion.
|
|
4
|
+
*
|
|
5
|
+
* SAFETY: hand-written tokenizer + recursive-descent parser, no
|
|
6
|
+
* `Function` / `eval`. Only the operators below are supported.
|
|
7
|
+
*
|
|
8
|
+
* Grammar (informal):
|
|
9
|
+
*
|
|
10
|
+
* expr := or
|
|
11
|
+
* or := and ( "||" and )*
|
|
12
|
+
* and := not ( "&&" not )*
|
|
13
|
+
* not := "!" not | cmp
|
|
14
|
+
* cmp := primary ( OP primary )? OP ∈ { == != < <= > >= contains }
|
|
15
|
+
* primary := "(" expr ")" | ref | string | number | bool | null
|
|
16
|
+
* ref := "${" "step" "." ID "." ID "}" OR "${item}"
|
|
17
|
+
*
|
|
18
|
+
* String literals use single or double quotes; `\'` / `\"` / `\\` escapes.
|
|
19
|
+
*
|
|
20
|
+
* `evaluate(expr, ctx)` returns a boolean. `ctx` shape:
|
|
21
|
+
* {
|
|
22
|
+
* step: Map<stepId, { status, taskId, result: { summary, tokenCount, ... } }>,
|
|
23
|
+
* item: any, // for forEach expansion
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* `resolveReference(ref, ctx)` returns the raw value of a `${...}` token
|
|
27
|
+
* (used by forEach to materialize array sources).
|
|
28
|
+
*
|
|
29
|
+
* @module workflow-expr
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const TOKEN_OP = new Set([
|
|
33
|
+
"==",
|
|
34
|
+
"!=",
|
|
35
|
+
"<",
|
|
36
|
+
"<=",
|
|
37
|
+
">",
|
|
38
|
+
">=",
|
|
39
|
+
"contains",
|
|
40
|
+
"&&",
|
|
41
|
+
"||",
|
|
42
|
+
"!",
|
|
43
|
+
"(",
|
|
44
|
+
")",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Tokenize an expression string. Returns an array of `{ type, value }`.
|
|
49
|
+
* Types: `op`, `ref`, `string`, `number`, `bool`, `null`.
|
|
50
|
+
*/
|
|
51
|
+
export function tokenize(src) {
|
|
52
|
+
if (typeof src !== "string") throw new Error("expr must be a string");
|
|
53
|
+
const tokens = [];
|
|
54
|
+
let i = 0;
|
|
55
|
+
const n = src.length;
|
|
56
|
+
while (i < n) {
|
|
57
|
+
const c = src[i];
|
|
58
|
+
if (c === " " || c === "\t" || c === "\n") {
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (c === "(" || c === ")") {
|
|
63
|
+
tokens.push({ type: "op", value: c });
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (c === "$" && src[i + 1] === "{") {
|
|
68
|
+
const end = src.indexOf("}", i + 2);
|
|
69
|
+
if (end === -1) throw new Error("unterminated reference");
|
|
70
|
+
tokens.push({ type: "ref", value: src.slice(i + 2, end).trim() });
|
|
71
|
+
i = end + 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === "'" || c === '"') {
|
|
75
|
+
let j = i + 1;
|
|
76
|
+
let out = "";
|
|
77
|
+
while (j < n && src[j] !== c) {
|
|
78
|
+
if (src[j] === "\\" && j + 1 < n) {
|
|
79
|
+
out += src[j + 1];
|
|
80
|
+
j += 2;
|
|
81
|
+
} else {
|
|
82
|
+
out += src[j];
|
|
83
|
+
j++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (j >= n) throw new Error("unterminated string literal");
|
|
87
|
+
tokens.push({ type: "string", value: out });
|
|
88
|
+
i = j + 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (/[0-9-]/.test(c)) {
|
|
92
|
+
let j = i;
|
|
93
|
+
if (src[j] === "-") j++;
|
|
94
|
+
while (j < n && /[0-9.]/.test(src[j])) j++;
|
|
95
|
+
const lit = src.slice(i, j);
|
|
96
|
+
const num = Number(lit);
|
|
97
|
+
if (!Number.isFinite(num)) throw new Error(`bad number: ${lit}`);
|
|
98
|
+
tokens.push({ type: "number", value: num });
|
|
99
|
+
i = j;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Multi-char ops
|
|
103
|
+
const two = src.slice(i, i + 2);
|
|
104
|
+
if (
|
|
105
|
+
two === "==" ||
|
|
106
|
+
two === "!=" ||
|
|
107
|
+
two === "<=" ||
|
|
108
|
+
two === ">=" ||
|
|
109
|
+
two === "&&" ||
|
|
110
|
+
two === "||"
|
|
111
|
+
) {
|
|
112
|
+
tokens.push({ type: "op", value: two });
|
|
113
|
+
i += 2;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (c === "<" || c === ">" || c === "!") {
|
|
117
|
+
tokens.push({ type: "op", value: c });
|
|
118
|
+
i++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Identifiers: contains / true / false / null
|
|
122
|
+
if (/[a-zA-Z_]/.test(c)) {
|
|
123
|
+
let j = i;
|
|
124
|
+
while (j < n && /[a-zA-Z0-9_]/.test(src[j])) j++;
|
|
125
|
+
const id = src.slice(i, j);
|
|
126
|
+
if (id === "true" || id === "false") {
|
|
127
|
+
tokens.push({ type: "bool", value: id === "true" });
|
|
128
|
+
} else if (id === "null") {
|
|
129
|
+
tokens.push({ type: "null", value: null });
|
|
130
|
+
} else if (id === "contains") {
|
|
131
|
+
tokens.push({ type: "op", value: "contains" });
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(`unknown identifier: ${id}`);
|
|
134
|
+
}
|
|
135
|
+
i = j;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`unexpected char at ${i}: ${c}`);
|
|
139
|
+
}
|
|
140
|
+
return tokens;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Resolve a ref token like `step.fetch.summary` or `item` against ctx. */
|
|
144
|
+
export function resolveReference(ref, ctx) {
|
|
145
|
+
if (ref === "item") return ctx?.item;
|
|
146
|
+
const parts = ref.split(".");
|
|
147
|
+
if (parts[0] !== "step") {
|
|
148
|
+
throw new Error(`unknown reference root: ${parts[0]}`);
|
|
149
|
+
}
|
|
150
|
+
if (parts.length !== 3) {
|
|
151
|
+
throw new Error(`ref must be step.<id>.<field>: ${ref}`);
|
|
152
|
+
}
|
|
153
|
+
const [, stepId, field] = parts;
|
|
154
|
+
const entry = ctx?.step?.get?.(stepId);
|
|
155
|
+
if (!entry) return undefined;
|
|
156
|
+
if (field === "status") return entry.status;
|
|
157
|
+
if (field === "taskId") return entry.taskId;
|
|
158
|
+
if (field === "summary") return entry.result?.summary;
|
|
159
|
+
if (field === "tokenCount") return entry.result?.tokenCount;
|
|
160
|
+
if (field === "iterationCount") return entry.result?.iterationCount;
|
|
161
|
+
if (field === "toolsUsed") return entry.result?.toolsUsed;
|
|
162
|
+
if (field === "items") return entry.result?.items;
|
|
163
|
+
// Generic fallback: look up directly on result
|
|
164
|
+
return entry.result?.[field];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Recursive-descent parser returning an AST. */
|
|
168
|
+
function parse(tokens) {
|
|
169
|
+
let pos = 0;
|
|
170
|
+
function peek() {
|
|
171
|
+
return tokens[pos];
|
|
172
|
+
}
|
|
173
|
+
function consume(expected) {
|
|
174
|
+
const t = tokens[pos];
|
|
175
|
+
if (!t) throw new Error("unexpected end of expression");
|
|
176
|
+
if (expected && (t.type !== "op" || t.value !== expected)) {
|
|
177
|
+
throw new Error(`expected ${expected}, got ${t.value ?? t.type}`);
|
|
178
|
+
}
|
|
179
|
+
pos++;
|
|
180
|
+
return t;
|
|
181
|
+
}
|
|
182
|
+
function parseExpr() {
|
|
183
|
+
return parseOr();
|
|
184
|
+
}
|
|
185
|
+
function parseOr() {
|
|
186
|
+
let left = parseAnd();
|
|
187
|
+
while (peek() && peek().type === "op" && peek().value === "||") {
|
|
188
|
+
pos++;
|
|
189
|
+
const right = parseAnd();
|
|
190
|
+
left = { kind: "or", left, right };
|
|
191
|
+
}
|
|
192
|
+
return left;
|
|
193
|
+
}
|
|
194
|
+
function parseAnd() {
|
|
195
|
+
let left = parseNot();
|
|
196
|
+
while (peek() && peek().type === "op" && peek().value === "&&") {
|
|
197
|
+
pos++;
|
|
198
|
+
const right = parseNot();
|
|
199
|
+
left = { kind: "and", left, right };
|
|
200
|
+
}
|
|
201
|
+
return left;
|
|
202
|
+
}
|
|
203
|
+
function parseNot() {
|
|
204
|
+
if (peek() && peek().type === "op" && peek().value === "!") {
|
|
205
|
+
pos++;
|
|
206
|
+
return { kind: "not", expr: parseNot() };
|
|
207
|
+
}
|
|
208
|
+
return parseCmp();
|
|
209
|
+
}
|
|
210
|
+
function parseCmp() {
|
|
211
|
+
const left = parsePrimary();
|
|
212
|
+
const t = peek();
|
|
213
|
+
const cmpOps = new Set(["==", "!=", "<", "<=", ">", ">=", "contains"]);
|
|
214
|
+
if (t && t.type === "op" && cmpOps.has(t.value)) {
|
|
215
|
+
pos++;
|
|
216
|
+
const right = parsePrimary();
|
|
217
|
+
return { kind: "cmp", op: t.value, left, right };
|
|
218
|
+
}
|
|
219
|
+
// bare value → truthiness check
|
|
220
|
+
return { kind: "truthy", expr: left };
|
|
221
|
+
}
|
|
222
|
+
function parsePrimary() {
|
|
223
|
+
const t = peek();
|
|
224
|
+
if (!t) throw new Error("unexpected end of expression");
|
|
225
|
+
if (t.type === "op" && t.value === "(") {
|
|
226
|
+
pos++;
|
|
227
|
+
const e = parseExpr();
|
|
228
|
+
consume(")");
|
|
229
|
+
return e;
|
|
230
|
+
}
|
|
231
|
+
if (t.type === "ref") {
|
|
232
|
+
pos++;
|
|
233
|
+
return { kind: "ref", name: t.value };
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
t.type === "string" ||
|
|
237
|
+
t.type === "number" ||
|
|
238
|
+
t.type === "bool" ||
|
|
239
|
+
t.type === "null"
|
|
240
|
+
) {
|
|
241
|
+
pos++;
|
|
242
|
+
return { kind: "lit", value: t.value };
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`unexpected token: ${t.value ?? t.type}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const ast = parseExpr();
|
|
248
|
+
if (pos !== tokens.length) {
|
|
249
|
+
throw new Error(`trailing tokens at ${pos}`);
|
|
250
|
+
}
|
|
251
|
+
return ast;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function evalAst(ast, ctx) {
|
|
255
|
+
switch (ast.kind) {
|
|
256
|
+
case "lit":
|
|
257
|
+
return ast.value;
|
|
258
|
+
case "ref":
|
|
259
|
+
return resolveReference(ast.name, ctx);
|
|
260
|
+
case "not":
|
|
261
|
+
return !evalAst(ast.expr, ctx);
|
|
262
|
+
case "and":
|
|
263
|
+
return evalAst(ast.left, ctx) && evalAst(ast.right, ctx);
|
|
264
|
+
case "or":
|
|
265
|
+
return evalAst(ast.left, ctx) || evalAst(ast.right, ctx);
|
|
266
|
+
case "truthy": {
|
|
267
|
+
const v = evalAst(ast.expr, ctx);
|
|
268
|
+
return !!v;
|
|
269
|
+
}
|
|
270
|
+
case "cmp": {
|
|
271
|
+
const l = evalAst(ast.left, ctx);
|
|
272
|
+
const r = evalAst(ast.right, ctx);
|
|
273
|
+
switch (ast.op) {
|
|
274
|
+
case "==":
|
|
275
|
+
// Loose equality across string/number for ergonomic use
|
|
276
|
+
// eslint-disable-next-line eqeqeq
|
|
277
|
+
return l == r;
|
|
278
|
+
case "!=":
|
|
279
|
+
// eslint-disable-next-line eqeqeq
|
|
280
|
+
return l != r;
|
|
281
|
+
case "<":
|
|
282
|
+
return l < r;
|
|
283
|
+
case "<=":
|
|
284
|
+
return l <= r;
|
|
285
|
+
case ">":
|
|
286
|
+
return l > r;
|
|
287
|
+
case ">=":
|
|
288
|
+
return l >= r;
|
|
289
|
+
case "contains": {
|
|
290
|
+
if (l == null) return false;
|
|
291
|
+
if (Array.isArray(l)) return l.includes(r);
|
|
292
|
+
return String(l).includes(String(r));
|
|
293
|
+
}
|
|
294
|
+
default:
|
|
295
|
+
throw new Error(`unknown op: ${ast.op}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
throw new Error(`unknown node: ${ast.kind}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Evaluate an expression string against a context. Returns a boolean. */
|
|
304
|
+
export function evaluate(src, ctx) {
|
|
305
|
+
const tokens = tokenize(src);
|
|
306
|
+
const ast = parse(tokens);
|
|
307
|
+
const v = evalAst(ast, ctx);
|
|
308
|
+
return !!v;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Evaluate an expression and return the raw (non-coerced) value. */
|
|
312
|
+
export function evaluateRaw(src, ctx) {
|
|
313
|
+
const tokens = tokenize(src);
|
|
314
|
+
let ast = parse(tokens);
|
|
315
|
+
// Strip the implicit truthy-wrapper so bare refs return raw values.
|
|
316
|
+
if (ast.kind === "truthy") ast = ast.expr;
|
|
317
|
+
return evalAst(ast, ctx);
|
|
318
|
+
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from "../harness/jsonl-session-store.js";
|
|
40
40
|
import { storeMemory, consolidateMemory } from "../lib/hierarchical-memory.js";
|
|
41
41
|
import { CLIContextEngineering } from "../lib/cli-context-engineering.js";
|
|
42
|
+
import { defaultPrepareCall } from "../lib/turn-context.js";
|
|
42
43
|
import { createChatFn } from "../lib/cowork-adapter.js";
|
|
43
44
|
import {
|
|
44
45
|
detectTaskType,
|
|
@@ -1223,6 +1224,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
1223
1224
|
apiKey,
|
|
1224
1225
|
contextEngine,
|
|
1225
1226
|
iterationBudget,
|
|
1227
|
+
sessionId,
|
|
1228
|
+
cwd: process.cwd(),
|
|
1229
|
+
prepareCall: defaultPrepareCall,
|
|
1226
1230
|
});
|
|
1227
1231
|
|
|
1228
1232
|
if (response) {
|