neko-harness 0.3.2 → 0.3.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 +1 -0
- package/dist/client/assets/index-Dja-p0ED.js +133 -0
- package/dist/client/assets/index-K81InyqT.css +2 -0
- package/dist/client/index.html +2 -2
- package/dist/gateway.js +981 -350
- package/dist/neko.js +1524 -449
- package/package.json +1 -1
- package/dist/client/assets/index-DhW2urBC.css +0 -2
- package/dist/client/assets/index-Dz-jYu0K.js +0 -129
package/dist/gateway.js
CHANGED
|
@@ -434,6 +434,125 @@ async function prepareInput(content, model) {
|
|
|
434
434
|
let index = 0;
|
|
435
435
|
return content.map((b) => b.type === "image" ? prepared[index++] : b);
|
|
436
436
|
}
|
|
437
|
+
// packages/core/src/jobs.ts
|
|
438
|
+
import { defineDoc } from "@earendil-works/pi-durable";
|
|
439
|
+
var JOB_LIMITS = {
|
|
440
|
+
maxRunning: 4,
|
|
441
|
+
keepTerminal: 20,
|
|
442
|
+
startWindowMs: 2000,
|
|
443
|
+
tailBytes: 64 * 1024,
|
|
444
|
+
tailLines: 1000,
|
|
445
|
+
reportBytes: 4 * 1024,
|
|
446
|
+
reportLines: 40,
|
|
447
|
+
readBytes: 64 * 1024,
|
|
448
|
+
titleChars: 200,
|
|
449
|
+
commandBytes: 32 * 1024,
|
|
450
|
+
maxTimeoutSeconds: 7 * 24 * 3600
|
|
451
|
+
};
|
|
452
|
+
var JobsDoc = defineDoc({
|
|
453
|
+
kind: "app.neko.jobs",
|
|
454
|
+
version: 1,
|
|
455
|
+
scope: "conversation",
|
|
456
|
+
history: "latest",
|
|
457
|
+
fork: "initial",
|
|
458
|
+
initial: () => ({ next: 1, items: [] })
|
|
459
|
+
});
|
|
460
|
+
var summarizeJobRecord = (job, outputBytes = job.totalBytes ?? 0) => {
|
|
461
|
+
const {
|
|
462
|
+
tail: _tail,
|
|
463
|
+
totalBytes: _total,
|
|
464
|
+
runnerId: _runner,
|
|
465
|
+
reporterId: _reporter,
|
|
466
|
+
stopOrigin: _origin,
|
|
467
|
+
...rest
|
|
468
|
+
} = job;
|
|
469
|
+
return { ...rest, outputBytes };
|
|
470
|
+
};
|
|
471
|
+
var isTerminalJob = (status) => status !== "running";
|
|
472
|
+
var encoder = new TextEncoder;
|
|
473
|
+
var decoder = new TextDecoder;
|
|
474
|
+
var byteLength = (text) => encoder.encode(text).length;
|
|
475
|
+
function tailOf(text, maxBytes, maxLines) {
|
|
476
|
+
let bytes = encoder.encode(text);
|
|
477
|
+
if (bytes.length > maxBytes) {
|
|
478
|
+
let start = bytes.length - maxBytes;
|
|
479
|
+
while (start < bytes.length && (bytes[start] & 192) === 128)
|
|
480
|
+
start++;
|
|
481
|
+
bytes = bytes.subarray(start);
|
|
482
|
+
text = decoder.decode(bytes);
|
|
483
|
+
}
|
|
484
|
+
let index = text.length;
|
|
485
|
+
let seen = 0;
|
|
486
|
+
if (text.endsWith(`
|
|
487
|
+
`))
|
|
488
|
+
index--;
|
|
489
|
+
while (index > 0) {
|
|
490
|
+
const found = text.lastIndexOf(`
|
|
491
|
+
`, index - 1);
|
|
492
|
+
if (found < 0)
|
|
493
|
+
break;
|
|
494
|
+
if (++seen >= maxLines)
|
|
495
|
+
return text.slice(found + 1);
|
|
496
|
+
index = found;
|
|
497
|
+
}
|
|
498
|
+
return text;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
class OutputBuffer {
|
|
502
|
+
#chunks = [];
|
|
503
|
+
#bytes = 0;
|
|
504
|
+
#total = 0;
|
|
505
|
+
#maxBytes;
|
|
506
|
+
#maxLines;
|
|
507
|
+
constructor(maxBytes = JOB_LIMITS.tailBytes, maxLines = JOB_LIMITS.tailLines) {
|
|
508
|
+
this.#maxBytes = maxBytes;
|
|
509
|
+
this.#maxLines = maxLines;
|
|
510
|
+
}
|
|
511
|
+
static fromTail(tail, totalBytes) {
|
|
512
|
+
const buffer = new OutputBuffer;
|
|
513
|
+
buffer.#chunks = [tail];
|
|
514
|
+
buffer.#bytes = byteLength(tail);
|
|
515
|
+
buffer.#total = totalBytes;
|
|
516
|
+
return buffer;
|
|
517
|
+
}
|
|
518
|
+
get totalBytes() {
|
|
519
|
+
return this.#total;
|
|
520
|
+
}
|
|
521
|
+
append(text) {
|
|
522
|
+
if (!text)
|
|
523
|
+
return;
|
|
524
|
+
const size = byteLength(text);
|
|
525
|
+
this.#total += size;
|
|
526
|
+
this.#bytes += size;
|
|
527
|
+
this.#chunks.push(text);
|
|
528
|
+
if (this.#bytes > this.#maxBytes * 2)
|
|
529
|
+
this.#trim();
|
|
530
|
+
}
|
|
531
|
+
#trim() {
|
|
532
|
+
const text = tailOf(this.#chunks.join(""), this.#maxBytes, this.#maxLines);
|
|
533
|
+
this.#chunks = [text];
|
|
534
|
+
this.#bytes = byteLength(text);
|
|
535
|
+
return text;
|
|
536
|
+
}
|
|
537
|
+
tail() {
|
|
538
|
+
return this.#trim();
|
|
539
|
+
}
|
|
540
|
+
read(since, maxBytes = JOB_LIMITS.readBytes) {
|
|
541
|
+
const tail = this.#trim();
|
|
542
|
+
const start = this.#total - this.#bytes;
|
|
543
|
+
const all = encoder.encode(tail);
|
|
544
|
+
let begin = since === undefined ? start : Math.min(Math.max(since, start), this.#total);
|
|
545
|
+
let bytes = all.subarray(begin - start);
|
|
546
|
+
let cut = Math.max(0, bytes.length - maxBytes);
|
|
547
|
+
while (cut < bytes.length && (bytes[cut] & 192) === 128)
|
|
548
|
+
cut++;
|
|
549
|
+
begin += cut;
|
|
550
|
+
bytes = bytes.subarray(cut);
|
|
551
|
+
const dropped = Math.max(0, begin - (since ?? 0));
|
|
552
|
+
return { text: decoder.decode(bytes), offset: this.#total, truncated: dropped > 0, dropped };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
var reportTail = (text) => tailOf(text, JOB_LIMITS.reportBytes, JOB_LIMITS.reportLines);
|
|
437
556
|
// packages/core/src/image-prompt.ts
|
|
438
557
|
var DEFAULT_IMAGE_PROMPT = {
|
|
439
558
|
version: 2,
|
|
@@ -691,6 +810,17 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
691
810
|
const items = [];
|
|
692
811
|
for (const entry of view.entries) {
|
|
693
812
|
const message = entry.model?.[0];
|
|
813
|
+
const jobReport = entry.kind === NekoInput.kind ? entry.data?.jobReport : undefined;
|
|
814
|
+
if (message?.role === "user" && jobReport) {
|
|
815
|
+
items.push({
|
|
816
|
+
type: "system",
|
|
817
|
+
key: `e${entry.id}`,
|
|
818
|
+
id: entry.id,
|
|
819
|
+
text: userText(message.content),
|
|
820
|
+
jobId: jobReport.jobId
|
|
821
|
+
});
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
694
824
|
if (message?.role === "user") {
|
|
695
825
|
const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
|
|
696
826
|
items.push({
|
|
@@ -912,7 +1042,7 @@ import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
|
|
|
912
1042
|
import {
|
|
913
1043
|
AssistantEntry as AssistantEntry2,
|
|
914
1044
|
createRegistry,
|
|
915
|
-
defineDoc as
|
|
1045
|
+
defineDoc as defineDoc5,
|
|
916
1046
|
Harness,
|
|
917
1047
|
LiveDoc as importLiveDoc,
|
|
918
1048
|
MemoryStorage,
|
|
@@ -1151,150 +1281,20 @@ var ImageDetails = defineExtension2({
|
|
|
1151
1281
|
]
|
|
1152
1282
|
});
|
|
1153
1283
|
|
|
1154
|
-
// packages/core/src/
|
|
1155
|
-
import {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
};
|
|
1162
|
-
var isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
1163
|
-
|
|
1164
|
-
// packages/core/src/prompt/skills.ts
|
|
1165
|
-
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
1166
|
-
import { basename, dirname as dirname3, join as join3 } from "path";
|
|
1167
|
-
var MAX_NAME_LENGTH = 64;
|
|
1168
|
-
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
1169
|
-
var parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
|
|
1170
|
-
const skip = (message) => {
|
|
1171
|
-
report({ path: filePath, message });
|
|
1172
|
-
return null;
|
|
1173
|
-
};
|
|
1174
|
-
try {
|
|
1175
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
1176
|
-
const description = frontmatter.description;
|
|
1177
|
-
if (typeof description !== "string" || description.trim() === "")
|
|
1178
|
-
return skip("Skill description is missing or empty; skipped.");
|
|
1179
|
-
if (description.length > MAX_DESCRIPTION_LENGTH)
|
|
1180
|
-
return skip("Skill description is too long; skipped.");
|
|
1181
|
-
const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename(dirname3(filePath));
|
|
1182
|
-
if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
|
|
1183
|
-
return skip("Skill name is too long or contains whitespace/control characters; skipped.");
|
|
1184
|
-
return {
|
|
1185
|
-
name,
|
|
1186
|
-
description,
|
|
1187
|
-
filePath,
|
|
1188
|
-
disableModelInvocation: frontmatter["disable-model-invocation"] === true
|
|
1189
|
-
};
|
|
1190
|
-
} catch {
|
|
1191
|
-
return skip("Invalid skill frontmatter; skipped.");
|
|
1192
|
-
}
|
|
1193
|
-
};
|
|
1194
|
-
var scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
|
|
1195
|
-
let entries;
|
|
1196
|
-
try {
|
|
1197
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
1198
|
-
} catch (error) {
|
|
1199
|
-
if (!isMissing(error))
|
|
1200
|
-
report({ path: dir, message: "Cannot scan skills directory; skipped." });
|
|
1201
|
-
return [];
|
|
1202
|
-
}
|
|
1203
|
-
const skills = [];
|
|
1204
|
-
for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
1205
|
-
if (!entry.isDirectory())
|
|
1206
|
-
continue;
|
|
1207
|
-
const filePath = join3(dir, entry.name, "SKILL.md");
|
|
1208
|
-
let content;
|
|
1209
|
-
try {
|
|
1210
|
-
content = await readFile2(filePath, "utf8");
|
|
1211
|
-
} catch (error) {
|
|
1212
|
-
if (!isMissing(error))
|
|
1213
|
-
report({ path: filePath, message: "Cannot read skill; skipped." });
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
const skill = parseSkillFile(content, filePath, report);
|
|
1217
|
-
if (skill) {
|
|
1218
|
-
skills.push(skill);
|
|
1219
|
-
capture?.(skill, content);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
return skills;
|
|
1223
|
-
};
|
|
1224
|
-
var mergeSkills = (groups) => {
|
|
1225
|
-
const byName = new Map;
|
|
1226
|
-
for (const group of groups)
|
|
1227
|
-
for (const skill of group)
|
|
1228
|
-
byName.set(skill.name, skill);
|
|
1229
|
-
return [...byName.values()];
|
|
1230
|
-
};
|
|
1231
|
-
var escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1232
|
-
var formatSkillsForPrompt = (skills) => {
|
|
1233
|
-
const visible = skills.filter((skill) => !skill.disableModelInvocation);
|
|
1234
|
-
if (visible.length === 0)
|
|
1235
|
-
return "";
|
|
1236
|
-
const lines = [
|
|
1237
|
-
"The following skills provide specialized instructions for specific tasks.",
|
|
1238
|
-
"Use the read tool to load a skill's file when the task matches its description.",
|
|
1239
|
-
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.",
|
|
1240
|
-
"",
|
|
1241
|
-
"<available_skills>"
|
|
1242
|
-
];
|
|
1243
|
-
for (const skill of visible) {
|
|
1244
|
-
lines.push(" <skill>");
|
|
1245
|
-
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
1246
|
-
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
1247
|
-
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
1248
|
-
lines.push(" </skill>");
|
|
1249
|
-
}
|
|
1250
|
-
lines.push("</available_skills>");
|
|
1251
|
-
return lines.join(`
|
|
1252
|
-
`);
|
|
1253
|
-
};
|
|
1254
|
-
|
|
1255
|
-
// packages/core/src/prompt/invoke.ts
|
|
1256
|
-
var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
|
|
1257
|
-
if (!text.startsWith("/skill:"))
|
|
1258
|
-
return text;
|
|
1259
|
-
const spaceIndex = text.indexOf(" ");
|
|
1260
|
-
const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
|
1261
|
-
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
1262
|
-
const skill = skills.find((s) => s.name === skillName);
|
|
1263
|
-
if (!skill)
|
|
1264
|
-
return text;
|
|
1265
|
-
let body;
|
|
1266
|
-
try {
|
|
1267
|
-
body = stripFrontmatter(await readFile(skill.filePath)).trim();
|
|
1268
|
-
} catch {
|
|
1269
|
-
report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
|
|
1270
|
-
return text;
|
|
1271
|
-
}
|
|
1272
|
-
metadata?.({
|
|
1273
|
-
name: skill.name,
|
|
1274
|
-
location: skill.filePath,
|
|
1275
|
-
body: `References are relative to ${dirname4(skill.filePath)}.
|
|
1276
|
-
|
|
1277
|
-
${body}`,
|
|
1278
|
-
args
|
|
1279
|
-
});
|
|
1280
|
-
const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
|
|
1281
|
-
References are relative to ${dirname4(skill.filePath)}.
|
|
1282
|
-
|
|
1283
|
-
${body}
|
|
1284
|
-
</skill>`;
|
|
1285
|
-
return args ? `${block}
|
|
1286
|
-
|
|
1287
|
-
${args}` : block;
|
|
1288
|
-
};
|
|
1289
|
-
|
|
1290
|
-
// packages/core/src/prompt/prompt.ts
|
|
1291
|
-
import { defineExtension as defineExtension4, section } from "@earendil-works/pi-durable";
|
|
1284
|
+
// packages/core/src/job-engine.ts
|
|
1285
|
+
import { withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
|
|
1286
|
+
import {
|
|
1287
|
+
defineExtension as defineExtension4,
|
|
1288
|
+
defineTask as defineTask2,
|
|
1289
|
+
defineTool as defineTool3
|
|
1290
|
+
} from "@earendil-works/pi-durable";
|
|
1291
|
+
import { Type as Type3 } from "typebox";
|
|
1292
1292
|
|
|
1293
1293
|
// packages/core/src/subagents.ts
|
|
1294
1294
|
import {
|
|
1295
1295
|
AssistantEntry,
|
|
1296
1296
|
configure,
|
|
1297
|
-
defineDoc,
|
|
1297
|
+
defineDoc as defineDoc2,
|
|
1298
1298
|
defineExtension as defineExtension3,
|
|
1299
1299
|
defineTask,
|
|
1300
1300
|
defineTool as defineTool2,
|
|
@@ -1348,7 +1348,7 @@ function subagentUsageTotals(state) {
|
|
|
1348
1348
|
|
|
1349
1349
|
// packages/core/src/subagents.ts
|
|
1350
1350
|
var agentIdFor = (childId) => `agent-${childId}`;
|
|
1351
|
-
var DelegationsDoc =
|
|
1351
|
+
var DelegationsDoc = defineDoc2({
|
|
1352
1352
|
kind: "app.neko.delegations",
|
|
1353
1353
|
version: 1,
|
|
1354
1354
|
scope: "conversation",
|
|
@@ -1356,7 +1356,7 @@ var DelegationsDoc = defineDoc({
|
|
|
1356
1356
|
fork: "initial",
|
|
1357
1357
|
initial: () => ({ items: [] })
|
|
1358
1358
|
});
|
|
1359
|
-
var SubagentsDoc =
|
|
1359
|
+
var SubagentsDoc = defineDoc2({
|
|
1360
1360
|
kind: "app.neko.subagents",
|
|
1361
1361
|
version: 1,
|
|
1362
1362
|
scope: "conversation",
|
|
@@ -1364,7 +1364,7 @@ var SubagentsDoc = defineDoc({
|
|
|
1364
1364
|
fork: "initial",
|
|
1365
1365
|
initial: () => ({ items: [] })
|
|
1366
1366
|
});
|
|
1367
|
-
var SubagentDoc =
|
|
1367
|
+
var SubagentDoc = defineDoc2({
|
|
1368
1368
|
kind: "app.neko.subagent",
|
|
1369
1369
|
version: 1,
|
|
1370
1370
|
scope: "conversation",
|
|
@@ -1372,7 +1372,7 @@ var SubagentDoc = defineDoc({
|
|
|
1372
1372
|
fork: "initial",
|
|
1373
1373
|
initial: () => ({ role: null, parentId: null })
|
|
1374
1374
|
});
|
|
1375
|
-
var SlotsDoc =
|
|
1375
|
+
var SlotsDoc = defineDoc2({
|
|
1376
1376
|
kind: "app.neko.subagent-slots",
|
|
1377
1377
|
version: 1,
|
|
1378
1378
|
scope: "session",
|
|
@@ -1703,7 +1703,7 @@ function createSubagentExtension(prepare, discard) {
|
|
|
1703
1703
|
thinkingLevel: agent.thinkingLevel,
|
|
1704
1704
|
cwd: agent.cwd,
|
|
1705
1705
|
extensions: agent.extensions.filter((e) => e.name !== extension.name),
|
|
1706
|
-
tools: agent.tools.filter((t) => t.name !== "subagent" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
|
|
1706
|
+
tools: agent.tools.filter((t) => t.name !== "subagent" && t.name !== "job" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
|
|
1707
1707
|
});
|
|
1708
1708
|
Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
|
|
1709
1709
|
identity = {
|
|
@@ -1844,7 +1844,567 @@ async function installSubagentCoordinator(harness, parents, context) {
|
|
|
1844
1844
|
harness.closed.finally(stop).catch(() => {});
|
|
1845
1845
|
}
|
|
1846
1846
|
|
|
1847
|
+
// packages/core/src/job-engine.ts
|
|
1848
|
+
var plain = (value) => JSON.parse(JSON.stringify(value));
|
|
1849
|
+
|
|
1850
|
+
class JobEntry {
|
|
1851
|
+
controller = new AbortController;
|
|
1852
|
+
buffer = new OutputBuffer;
|
|
1853
|
+
windowOpen = false;
|
|
1854
|
+
suppressReport = false;
|
|
1855
|
+
exited;
|
|
1856
|
+
#resolve;
|
|
1857
|
+
constructor() {
|
|
1858
|
+
this.exited = new Promise((resolve) => {
|
|
1859
|
+
this.#resolve = resolve;
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
finish() {
|
|
1863
|
+
this.#resolve();
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
class JobRegistry {
|
|
1868
|
+
#entries = new Map;
|
|
1869
|
+
static key(conversationId, jobId) {
|
|
1870
|
+
return `${conversationId}:${jobId}`;
|
|
1871
|
+
}
|
|
1872
|
+
ensure(conversationId, jobId) {
|
|
1873
|
+
const key = JobRegistry.key(conversationId, jobId);
|
|
1874
|
+
let entry = this.#entries.get(key);
|
|
1875
|
+
if (!entry) {
|
|
1876
|
+
entry = new JobEntry;
|
|
1877
|
+
this.#entries.set(key, entry);
|
|
1878
|
+
}
|
|
1879
|
+
return entry;
|
|
1880
|
+
}
|
|
1881
|
+
get(conversationId, jobId) {
|
|
1882
|
+
return this.#entries.get(JobRegistry.key(conversationId, jobId));
|
|
1883
|
+
}
|
|
1884
|
+
delete(conversationId, jobId) {
|
|
1885
|
+
this.#entries.delete(JobRegistry.key(conversationId, jobId));
|
|
1886
|
+
}
|
|
1887
|
+
abortAll() {
|
|
1888
|
+
for (const entry of this.#entries.values())
|
|
1889
|
+
entry.controller.abort();
|
|
1890
|
+
}
|
|
1891
|
+
abortConversation(conversationId) {
|
|
1892
|
+
for (const [key, entry] of this.#entries)
|
|
1893
|
+
if (key.startsWith(`${conversationId}:`))
|
|
1894
|
+
entry.controller.abort();
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
var statusWord = {
|
|
1898
|
+
running: "running",
|
|
1899
|
+
exited: "exited",
|
|
1900
|
+
failed: "failed",
|
|
1901
|
+
stopped: "stopped",
|
|
1902
|
+
lost: "lost"
|
|
1903
|
+
};
|
|
1904
|
+
var describe = (job) => {
|
|
1905
|
+
const parts = [statusWord[job.status]];
|
|
1906
|
+
if (job.exitCode !== null)
|
|
1907
|
+
parts.push(`exit code ${job.exitCode}`);
|
|
1908
|
+
if (job.reason === "timeout")
|
|
1909
|
+
parts.push(`timed out after ${job.timeoutSeconds} seconds`);
|
|
1910
|
+
if (job.reason === "spawn-error")
|
|
1911
|
+
parts.push("could not start");
|
|
1912
|
+
if (job.reason === "restart")
|
|
1913
|
+
parts.push("the process was lost when the gateway restarted and was not re-run");
|
|
1914
|
+
return parts.join(", ");
|
|
1915
|
+
};
|
|
1916
|
+
function reportText(job, tail) {
|
|
1917
|
+
const body = reportTail(tail);
|
|
1918
|
+
return [
|
|
1919
|
+
`[Background job ${job.jobId} "${job.title}" ${describe(job)}]`,
|
|
1920
|
+
`Command: ${job.command}`,
|
|
1921
|
+
body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
|
|
1922
|
+
${body}` : "It produced no output."
|
|
1923
|
+
].join(`
|
|
1924
|
+
`);
|
|
1925
|
+
}
|
|
1926
|
+
async function settleJob(tx, conversationId, jobId, outcome, tail, quiet, reporter) {
|
|
1927
|
+
const state = await tx.doc(JobsDoc, conversationId);
|
|
1928
|
+
const job = state.items.find((item) => item.jobId === jobId);
|
|
1929
|
+
if (!job || isTerminalJob(job.status))
|
|
1930
|
+
return;
|
|
1931
|
+
job.status = outcome.status;
|
|
1932
|
+
job.exitCode = outcome.exitCode;
|
|
1933
|
+
if (outcome.reason)
|
|
1934
|
+
job.reason = outcome.reason;
|
|
1935
|
+
job.endedAt = Date.now();
|
|
1936
|
+
job.tail = tail.text;
|
|
1937
|
+
job.totalBytes = tail.totalBytes;
|
|
1938
|
+
if (!quiet && (job.status !== "stopped" || job.stopOrigin === "user"))
|
|
1939
|
+
job.reporterId = await tx.createTask(reporter, { jobId, text: reportText(job, tail.text), status: job.status, exitCode: job.exitCode }, { ownership: { kind: "conversation" }, conversationId, background: true });
|
|
1940
|
+
const terminal = state.items.filter((item) => isTerminalJob(item.status));
|
|
1941
|
+
if (terminal.length > JOB_LIMITS.keepTerminal) {
|
|
1942
|
+
const drop = new Set(terminal.sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0)).slice(0, terminal.length - JOB_LIMITS.keepTerminal).map((item) => item.jobId));
|
|
1943
|
+
state.items = state.items.filter((item) => !drop.has(item.jobId));
|
|
1944
|
+
}
|
|
1945
|
+
return job;
|
|
1946
|
+
}
|
|
1947
|
+
function createReporter() {
|
|
1948
|
+
return defineTask2({
|
|
1949
|
+
name: "app.neko.job-reporter",
|
|
1950
|
+
version: 1,
|
|
1951
|
+
initial: () => ({ phase: "report" }),
|
|
1952
|
+
phases: {
|
|
1953
|
+
report: async (task, runtime, ctx) => {
|
|
1954
|
+
const target = await runtime.conversation(runtime.conversationId, ctx);
|
|
1955
|
+
await target.submit({
|
|
1956
|
+
type: "input",
|
|
1957
|
+
content: task.input.text,
|
|
1958
|
+
whenBusy: "followUp",
|
|
1959
|
+
requestId: `job-report:${task.input.jobId}`,
|
|
1960
|
+
entry: {
|
|
1961
|
+
kind: NekoInput.kind,
|
|
1962
|
+
data: {
|
|
1963
|
+
at: Date.now(),
|
|
1964
|
+
prompt: task.input.text,
|
|
1965
|
+
images: [],
|
|
1966
|
+
jobReport: { jobId: task.input.jobId, status: task.input.status, exitCode: task.input.exitCode }
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}, ctx);
|
|
1970
|
+
await runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: null } }), ctx);
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
abort: (_task, runtime, ctx) => runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), ctx)
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
var delay = (ms, signal) => new Promise((resolve) => {
|
|
1977
|
+
const timer = setTimeout(done, ms);
|
|
1978
|
+
function done() {
|
|
1979
|
+
clearTimeout(timer);
|
|
1980
|
+
signal?.removeEventListener("abort", done);
|
|
1981
|
+
resolve();
|
|
1982
|
+
}
|
|
1983
|
+
if (signal?.aborted)
|
|
1984
|
+
done();
|
|
1985
|
+
else
|
|
1986
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
1987
|
+
});
|
|
1988
|
+
var action = Type3.Union([Type3.Literal("start"), Type3.Literal("output"), Type3.Literal("stop"), Type3.Literal("list")]);
|
|
1989
|
+
var parameters2 = Type3.Object({
|
|
1990
|
+
action,
|
|
1991
|
+
command: Type3.Optional(Type3.String({ description: "start: shell command to run in the session's working directory." })),
|
|
1992
|
+
title: Type3.Optional(Type3.String({ description: "start: short label shown in the job list." })),
|
|
1993
|
+
timeout: Type3.Optional(Type3.Number({ description: "start: optional timeout in seconds; no default timeout." })),
|
|
1994
|
+
jobId: Type3.Optional(Type3.String({ description: "output/stop: the job id returned by start or list." })),
|
|
1995
|
+
since: Type3.Optional(Type3.Integer({ description: "output: resume after this offset, as returned by the previous output call." }))
|
|
1996
|
+
});
|
|
1997
|
+
var stringField = (value, name) => {
|
|
1998
|
+
if (typeof value !== "string" || !value.trim())
|
|
1999
|
+
throw new Error(`Invalid job arguments: ${name} must be a nonempty string.`);
|
|
2000
|
+
return value;
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
class JobManager {
|
|
2004
|
+
registry = new JobRegistry;
|
|
2005
|
+
#reporter = createReporter();
|
|
2006
|
+
#runner = this.#createRunner();
|
|
2007
|
+
extension;
|
|
2008
|
+
constructor() {
|
|
2009
|
+
this.extension = this.#createExtension();
|
|
2010
|
+
}
|
|
2011
|
+
#closing = false;
|
|
2012
|
+
#createRunner() {
|
|
2013
|
+
const registry = this.registry;
|
|
2014
|
+
const reporter = this.#reporter;
|
|
2015
|
+
return defineTask2({
|
|
2016
|
+
name: "app.neko.job-runner",
|
|
2017
|
+
version: 1,
|
|
2018
|
+
initial: () => ({ phase: "start" }),
|
|
2019
|
+
phases: {
|
|
2020
|
+
start: (_task, runtime, ctx) => runtime.commit(() => ({ status: "running", checkpoint: { phase: "exec" } }), ctx),
|
|
2021
|
+
exec: async (task, runtime, ctx) => {
|
|
2022
|
+
const { jobId, command, cwd, timeoutSeconds } = task.input;
|
|
2023
|
+
const entry = registry.ensure(runtime.conversationId, jobId);
|
|
2024
|
+
let outcome;
|
|
2025
|
+
try {
|
|
2026
|
+
const env = await runtime.env(ctx);
|
|
2027
|
+
if (!env) {
|
|
2028
|
+
outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
|
|
2029
|
+
} else {
|
|
2030
|
+
const result = await env.exec(command, {
|
|
2031
|
+
...cwd ? { cwd } : {},
|
|
2032
|
+
...timeoutSeconds === undefined ? {} : { timeout: timeoutSeconds },
|
|
2033
|
+
onOutput: (text) => entry.buffer.append(text)
|
|
2034
|
+
}, withAbortSignal2(entry.controller.signal, ctx));
|
|
2035
|
+
if (result.ok)
|
|
2036
|
+
outcome = {
|
|
2037
|
+
status: result.value.exitCode === 0 ? "exited" : "failed",
|
|
2038
|
+
exitCode: result.value.exitCode
|
|
2039
|
+
};
|
|
2040
|
+
else if (result.error.code === "timeout")
|
|
2041
|
+
outcome = { status: "failed", exitCode: null, reason: "timeout" };
|
|
2042
|
+
else if (result.error.code === "aborted")
|
|
2043
|
+
outcome = { status: "stopped", exitCode: null, reason: "aborted" };
|
|
2044
|
+
else
|
|
2045
|
+
outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
|
|
2046
|
+
}
|
|
2047
|
+
if (this.#closing) {
|
|
2048
|
+
await new Promise((resolve) => {
|
|
2049
|
+
if (!ctx.abortSignal || ctx.abortSignal.aborted)
|
|
2050
|
+
resolve();
|
|
2051
|
+
else
|
|
2052
|
+
ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
2053
|
+
});
|
|
2054
|
+
throw ctx.abortSignal?.reason ?? new Error("Job runner interrupted by shutdown");
|
|
2055
|
+
}
|
|
2056
|
+
const quiet = entry.windowOpen;
|
|
2057
|
+
if (quiet)
|
|
2058
|
+
entry.suppressReport = true;
|
|
2059
|
+
const tail = { text: entry.buffer.tail(), totalBytes: entry.buffer.totalBytes };
|
|
2060
|
+
await runtime.commit(async (tx) => {
|
|
2061
|
+
const job = await settleJob(tx, runtime.conversationId, jobId, outcome, tail, quiet, reporter);
|
|
2062
|
+
return {
|
|
2063
|
+
status: "terminal",
|
|
2064
|
+
outcome: { status: "completed", result: { status: job?.status ?? outcome.status } }
|
|
2065
|
+
};
|
|
2066
|
+
}, ctx);
|
|
2067
|
+
} finally {
|
|
2068
|
+
registry.delete(runtime.conversationId, jobId);
|
|
2069
|
+
entry.finish();
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
},
|
|
2073
|
+
abort: async (task, runtime, ctx) => {
|
|
2074
|
+
const { jobId } = task.input;
|
|
2075
|
+
const entry = registry.get(runtime.conversationId, jobId);
|
|
2076
|
+
entry?.controller.abort();
|
|
2077
|
+
const restart = task.abortReason === "restart";
|
|
2078
|
+
const outcome = restart ? { status: "lost", exitCode: null, reason: "restart" } : { status: "stopped", exitCode: null, reason: "aborted" };
|
|
2079
|
+
try {
|
|
2080
|
+
await runtime.commit(async (tx) => {
|
|
2081
|
+
const known = (await tx.doc(JobsDoc, runtime.conversationId)).items.find((item) => item.jobId === jobId);
|
|
2082
|
+
const tail = entry ? { text: entry.buffer.tail(), totalBytes: entry.buffer.totalBytes } : { text: known?.tail ?? "", totalBytes: known?.totalBytes ?? 0 };
|
|
2083
|
+
await settleJob(tx, runtime.conversationId, jobId, outcome, tail, false, reporter);
|
|
2084
|
+
return { status: "terminal", outcome: { status: "aborted" } };
|
|
2085
|
+
}, ctx);
|
|
2086
|
+
} finally {
|
|
2087
|
+
registry.delete(runtime.conversationId, jobId);
|
|
2088
|
+
entry?.finish();
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
#createExtension() {
|
|
2094
|
+
return defineExtension4({
|
|
2095
|
+
name: "neko-jobs",
|
|
2096
|
+
tasks: [this.#runner, this.#reporter],
|
|
2097
|
+
tools: [
|
|
2098
|
+
defineTool3({
|
|
2099
|
+
name: "job",
|
|
2100
|
+
description: "Run long-lived commands (dev servers, watchers, long builds) in the background. start: command (optional title, timeout seconds) returns a jobId immediately; the command has no stdin. output: jobId (optional since offset) reads the output tail or what is new. stop: jobId terminates the whole process tree. list: show this session's jobs. When a job ends you receive a report; do not poll. At most 4 jobs run at once.",
|
|
2101
|
+
parameters: parameters2,
|
|
2102
|
+
replay: "unsafe",
|
|
2103
|
+
execute: async (input, api, context) => {
|
|
2104
|
+
const args = input;
|
|
2105
|
+
if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
|
|
2106
|
+
throw new Error("Subagents cannot use jobs.");
|
|
2107
|
+
const io = {
|
|
2108
|
+
commit: (change) => api.commit(change, context),
|
|
2109
|
+
state: async () => api.snapshot(JobsDoc, api.conversationId, context)
|
|
2110
|
+
};
|
|
2111
|
+
const kind = args.action;
|
|
2112
|
+
if (kind === "list")
|
|
2113
|
+
return this.#text(this.#listText((await io.state())?.items ?? []));
|
|
2114
|
+
if (kind === "output") {
|
|
2115
|
+
const jobId = stringField(args.jobId, "jobId");
|
|
2116
|
+
const since = args.since;
|
|
2117
|
+
if (since !== undefined && (!Number.isInteger(since) || since < 0))
|
|
2118
|
+
throw new Error("Invalid job arguments: since must be a non-negative integer.");
|
|
2119
|
+
return this.#text(await this.output(io, api.conversationId, jobId, since));
|
|
2120
|
+
}
|
|
2121
|
+
if (kind === "stop") {
|
|
2122
|
+
const jobId = stringField(args.jobId, "jobId");
|
|
2123
|
+
const job = await this.stop(io, api.conversationId, jobId, "model", context);
|
|
2124
|
+
return this.#text(`Job ${job.jobId} ${describe(job)}.`);
|
|
2125
|
+
}
|
|
2126
|
+
if (kind !== "start")
|
|
2127
|
+
throw new Error("Invalid job arguments: action must be start, output, stop or list.");
|
|
2128
|
+
const command = stringField(args.command, "command");
|
|
2129
|
+
if (Buffer.byteLength(command) > JOB_LIMITS.commandBytes)
|
|
2130
|
+
throw new Error("Invalid job arguments: command is too long.");
|
|
2131
|
+
const timeout = args.timeout;
|
|
2132
|
+
if (timeout !== undefined && (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0 || timeout > JOB_LIMITS.maxTimeoutSeconds))
|
|
2133
|
+
throw new Error("Invalid job arguments: timeout must be a positive number of seconds.");
|
|
2134
|
+
const title = typeof args.title === "string" && args.title.trim() ? args.title.trim() : command;
|
|
2135
|
+
const agent = await api.agent(context);
|
|
2136
|
+
return this.#text(await this.start(io, api.conversationId, agent.cwd ?? "", {
|
|
2137
|
+
command,
|
|
2138
|
+
title: title.slice(0, JOB_LIMITS.titleChars),
|
|
2139
|
+
...timeout === undefined ? {} : { timeoutSeconds: timeout }
|
|
2140
|
+
}, context));
|
|
2141
|
+
}
|
|
2142
|
+
})
|
|
2143
|
+
]
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
#text(text) {
|
|
2147
|
+
return { output: [{ type: "text", text }] };
|
|
2148
|
+
}
|
|
2149
|
+
#listText(items) {
|
|
2150
|
+
if (!items.length)
|
|
2151
|
+
return "No jobs.";
|
|
2152
|
+
return items.map((job) => `${job.jobId} [${describe(job)}] "${job.title}" \u2014 ${job.command}`).join(`
|
|
2153
|
+
`);
|
|
2154
|
+
}
|
|
2155
|
+
async start(io, conversationId, cwd, args, context) {
|
|
2156
|
+
let allocated;
|
|
2157
|
+
let entry;
|
|
2158
|
+
let created;
|
|
2159
|
+
try {
|
|
2160
|
+
created = await io.commit(async (tx) => {
|
|
2161
|
+
const state = await tx.doc(JobsDoc, conversationId);
|
|
2162
|
+
if (state.items.filter((item) => item.status === "running").length >= JOB_LIMITS.maxRunning)
|
|
2163
|
+
throw new Error(`Too many running jobs (limit ${JOB_LIMITS.maxRunning}). Stop one with the job tool's stop action first.`);
|
|
2164
|
+
const jobId = `job-${state.next++}`;
|
|
2165
|
+
const record = {
|
|
2166
|
+
jobId,
|
|
2167
|
+
title: args.title,
|
|
2168
|
+
command: args.command,
|
|
2169
|
+
cwd,
|
|
2170
|
+
status: "running",
|
|
2171
|
+
startedAt: Date.now(),
|
|
2172
|
+
endedAt: null,
|
|
2173
|
+
exitCode: null,
|
|
2174
|
+
...args.timeoutSeconds === undefined ? {} : { timeoutSeconds: args.timeoutSeconds },
|
|
2175
|
+
runnerId: 0
|
|
2176
|
+
};
|
|
2177
|
+
allocated = jobId;
|
|
2178
|
+
entry = this.registry.ensure(conversationId, jobId);
|
|
2179
|
+
entry.windowOpen = true;
|
|
2180
|
+
record.runnerId = await tx.createTask(this.#runner, {
|
|
2181
|
+
jobId,
|
|
2182
|
+
command: args.command,
|
|
2183
|
+
cwd,
|
|
2184
|
+
...args.timeoutSeconds === undefined ? {} : { timeoutSeconds: args.timeoutSeconds }
|
|
2185
|
+
}, { ownership: { kind: "conversation" }, conversationId, background: true, abandonOnRestart: true });
|
|
2186
|
+
state.items.push(record);
|
|
2187
|
+
return plain(record);
|
|
2188
|
+
});
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
if (allocated)
|
|
2191
|
+
this.registry.delete(conversationId, allocated);
|
|
2192
|
+
throw error;
|
|
2193
|
+
}
|
|
2194
|
+
const live = entry;
|
|
2195
|
+
await Promise.race([live.exited, delay(JOB_LIMITS.startWindowMs, context.abortSignal)]);
|
|
2196
|
+
live.windowOpen = false;
|
|
2197
|
+
if (live.suppressReport)
|
|
2198
|
+
await live.exited;
|
|
2199
|
+
const job = (await io.state())?.items.find((item) => item.jobId === created.jobId) ?? created;
|
|
2200
|
+
const read = live.buffer.read();
|
|
2201
|
+
return this.#render(job, read.text, read.offset, read.dropped);
|
|
2202
|
+
}
|
|
2203
|
+
#render(job, text, offset, dropped) {
|
|
2204
|
+
const lines = [`Job ${job.jobId} ${describe(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
|
|
2205
|
+
if (dropped > 0)
|
|
2206
|
+
lines.push(`(${dropped} bytes of earlier output were dropped)`);
|
|
2207
|
+
lines.push(text ? `Output (next offset ${offset}):
|
|
2208
|
+
${text}` : `No new output (next offset ${offset}).`);
|
|
2209
|
+
return lines.join(`
|
|
2210
|
+
`);
|
|
2211
|
+
}
|
|
2212
|
+
async output(io, conversationId, jobId, since) {
|
|
2213
|
+
const job = await this.#find(io, jobId);
|
|
2214
|
+
const output = this.read(conversationId, job, since);
|
|
2215
|
+
return this.#render(job, output.text, output.offset, output.dropped);
|
|
2216
|
+
}
|
|
2217
|
+
async readOutput(io, conversationId, jobId, since) {
|
|
2218
|
+
const job = await this.#find(io, jobId);
|
|
2219
|
+
const output = this.read(conversationId, job, since);
|
|
2220
|
+
return { job: summarizeJobRecord(job, output.offset), output };
|
|
2221
|
+
}
|
|
2222
|
+
async#find(io, jobId) {
|
|
2223
|
+
const job = (await io.state())?.items.find((item) => item.jobId === jobId);
|
|
2224
|
+
if (!job)
|
|
2225
|
+
throw new Error(`Unknown job ${jobId}.`);
|
|
2226
|
+
return job;
|
|
2227
|
+
}
|
|
2228
|
+
async list(io, conversationId) {
|
|
2229
|
+
return ((await io.state())?.items ?? []).map((job) => summarizeJobRecord(job, this.registry.get(conversationId, job.jobId)?.buffer.totalBytes ?? job.totalBytes ?? 0));
|
|
2230
|
+
}
|
|
2231
|
+
read(conversationId, job, since) {
|
|
2232
|
+
const entry = isTerminalJob(job.status) ? undefined : this.registry.get(conversationId, job.jobId);
|
|
2233
|
+
return (entry?.buffer ?? OutputBuffer.fromTail(job.tail ?? "", job.totalBytes ?? 0)).read(since);
|
|
2234
|
+
}
|
|
2235
|
+
async stop(io, conversationId, jobId, origin, context) {
|
|
2236
|
+
let live;
|
|
2237
|
+
const found = await io.commit(async (tx) => {
|
|
2238
|
+
live = undefined;
|
|
2239
|
+
const job = (await tx.doc(JobsDoc, conversationId)).items.find((item) => item.jobId === jobId);
|
|
2240
|
+
if (!job)
|
|
2241
|
+
throw new Error(`Unknown job ${jobId}.`);
|
|
2242
|
+
if (isTerminalJob(job.status))
|
|
2243
|
+
return { job: plain(job) };
|
|
2244
|
+
job.stopOrigin ??= origin;
|
|
2245
|
+
live = this.registry.get(conversationId, jobId);
|
|
2246
|
+
if (live)
|
|
2247
|
+
return { job: plain(job) };
|
|
2248
|
+
const settled = await settleJob(tx, conversationId, jobId, { status: "stopped", exitCode: null }, { text: job.tail ?? "", totalBytes: job.totalBytes ?? 0 }, false, this.#reporter);
|
|
2249
|
+
return { job: plain(settled ?? job) };
|
|
2250
|
+
});
|
|
2251
|
+
const entry = live;
|
|
2252
|
+
if (entry) {
|
|
2253
|
+
entry.controller.abort();
|
|
2254
|
+
await Promise.race([entry.exited, delay(1e4, context?.abortSignal)]);
|
|
2255
|
+
return (await io.state())?.items.find((item) => item.jobId === jobId) ?? found.job;
|
|
2256
|
+
}
|
|
2257
|
+
return found.job;
|
|
2258
|
+
}
|
|
2259
|
+
async stopAll(io, conversationId, origin = "archive") {
|
|
2260
|
+
const running = ((await io.state())?.items ?? []).filter((item) => item.status === "running");
|
|
2261
|
+
for (const job of running)
|
|
2262
|
+
await this.stop(io, conversationId, job.jobId, origin);
|
|
2263
|
+
}
|
|
2264
|
+
shutdown() {
|
|
2265
|
+
this.#closing = true;
|
|
2266
|
+
this.registry.abortAll();
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// packages/core/src/prompt/invoke.ts
|
|
2271
|
+
import { dirname as dirname4 } from "path";
|
|
2272
|
+
|
|
2273
|
+
// packages/core/src/prompt/diagnostics.ts
|
|
2274
|
+
var ignorePromptDiagnostic = () => {};
|
|
2275
|
+
var warnPromptDiagnostic = ({ path, message }) => {
|
|
2276
|
+
console.warn(`[neko-prompt] ${JSON.stringify(path)}: ${message}`);
|
|
2277
|
+
};
|
|
2278
|
+
var isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
2279
|
+
|
|
2280
|
+
// packages/core/src/prompt/skills.ts
|
|
2281
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
2282
|
+
import { basename, dirname as dirname3, join as join3 } from "path";
|
|
2283
|
+
var MAX_NAME_LENGTH = 64;
|
|
2284
|
+
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
2285
|
+
var parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
|
|
2286
|
+
const skip = (message) => {
|
|
2287
|
+
report({ path: filePath, message });
|
|
2288
|
+
return null;
|
|
2289
|
+
};
|
|
2290
|
+
try {
|
|
2291
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
2292
|
+
const description = frontmatter.description;
|
|
2293
|
+
if (typeof description !== "string" || description.trim() === "")
|
|
2294
|
+
return skip("Skill description is missing or empty; skipped.");
|
|
2295
|
+
if (description.length > MAX_DESCRIPTION_LENGTH)
|
|
2296
|
+
return skip("Skill description is too long; skipped.");
|
|
2297
|
+
const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename(dirname3(filePath));
|
|
2298
|
+
if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
|
|
2299
|
+
return skip("Skill name is too long or contains whitespace/control characters; skipped.");
|
|
2300
|
+
return {
|
|
2301
|
+
name,
|
|
2302
|
+
description,
|
|
2303
|
+
filePath,
|
|
2304
|
+
disableModelInvocation: frontmatter["disable-model-invocation"] === true
|
|
2305
|
+
};
|
|
2306
|
+
} catch {
|
|
2307
|
+
return skip("Invalid skill frontmatter; skipped.");
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
var scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
|
|
2311
|
+
let entries;
|
|
2312
|
+
try {
|
|
2313
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
if (!isMissing(error))
|
|
2316
|
+
report({ path: dir, message: "Cannot scan skills directory; skipped." });
|
|
2317
|
+
return [];
|
|
2318
|
+
}
|
|
2319
|
+
const skills = [];
|
|
2320
|
+
for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
2321
|
+
if (!entry.isDirectory())
|
|
2322
|
+
continue;
|
|
2323
|
+
const filePath = join3(dir, entry.name, "SKILL.md");
|
|
2324
|
+
let content;
|
|
2325
|
+
try {
|
|
2326
|
+
content = await readFile2(filePath, "utf8");
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
if (!isMissing(error))
|
|
2329
|
+
report({ path: filePath, message: "Cannot read skill; skipped." });
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
const skill = parseSkillFile(content, filePath, report);
|
|
2333
|
+
if (skill) {
|
|
2334
|
+
skills.push(skill);
|
|
2335
|
+
capture?.(skill, content);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return skills;
|
|
2339
|
+
};
|
|
2340
|
+
var mergeSkills = (groups) => {
|
|
2341
|
+
const byName = new Map;
|
|
2342
|
+
for (const group of groups)
|
|
2343
|
+
for (const skill of group)
|
|
2344
|
+
byName.set(skill.name, skill);
|
|
2345
|
+
return [...byName.values()];
|
|
2346
|
+
};
|
|
2347
|
+
var escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2348
|
+
var formatSkillsForPrompt = (skills) => {
|
|
2349
|
+
const visible = skills.filter((skill) => !skill.disableModelInvocation);
|
|
2350
|
+
if (visible.length === 0)
|
|
2351
|
+
return "";
|
|
2352
|
+
const lines = [
|
|
2353
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
2354
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
2355
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.",
|
|
2356
|
+
"",
|
|
2357
|
+
"<available_skills>"
|
|
2358
|
+
];
|
|
2359
|
+
for (const skill of visible) {
|
|
2360
|
+
lines.push(" <skill>");
|
|
2361
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
2362
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
2363
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
2364
|
+
lines.push(" </skill>");
|
|
2365
|
+
}
|
|
2366
|
+
lines.push("</available_skills>");
|
|
2367
|
+
return lines.join(`
|
|
2368
|
+
`);
|
|
2369
|
+
};
|
|
2370
|
+
|
|
2371
|
+
// packages/core/src/prompt/invoke.ts
|
|
2372
|
+
var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
|
|
2373
|
+
if (!text.startsWith("/skill:"))
|
|
2374
|
+
return text;
|
|
2375
|
+
const spaceIndex = text.indexOf(" ");
|
|
2376
|
+
const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
|
2377
|
+
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
2378
|
+
const skill = skills.find((s) => s.name === skillName);
|
|
2379
|
+
if (!skill)
|
|
2380
|
+
return text;
|
|
2381
|
+
let body;
|
|
2382
|
+
try {
|
|
2383
|
+
body = stripFrontmatter(await readFile(skill.filePath)).trim();
|
|
2384
|
+
} catch {
|
|
2385
|
+
report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
|
|
2386
|
+
return text;
|
|
2387
|
+
}
|
|
2388
|
+
metadata?.({
|
|
2389
|
+
name: skill.name,
|
|
2390
|
+
location: skill.filePath,
|
|
2391
|
+
body: `References are relative to ${dirname4(skill.filePath)}.
|
|
2392
|
+
|
|
2393
|
+
${body}`,
|
|
2394
|
+
args
|
|
2395
|
+
});
|
|
2396
|
+
const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
|
|
2397
|
+
References are relative to ${dirname4(skill.filePath)}.
|
|
2398
|
+
|
|
2399
|
+
${body}
|
|
2400
|
+
</skill>`;
|
|
2401
|
+
return args ? `${block}
|
|
2402
|
+
|
|
2403
|
+
${args}` : block;
|
|
2404
|
+
};
|
|
2405
|
+
|
|
1847
2406
|
// packages/core/src/prompt/prompt.ts
|
|
2407
|
+
import { defineExtension as defineExtension5, section } from "@earendil-works/pi-durable";
|
|
1848
2408
|
var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
|
|
1849
2409
|
var PREAMBLE = "You are an expert coding assistant operating inside neko, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
1850
2410
|
var TOOL_GUIDELINES = {
|
|
@@ -1866,6 +2426,9 @@ var TOOL_GUIDELINES = {
|
|
|
1866
2426
|
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."
|
|
1867
2427
|
],
|
|
1868
2428
|
bash: ["Use bash for file operations like ls, rg, find"],
|
|
2429
|
+
job: [
|
|
2430
|
+
'Use job {action:"start", command, title?, timeout?} for long-running commands (dev servers, watchers, long builds or tests) instead of bash, which would block. Jobs have no stdin. start returns within 2 seconds, with the early result if the command already ended; a job that ends later reports back to you automatically, so do not poll. Read new output with {action:"output", jobId, since?} using the offset from the previous call, list jobs with {action:"list"}, end one with {action:"stop", jobId}. Run short commands with bash.'
|
|
2431
|
+
],
|
|
1869
2432
|
web_search: ["Use web_search for current information from the internet instead of guessing."],
|
|
1870
2433
|
web_extract: ["Use web_extract to read the full content of a web page when you have its URL."]
|
|
1871
2434
|
};
|
|
@@ -1905,7 +2468,7 @@ var createNekoPrompt = (loadResources) => {
|
|
|
1905
2468
|
};
|
|
1906
2469
|
return sections;
|
|
1907
2470
|
};
|
|
1908
|
-
return
|
|
2471
|
+
return defineExtension5({
|
|
1909
2472
|
name: "neko-prompt",
|
|
1910
2473
|
sections: KEYS.map((key) => section(key, async (input, context) => {
|
|
1911
2474
|
let pending = built.get(input);
|
|
@@ -2057,13 +2620,13 @@ var createResourceSnapshots = (load, submission) => {
|
|
|
2057
2620
|
import { stripVTControlCharacters } from "util";
|
|
2058
2621
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
2059
2622
|
import {
|
|
2060
|
-
defineDoc as
|
|
2061
|
-
defineExtension as
|
|
2062
|
-
defineTask as
|
|
2623
|
+
defineDoc as defineDoc3,
|
|
2624
|
+
defineExtension as defineExtension6,
|
|
2625
|
+
defineTask as defineTask3,
|
|
2063
2626
|
InboxDoc,
|
|
2064
2627
|
LiveDoc as LiveDoc2
|
|
2065
2628
|
} from "@earendil-works/pi-durable";
|
|
2066
|
-
var SessionMetadataDoc =
|
|
2629
|
+
var SessionMetadataDoc = defineDoc3({
|
|
2067
2630
|
kind: "app.neko.session-metadata",
|
|
2068
2631
|
version: 1,
|
|
2069
2632
|
scope: "conversation",
|
|
@@ -2126,7 +2689,7 @@ function mergeUsage(base, extra) {
|
|
|
2126
2689
|
function namingTask() {
|
|
2127
2690
|
let active = 0;
|
|
2128
2691
|
const waits = new Set;
|
|
2129
|
-
return
|
|
2692
|
+
return defineTask3({
|
|
2130
2693
|
name: "app.neko.name",
|
|
2131
2694
|
version: 1,
|
|
2132
2695
|
initial: () => ({ phase: "wait", until: Date.now() + 100 }),
|
|
@@ -2250,7 +2813,7 @@ class SessionMetadataManager {
|
|
|
2250
2813
|
this.storage = storage;
|
|
2251
2814
|
this.context = context;
|
|
2252
2815
|
this.report = report;
|
|
2253
|
-
registry.install(
|
|
2816
|
+
registry.install(defineExtension6({ name: "neko-naming", tasks: [this.task] }));
|
|
2254
2817
|
this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
|
|
2255
2818
|
const ids = new Set;
|
|
2256
2819
|
const submissions = new Map;
|
|
@@ -2394,14 +2957,14 @@ import { randomUUID } from "crypto";
|
|
|
2394
2957
|
import { realpath } from "fs/promises";
|
|
2395
2958
|
import {
|
|
2396
2959
|
configure as configure2,
|
|
2397
|
-
defineDoc as
|
|
2398
|
-
defineExtension as
|
|
2960
|
+
defineDoc as defineDoc4,
|
|
2961
|
+
defineExtension as defineExtension7,
|
|
2399
2962
|
hook as hook2,
|
|
2400
2963
|
ToolTask as ToolTask2,
|
|
2401
2964
|
wrapTool
|
|
2402
2965
|
} from "@earendil-works/pi-durable";
|
|
2403
2966
|
import { createBashTool } from "@earendil-works/pi-durable/tools";
|
|
2404
|
-
var ApprovalsDoc =
|
|
2967
|
+
var ApprovalsDoc = defineDoc4({
|
|
2405
2968
|
kind: "app.neko.approvals",
|
|
2406
2969
|
version: 1,
|
|
2407
2970
|
scope: "conversation",
|
|
@@ -2509,7 +3072,7 @@ class ToolApprovals {
|
|
|
2509
3072
|
}
|
|
2510
3073
|
}
|
|
2511
3074
|
function createApprovalExtension(service) {
|
|
2512
|
-
return
|
|
3075
|
+
return defineExtension7({
|
|
2513
3076
|
name: "neko-tool-approvals",
|
|
2514
3077
|
hooks: [
|
|
2515
3078
|
hook2(ToolTask2, {
|
|
@@ -2573,8 +3136,8 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
|
|
|
2573
3136
|
}
|
|
2574
3137
|
|
|
2575
3138
|
// packages/core/src/web-search.ts
|
|
2576
|
-
import { Type as
|
|
2577
|
-
import { defineExtension as
|
|
3139
|
+
import { Type as Type4 } from "@earendil-works/pi-ai";
|
|
3140
|
+
import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
2578
3141
|
var SEARCH_URL = "https://api.tavily.com/search";
|
|
2579
3142
|
var EXTRACT_URL = "https://api.tavily.com/extract";
|
|
2580
3143
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
@@ -2692,15 +3255,15 @@ function urlKey(value) {
|
|
|
2692
3255
|
}
|
|
2693
3256
|
}
|
|
2694
3257
|
function createWebSearchTools(resolveKey, fetchImpl = fetch) {
|
|
2695
|
-
return
|
|
3258
|
+
return defineExtension8({
|
|
2696
3259
|
name: "neko-web-search",
|
|
2697
3260
|
tools: [
|
|
2698
|
-
|
|
3261
|
+
defineTool4({
|
|
2699
3262
|
name: "web_search",
|
|
2700
3263
|
description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
|
|
2701
|
-
parameters:
|
|
2702
|
-
query:
|
|
2703
|
-
maxResults:
|
|
3264
|
+
parameters: Type4.Object({
|
|
3265
|
+
query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
|
|
3266
|
+
maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
|
|
2704
3267
|
}),
|
|
2705
3268
|
async execute({ query, maxResults }, _api, context) {
|
|
2706
3269
|
try {
|
|
@@ -2725,11 +3288,11 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
2725
3288
|
}
|
|
2726
3289
|
}
|
|
2727
3290
|
}),
|
|
2728
|
-
|
|
3291
|
+
defineTool4({
|
|
2729
3292
|
name: "web_extract",
|
|
2730
3293
|
description: "Extract bounded markdown excerpts from web pages by URL. Every requested URL has a result or failure entry; long content is truncated.",
|
|
2731
|
-
parameters:
|
|
2732
|
-
urls:
|
|
3294
|
+
parameters: Type4.Object({
|
|
3295
|
+
urls: Type4.Array(Type4.String({ maxLength: 2048, description: "http(s) URL" }), {
|
|
2733
3296
|
minItems: 1,
|
|
2734
3297
|
maxItems: MAX_URLS,
|
|
2735
3298
|
description: `URLs to extract (1-${MAX_URLS})`
|
|
@@ -2776,7 +3339,7 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
2776
3339
|
// packages/core/src/workspace-files.ts
|
|
2777
3340
|
import { realpath as realpath2, stat } from "fs/promises";
|
|
2778
3341
|
import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
|
|
2779
|
-
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as
|
|
3342
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
|
|
2780
3343
|
import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
|
|
2781
3344
|
import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
|
|
2782
3345
|
function relativeQuery(query) {
|
|
@@ -2942,7 +3505,7 @@ class WorkspaceFiles {
|
|
|
2942
3505
|
for (const user of [...current.users])
|
|
2943
3506
|
this.#closeWatcher(user);
|
|
2944
3507
|
}
|
|
2945
|
-
},
|
|
3508
|
+
}, withAbortSignal3(current.controller.signal, BACKGROUND_CONTEXT2));
|
|
2946
3509
|
if (result.ok) {
|
|
2947
3510
|
current.watcher = result.value;
|
|
2948
3511
|
if (current.controller.signal.aborted)
|
|
@@ -2970,7 +3533,7 @@ class WorkspaceFiles {
|
|
|
2970
3533
|
async#scan(item) {
|
|
2971
3534
|
const controller = new AbortController;
|
|
2972
3535
|
item.controller = controller;
|
|
2973
|
-
const context =
|
|
3536
|
+
const context = withAbortSignal3(controller.signal, BACKGROUND_CONTEXT2);
|
|
2974
3537
|
while (this.#active >= this.limits.concurrent) {
|
|
2975
3538
|
await new Promise((resolve, reject) => {
|
|
2976
3539
|
const wake = () => {
|
|
@@ -3136,7 +3699,7 @@ function cancellable(work, signal) {
|
|
|
3136
3699
|
// packages/core/src/runtime.ts
|
|
3137
3700
|
var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
|
|
3138
3701
|
var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
|
|
3139
|
-
var ArchivedDoc =
|
|
3702
|
+
var ArchivedDoc = defineDoc5({
|
|
3140
3703
|
kind: "app.neko.conversation",
|
|
3141
3704
|
version: 1,
|
|
3142
3705
|
scope: "conversation",
|
|
@@ -3144,7 +3707,7 @@ var ArchivedDoc = defineDoc4({
|
|
|
3144
3707
|
fork: "initial",
|
|
3145
3708
|
initial: () => ({ archived: false })
|
|
3146
3709
|
});
|
|
3147
|
-
var WorkspacesDoc =
|
|
3710
|
+
var WorkspacesDoc = defineDoc5({
|
|
3148
3711
|
kind: "app.neko.workspaces",
|
|
3149
3712
|
version: 1,
|
|
3150
3713
|
scope: "conversation",
|
|
@@ -3152,7 +3715,7 @@ var WorkspacesDoc = defineDoc4({
|
|
|
3152
3715
|
fork: "initial",
|
|
3153
3716
|
initial: () => ({ items: [] })
|
|
3154
3717
|
});
|
|
3155
|
-
var ActivityDoc =
|
|
3718
|
+
var ActivityDoc = defineDoc5({
|
|
3156
3719
|
kind: "app.neko.activity",
|
|
3157
3720
|
version: 1,
|
|
3158
3721
|
scope: "conversation",
|
|
@@ -3210,6 +3773,8 @@ async function createNekoRuntime(options = {}) {
|
|
|
3210
3773
|
registry.install(createSubagentExtension(async (id, requestId, cwd) => {
|
|
3211
3774
|
resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
|
|
3212
3775
|
}, resourceSnapshots.discard));
|
|
3776
|
+
const jobs = new JobManager;
|
|
3777
|
+
registry.install(jobs.extension);
|
|
3213
3778
|
for (const extension of options.extensions ?? [])
|
|
3214
3779
|
registry.install(extension);
|
|
3215
3780
|
let approvals;
|
|
@@ -3273,6 +3838,10 @@ async function createNekoRuntime(options = {}) {
|
|
|
3273
3838
|
if (target)
|
|
3274
3839
|
await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
|
|
3275
3840
|
}
|
|
3841
|
+
const jobIo = (id) => ({
|
|
3842
|
+
commit: (change) => harness.commit(change, context),
|
|
3843
|
+
state: () => harness.snapshot(JobsDoc, id, context)
|
|
3844
|
+
});
|
|
3276
3845
|
let lastActivity = 0;
|
|
3277
3846
|
for (const record of existing)
|
|
3278
3847
|
lastActivity = Math.max(lastActivity, (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0);
|
|
@@ -3295,6 +3864,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3295
3864
|
const cleanup = () => {
|
|
3296
3865
|
if (!closing)
|
|
3297
3866
|
closing = (async () => {
|
|
3867
|
+
jobs.shutdown();
|
|
3298
3868
|
await workspaceFiles.close();
|
|
3299
3869
|
await metadata.close();
|
|
3300
3870
|
})();
|
|
@@ -3427,6 +3997,16 @@ async function createNekoRuntime(options = {}) {
|
|
|
3427
3997
|
compactionRecord() {
|
|
3428
3998
|
return readCompaction(harness, storage, target.id, context);
|
|
3429
3999
|
},
|
|
4000
|
+
jobs() {
|
|
4001
|
+
return jobs.list(jobIo(target.id), target.id);
|
|
4002
|
+
},
|
|
4003
|
+
jobOutput(jobId, since) {
|
|
4004
|
+
return jobs.readOutput(jobIo(target.id), target.id, jobId, since);
|
|
4005
|
+
},
|
|
4006
|
+
async stopJob(jobId) {
|
|
4007
|
+
const job = await jobs.stop(jobIo(target.id), target.id, jobId, "user", context);
|
|
4008
|
+
return summarizeJobRecord(job);
|
|
4009
|
+
},
|
|
3430
4010
|
async compact(instructions) {
|
|
3431
4011
|
const agent = await target.agent(context);
|
|
3432
4012
|
if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
|
|
@@ -3618,6 +4198,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3618
4198
|
getConversation,
|
|
3619
4199
|
async archiveConversation(id) {
|
|
3620
4200
|
await (await sessionFor(id)).abort();
|
|
4201
|
+
await jobs.stopAll(jobIo(id), id);
|
|
3621
4202
|
await metadata.archive(id);
|
|
3622
4203
|
await harness.commit(async (tx) => {
|
|
3623
4204
|
(await tx.doc(ArchivedDoc, id)).archived = true;
|
|
@@ -3759,7 +4340,7 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
3759
4340
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3760
4341
|
|
|
3761
4342
|
// packages/core/src/application-version.ts
|
|
3762
|
-
var RUNNING_VERSION = "0.3.
|
|
4343
|
+
var RUNNING_VERSION = "0.3.3";
|
|
3763
4344
|
var APPLICATION_PROTOCOL = 4;
|
|
3764
4345
|
function stableVersion(value) {
|
|
3765
4346
|
return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
|
|
@@ -3909,11 +4490,11 @@ class UpdateChecker {
|
|
|
3909
4490
|
}
|
|
3910
4491
|
|
|
3911
4492
|
// packages/gateway/src/server-services.ts
|
|
3912
|
-
import { Type as
|
|
4493
|
+
import { Type as Type8 } from "typebox";
|
|
3913
4494
|
|
|
3914
4495
|
// packages/gateway/src/contract.ts
|
|
3915
4496
|
import { method, remote } from "@earendil-works/chord";
|
|
3916
|
-
import { Type as
|
|
4497
|
+
import { Type as Type5 } from "typebox";
|
|
3917
4498
|
var rpc = (args, result) => [args, result];
|
|
3918
4499
|
function contract(name, def) {
|
|
3919
4500
|
const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
|
|
@@ -3925,19 +4506,19 @@ function contract(name, def) {
|
|
|
3925
4506
|
]));
|
|
3926
4507
|
return remote(name, { ...def, variants });
|
|
3927
4508
|
}
|
|
3928
|
-
var str =
|
|
3929
|
-
var num =
|
|
3930
|
-
var bool =
|
|
3931
|
-
var nil =
|
|
3932
|
-
var integer =
|
|
3933
|
-
var nullableString =
|
|
3934
|
-
var phase =
|
|
3935
|
-
var level =
|
|
3936
|
-
var thinking =
|
|
3937
|
-
var model =
|
|
3938
|
-
var change =
|
|
3939
|
-
var usage =
|
|
3940
|
-
var image =
|
|
4509
|
+
var str = Type5.String();
|
|
4510
|
+
var num = Type5.Number();
|
|
4511
|
+
var bool = Type5.Boolean();
|
|
4512
|
+
var nil = Type5.Null();
|
|
4513
|
+
var integer = Type5.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
4514
|
+
var nullableString = Type5.Union([str, nil]);
|
|
4515
|
+
var phase = Type5.Enum(["pending", "running", "done"]);
|
|
4516
|
+
var level = Type5.Enum(["project", "user", "builtin"]);
|
|
4517
|
+
var thinking = Type5.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
4518
|
+
var model = Type5.Object({ provider: str, modelId: str });
|
|
4519
|
+
var change = Type5.Object({ model: Type5.Optional(model), thinkingLevel: Type5.Optional(thinking) }, { additionalProperties: false });
|
|
4520
|
+
var usage = Type5.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
|
|
4521
|
+
var image = Type5.Object({
|
|
3941
4522
|
name: str,
|
|
3942
4523
|
mimeType: str,
|
|
3943
4524
|
width: integer,
|
|
@@ -3946,8 +4527,8 @@ var image = Type4.Object({
|
|
|
3946
4527
|
entryId: integer,
|
|
3947
4528
|
contentIndex: integer
|
|
3948
4529
|
});
|
|
3949
|
-
var skill =
|
|
3950
|
-
var call =
|
|
4530
|
+
var skill = Type5.Object({ name: str, location: str, body: str, args: str });
|
|
4531
|
+
var call = Type5.Object({
|
|
3951
4532
|
callId: str,
|
|
3952
4533
|
name: str,
|
|
3953
4534
|
summary: str,
|
|
@@ -3955,94 +4536,116 @@ var call = Type4.Object({
|
|
|
3955
4536
|
phase,
|
|
3956
4537
|
output: nullableString,
|
|
3957
4538
|
diff: nullableString,
|
|
3958
|
-
diffAdded:
|
|
3959
|
-
diffRemoved:
|
|
3960
|
-
diffTruncated:
|
|
4539
|
+
diffAdded: Type5.Union([num, nil]),
|
|
4540
|
+
diffRemoved: Type5.Union([num, nil]),
|
|
4541
|
+
diffTruncated: Type5.Optional(bool)
|
|
3961
4542
|
});
|
|
3962
|
-
var DelegationSchema =
|
|
4543
|
+
var DelegationSchema = Type5.Object({
|
|
3963
4544
|
parentId: integer,
|
|
3964
|
-
agentId:
|
|
3965
|
-
round:
|
|
3966
|
-
runnerId:
|
|
3967
|
-
inputEntryId:
|
|
3968
|
-
answerEntryId:
|
|
3969
|
-
roundUsage:
|
|
4545
|
+
agentId: Type5.Optional(str),
|
|
4546
|
+
round: Type5.Optional(integer),
|
|
4547
|
+
runnerId: Type5.Optional(integer),
|
|
4548
|
+
inputEntryId: Type5.Optional(integer),
|
|
4549
|
+
answerEntryId: Type5.Optional(integer),
|
|
4550
|
+
roundUsage: Type5.Optional(Type5.Union([usage, nil])),
|
|
3970
4551
|
taskId: integer,
|
|
3971
4552
|
callId: str,
|
|
3972
4553
|
childId: integer,
|
|
3973
|
-
role:
|
|
4554
|
+
role: Type5.Enum(["explore", "plan", "coder"]),
|
|
3974
4555
|
title: str,
|
|
3975
4556
|
createdAt: num,
|
|
3976
|
-
startedAt:
|
|
3977
|
-
endedAt:
|
|
3978
|
-
status:
|
|
4557
|
+
startedAt: Type5.Union([num, nil]),
|
|
4558
|
+
endedAt: Type5.Union([num, nil]),
|
|
4559
|
+
status: Type5.Enum(["queued", "running", "completed", "failed", "cancelled"]),
|
|
3979
4560
|
report: nullableString,
|
|
3980
|
-
approvalCount:
|
|
3981
|
-
historyThrough:
|
|
3982
|
-
historyUsage:
|
|
4561
|
+
approvalCount: Type5.Optional(integer),
|
|
4562
|
+
historyThrough: Type5.Optional(integer),
|
|
4563
|
+
historyUsage: Type5.Optional(usage)
|
|
3983
4564
|
});
|
|
3984
|
-
var SubagentSummarySchema =
|
|
4565
|
+
var SubagentSummarySchema = Type5.Object({
|
|
3985
4566
|
agentId: str,
|
|
3986
4567
|
parentId: integer,
|
|
3987
4568
|
childId: integer,
|
|
3988
|
-
role:
|
|
4569
|
+
role: Type5.Enum(["explore", "plan", "coder"]),
|
|
3989
4570
|
title: str,
|
|
3990
|
-
status:
|
|
3991
|
-
activeRoundId:
|
|
4571
|
+
status: Type5.Enum(["idle", "queued", "running"]),
|
|
4572
|
+
activeRoundId: Type5.Union([integer, nil]),
|
|
3992
4573
|
lastRoundId: integer,
|
|
3993
|
-
lastReport:
|
|
4574
|
+
lastReport: Type5.Optional(nullableString),
|
|
3994
4575
|
roundCount: integer,
|
|
3995
4576
|
taskTitle: str,
|
|
3996
4577
|
approvalCount: integer,
|
|
3997
4578
|
createdAt: num,
|
|
3998
|
-
historyThrough:
|
|
3999
|
-
historyUsage:
|
|
4579
|
+
historyThrough: Type5.Optional(integer),
|
|
4580
|
+
historyUsage: Type5.Optional(usage)
|
|
4000
4581
|
});
|
|
4001
|
-
var
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4582
|
+
var jobStatus = Type5.Enum(["running", "exited", "failed", "stopped", "lost"]);
|
|
4583
|
+
var JobSummarySchema = Type5.Object({
|
|
4584
|
+
jobId: str,
|
|
4585
|
+
title: str,
|
|
4586
|
+
command: str,
|
|
4587
|
+
cwd: str,
|
|
4588
|
+
status: jobStatus,
|
|
4589
|
+
startedAt: num,
|
|
4590
|
+
endedAt: Type5.Union([num, nil]),
|
|
4591
|
+
exitCode: Type5.Union([num, nil]),
|
|
4592
|
+
reason: Type5.Optional(Type5.Enum(["timeout", "spawn-error", "restart", "aborted"])),
|
|
4593
|
+
timeoutSeconds: Type5.Optional(num),
|
|
4594
|
+
outputBytes: integer
|
|
4595
|
+
});
|
|
4596
|
+
var JobOutputSchema = Type5.Object({
|
|
4597
|
+
job: JobSummarySchema,
|
|
4598
|
+
text: str,
|
|
4599
|
+
offset: integer,
|
|
4600
|
+
truncated: bool,
|
|
4601
|
+
dropped: integer
|
|
4602
|
+
});
|
|
4603
|
+
var ChatViewSchema = Type5.Object({
|
|
4604
|
+
historyBefore: Type5.Optional(Type5.Union([integer, nil])),
|
|
4605
|
+
agents: Type5.Optional(Type5.Array(SubagentSummarySchema)),
|
|
4606
|
+
jobs: Type5.Optional(Type5.Array(JobSummarySchema)),
|
|
4607
|
+
rounds: Type5.Optional(Type5.Array(DelegationSchema)),
|
|
4608
|
+
subagents: Type5.Optional(Type5.Array(DelegationSchema)),
|
|
4609
|
+
taskUsage: Type5.Optional(usage),
|
|
4007
4610
|
model: str,
|
|
4008
|
-
agent:
|
|
4611
|
+
agent: Type5.Object({ model: Type5.Union([model, nil]), thinkingLevel: Type5.Union([thinking, nil]) }),
|
|
4009
4612
|
cwd: str,
|
|
4010
4613
|
busy: bool,
|
|
4011
4614
|
status: nullableString,
|
|
4012
4615
|
usage,
|
|
4013
|
-
contextUsage:
|
|
4014
|
-
compaction:
|
|
4015
|
-
failure:
|
|
4016
|
-
active:
|
|
4616
|
+
contextUsage: Type5.Optional(Type5.Object({ tokens: Type5.Union([num, nil]), window: num, percent: Type5.Union([num, nil]) })),
|
|
4617
|
+
compaction: Type5.Optional(Type5.Object({
|
|
4618
|
+
failure: Type5.Optional(Type5.Object({ taskId: integer, reason: Type5.Enum(["manual", "threshold", "overflow"]), error: str })),
|
|
4619
|
+
active: Type5.Array(Type5.Object({
|
|
4017
4620
|
taskId: integer,
|
|
4018
|
-
reason:
|
|
4621
|
+
reason: Type5.Enum(["manual", "threshold", "overflow"]),
|
|
4019
4622
|
blocking: bool,
|
|
4020
4623
|
attempt: integer,
|
|
4021
|
-
retry:
|
|
4624
|
+
retry: Type5.Optional(Type5.Object({ at: num, error: str }))
|
|
4022
4625
|
})),
|
|
4023
|
-
latest:
|
|
4626
|
+
latest: Type5.Union([
|
|
4024
4627
|
nil,
|
|
4025
|
-
|
|
4628
|
+
Type5.Object({
|
|
4026
4629
|
taskId: integer,
|
|
4027
|
-
phase:
|
|
4630
|
+
phase: Type5.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
|
|
4028
4631
|
at: num
|
|
4029
4632
|
})
|
|
4030
4633
|
])
|
|
4031
4634
|
})),
|
|
4032
|
-
items:
|
|
4033
|
-
|
|
4034
|
-
type:
|
|
4035
|
-
order:
|
|
4635
|
+
items: Type5.Array(Type5.Union([
|
|
4636
|
+
Type5.Object({
|
|
4637
|
+
type: Type5.Literal("user"),
|
|
4638
|
+
order: Type5.Optional(num),
|
|
4036
4639
|
key: str,
|
|
4037
4640
|
id: integer,
|
|
4038
4641
|
text: str,
|
|
4039
|
-
images:
|
|
4040
|
-
skill:
|
|
4041
|
-
defaultImagePrompt:
|
|
4642
|
+
images: Type5.Optional(Type5.Array(image)),
|
|
4643
|
+
skill: Type5.Optional(skill),
|
|
4644
|
+
defaultImagePrompt: Type5.Optional(Type5.Object({ version: Type5.Integer({ minimum: 1 }), text: str }))
|
|
4042
4645
|
}),
|
|
4043
|
-
|
|
4044
|
-
type:
|
|
4045
|
-
order:
|
|
4646
|
+
Type5.Object({
|
|
4647
|
+
type: Type5.Literal("assistant"),
|
|
4648
|
+
order: Type5.Optional(num),
|
|
4046
4649
|
key: str,
|
|
4047
4650
|
id: integer,
|
|
4048
4651
|
thinking: str,
|
|
@@ -4051,11 +4654,11 @@ var ChatViewSchema = Type4.Object({
|
|
|
4051
4654
|
errorMessage: nullableString,
|
|
4052
4655
|
note: nullableString
|
|
4053
4656
|
}),
|
|
4054
|
-
|
|
4055
|
-
type:
|
|
4056
|
-
order:
|
|
4657
|
+
Type5.Object({
|
|
4658
|
+
type: Type5.Literal("tool"),
|
|
4659
|
+
order: Type5.Optional(num),
|
|
4057
4660
|
key: str,
|
|
4058
|
-
images:
|
|
4661
|
+
images: Type5.Optional(Type5.Array(image)),
|
|
4059
4662
|
callId: str,
|
|
4060
4663
|
name: str,
|
|
4061
4664
|
summary: str,
|
|
@@ -4064,54 +4667,61 @@ var ChatViewSchema = Type4.Object({
|
|
|
4064
4667
|
isError: bool,
|
|
4065
4668
|
output: nullableString,
|
|
4066
4669
|
result: nullableString,
|
|
4067
|
-
durationMs:
|
|
4670
|
+
durationMs: Type5.Union([num, nil]),
|
|
4068
4671
|
note: nullableString,
|
|
4069
4672
|
diff: nullableString,
|
|
4070
|
-
diffAdded:
|
|
4071
|
-
diffRemoved:
|
|
4072
|
-
diffTruncated:
|
|
4673
|
+
diffAdded: Type5.Union([num, nil]),
|
|
4674
|
+
diffRemoved: Type5.Union([num, nil]),
|
|
4675
|
+
diffTruncated: Type5.Optional(bool)
|
|
4073
4676
|
}),
|
|
4074
|
-
|
|
4677
|
+
Type5.Object({
|
|
4678
|
+
type: Type5.Literal("system"),
|
|
4679
|
+
order: Type5.Optional(num),
|
|
4680
|
+
key: str,
|
|
4681
|
+
id: integer,
|
|
4682
|
+
text: str,
|
|
4683
|
+
jobId: Type5.Optional(str)
|
|
4684
|
+
})
|
|
4075
4685
|
])),
|
|
4076
|
-
streaming:
|
|
4686
|
+
streaming: Type5.Union([nil, Type5.Object({ thinking: str, text: str, toolCalls: Type5.Array(call) })])
|
|
4077
4687
|
});
|
|
4078
|
-
var SessionSummarySchema =
|
|
4688
|
+
var SessionSummarySchema = Type5.Object({
|
|
4079
4689
|
workspaceRoot: str,
|
|
4080
4690
|
lastUsedAt: num,
|
|
4081
4691
|
title: str,
|
|
4082
4692
|
firstPromptPreview: str,
|
|
4083
4693
|
createdAt: num,
|
|
4084
|
-
lastMessageAt:
|
|
4085
|
-
model:
|
|
4086
|
-
status:
|
|
4694
|
+
lastMessageAt: Type5.Union([num, nil]),
|
|
4695
|
+
model: Type5.Union([model, nil]),
|
|
4696
|
+
status: Type5.Enum(["idle", "running", "error"]),
|
|
4087
4697
|
id: str
|
|
4088
4698
|
});
|
|
4089
|
-
var DirectoryStateSchema =
|
|
4090
|
-
var WorkspaceSummarySchema =
|
|
4091
|
-
var WorkspaceStateSchema =
|
|
4092
|
-
var credentialStatus =
|
|
4699
|
+
var DirectoryStateSchema = Type5.Object({ revision: integer, sessions: Type5.Array(SessionSummarySchema) });
|
|
4700
|
+
var WorkspaceSummarySchema = Type5.Object({ path: str, createdAt: num });
|
|
4701
|
+
var WorkspaceStateSchema = Type5.Object({ revision: integer, workspaces: Type5.Array(WorkspaceSummarySchema) });
|
|
4702
|
+
var credentialStatus = Type5.Object({
|
|
4093
4703
|
savedConfigured: bool,
|
|
4094
4704
|
environmentConfigured: bool,
|
|
4095
|
-
source:
|
|
4705
|
+
source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
|
|
4096
4706
|
});
|
|
4097
|
-
var CredentialStateSchema =
|
|
4707
|
+
var CredentialStateSchema = Type5.Object({
|
|
4098
4708
|
revision: integer,
|
|
4099
|
-
statuses:
|
|
4709
|
+
statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
|
|
4100
4710
|
});
|
|
4101
|
-
var AttachmentSchema =
|
|
4102
|
-
var chatInput =
|
|
4711
|
+
var AttachmentSchema = Type5.Object({ sessionId: str, attachmentId: str });
|
|
4712
|
+
var chatInput = Type5.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
|
|
4103
4713
|
var NekoSessionDirectory = contract("neko.session-directory", {
|
|
4104
4714
|
version: 1,
|
|
4105
|
-
input:
|
|
4715
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4106
4716
|
variants: { ready: { state: { state: DirectoryStateSchema } } }
|
|
4107
4717
|
});
|
|
4108
4718
|
var NekoSessionManagement = contract("neko.session-management", {
|
|
4109
4719
|
version: 1,
|
|
4110
|
-
input:
|
|
4720
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4111
4721
|
variants: {
|
|
4112
4722
|
ready: {
|
|
4113
4723
|
methods: {
|
|
4114
|
-
create: rpc(
|
|
4724
|
+
create: rpc(Type5.Object({ ...change.properties, cwd: Type5.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
|
|
4115
4725
|
archive: rpc(str, nil),
|
|
4116
4726
|
attach: rpc(str, AttachmentSchema),
|
|
4117
4727
|
detach: rpc(nil, nil),
|
|
@@ -4120,30 +4730,30 @@ var NekoSessionManagement = contract("neko.session-management", {
|
|
|
4120
4730
|
}
|
|
4121
4731
|
}
|
|
4122
4732
|
});
|
|
4123
|
-
var resize =
|
|
4124
|
-
maxWidth:
|
|
4125
|
-
maxHeight:
|
|
4126
|
-
maxBytes:
|
|
4127
|
-
jpegQuality:
|
|
4733
|
+
var resize = Type5.Object({
|
|
4734
|
+
maxWidth: Type5.Optional(integer),
|
|
4735
|
+
maxHeight: Type5.Optional(integer),
|
|
4736
|
+
maxBytes: Type5.Optional(integer),
|
|
4737
|
+
jpegQuality: Type5.Optional(integer)
|
|
4128
4738
|
});
|
|
4129
|
-
var inputLimits =
|
|
4130
|
-
maxRequestBytes:
|
|
4131
|
-
images:
|
|
4132
|
-
resize:
|
|
4133
|
-
maxPerMessage:
|
|
4134
|
-
maxPerRequest:
|
|
4739
|
+
var inputLimits = Type5.Object({
|
|
4740
|
+
maxRequestBytes: Type5.Optional(integer),
|
|
4741
|
+
images: Type5.Optional(Type5.Object({
|
|
4742
|
+
resize: Type5.Optional(resize),
|
|
4743
|
+
maxPerMessage: Type5.Optional(integer),
|
|
4744
|
+
maxPerRequest: Type5.Optional(integer)
|
|
4135
4745
|
}))
|
|
4136
4746
|
});
|
|
4137
|
-
var modelInfo =
|
|
4747
|
+
var modelInfo = Type5.Object({
|
|
4138
4748
|
provider: str,
|
|
4139
4749
|
modelId: str,
|
|
4140
4750
|
name: str,
|
|
4141
4751
|
contextWindow: num,
|
|
4142
|
-
input:
|
|
4143
|
-
inputLimits:
|
|
4752
|
+
input: Type5.Array(str),
|
|
4753
|
+
inputLimits: Type5.Optional(inputLimits),
|
|
4144
4754
|
reasoning: bool,
|
|
4145
|
-
supportedThinkingLevels:
|
|
4146
|
-
thinkingLevelAdjustments:
|
|
4755
|
+
supportedThinkingLevels: Type5.Array(thinking),
|
|
4756
|
+
thinkingLevelAdjustments: Type5.Object({
|
|
4147
4757
|
off: thinking,
|
|
4148
4758
|
minimal: thinking,
|
|
4149
4759
|
low: thinking,
|
|
@@ -4155,31 +4765,31 @@ var modelInfo = Type4.Object({
|
|
|
4155
4765
|
});
|
|
4156
4766
|
var NekoModels = contract("neko.models", {
|
|
4157
4767
|
version: 1,
|
|
4158
|
-
input:
|
|
4159
|
-
variants: { ready: { methods: { list: rpc(nil,
|
|
4768
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4769
|
+
variants: { ready: { methods: { list: rpc(nil, Type5.Array(modelInfo)) } } }
|
|
4160
4770
|
});
|
|
4161
|
-
var imageChunk =
|
|
4771
|
+
var imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
|
|
4162
4772
|
var NekoSubagent = contract("neko.subagent", {
|
|
4163
4773
|
version: 1,
|
|
4164
|
-
input:
|
|
4774
|
+
input: Type5.Object({
|
|
4165
4775
|
sessionId: str,
|
|
4166
4776
|
attachmentId: str,
|
|
4167
|
-
taskId:
|
|
4168
|
-
agentId:
|
|
4777
|
+
taskId: Type5.Optional(integer),
|
|
4778
|
+
agentId: Type5.Optional(str)
|
|
4169
4779
|
}),
|
|
4170
4780
|
variants: {
|
|
4171
4781
|
ready: {
|
|
4172
4782
|
state: { revision: integer },
|
|
4173
4783
|
methods: {
|
|
4174
|
-
chunk: rpc(
|
|
4175
|
-
kind:
|
|
4176
|
-
before:
|
|
4177
|
-
token:
|
|
4784
|
+
chunk: rpc(Type5.Object({
|
|
4785
|
+
kind: Type5.Enum(["view", "history"]),
|
|
4786
|
+
before: Type5.Optional(integer),
|
|
4787
|
+
token: Type5.Optional(integer),
|
|
4178
4788
|
offset: integer
|
|
4179
|
-
}),
|
|
4180
|
-
getImage: rpc(
|
|
4181
|
-
decideApproval: rpc(
|
|
4182
|
-
stop: rpc(
|
|
4789
|
+
}), Type5.Object({ token: integer, data: str, next: Type5.Union([integer, nil]) })),
|
|
4790
|
+
getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
|
|
4791
|
+
decideApproval: rpc(Type5.Object({ id: integer, version: str, decision: Type5.Enum(["allowed", "denied"]) }), Type5.Enum(["pending", "allowed", "denied", "cancelled"])),
|
|
4792
|
+
stop: rpc(Type5.Union([nil, Type5.Object({ taskId: integer })]), nil)
|
|
4183
4793
|
}
|
|
4184
4794
|
}
|
|
4185
4795
|
}
|
|
@@ -4191,79 +4801,81 @@ var NekoChat = contract("neko.chat", {
|
|
|
4191
4801
|
ready: {
|
|
4192
4802
|
state: { view: ChatViewSchema },
|
|
4193
4803
|
methods: {
|
|
4194
|
-
submit: rpc(
|
|
4804
|
+
submit: rpc(Type5.Object({
|
|
4195
4805
|
content: str,
|
|
4196
|
-
images:
|
|
4197
|
-
requestId:
|
|
4198
|
-
whenBusy:
|
|
4806
|
+
images: Type5.Optional(Type5.Array(str, { maxItems: 4 })),
|
|
4807
|
+
requestId: Type5.Optional(str),
|
|
4808
|
+
whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
|
|
4199
4809
|
}, { additionalProperties: false }), nil),
|
|
4200
4810
|
abort: rpc(nil, nil),
|
|
4201
4811
|
usage: rpc(nil, usage),
|
|
4202
|
-
skills: rpc(nil,
|
|
4812
|
+
skills: rpc(nil, Type5.Array(Type5.Object({ name: str, description: str, location: str, level }))),
|
|
4813
|
+
jobOutput: rpc(Type5.Object({ jobId: str, since: Type5.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
|
|
4814
|
+
jobStop: rpc(Type5.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
|
|
4203
4815
|
configure: rpc(change, nil),
|
|
4204
|
-
compact: rpc(
|
|
4205
|
-
importImage: rpc(str,
|
|
4206
|
-
draftImage: rpc(
|
|
4207
|
-
beginImage: rpc(
|
|
4208
|
-
appendImage: rpc(
|
|
4816
|
+
compact: rpc(Type5.Object({ instructions: Type5.Optional(str) }, { additionalProperties: false }), nil),
|
|
4817
|
+
importImage: rpc(str, Type5.Object({ id: str, name: str })),
|
|
4818
|
+
draftImage: rpc(Type5.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
|
|
4819
|
+
beginImage: rpc(Type5.Object({ name: str, mimeType: str, size: Type5.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
|
|
4820
|
+
appendImage: rpc(Type5.Object({ id: str, offset: integer, data: Type5.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
|
|
4209
4821
|
finishImage: rpc(str, nil),
|
|
4210
4822
|
releaseImage: rpc(str, nil),
|
|
4211
|
-
getImage: rpc(
|
|
4823
|
+
getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
|
|
4212
4824
|
}
|
|
4213
4825
|
}
|
|
4214
4826
|
}
|
|
4215
4827
|
});
|
|
4216
|
-
var browse =
|
|
4217
|
-
var page =
|
|
4828
|
+
var browse = Type5.Object({ path: str, query: Type5.Optional(str), showHidden: Type5.Optional(bool), cursor: Type5.Optional(str) }, { additionalProperties: false });
|
|
4829
|
+
var page = Type5.Object({
|
|
4218
4830
|
path: str,
|
|
4219
4831
|
parent: nullableString,
|
|
4220
|
-
entries:
|
|
4832
|
+
entries: Type5.Array(Type5.Object({ name: str, path: str, error: Type5.Optional(str) })),
|
|
4221
4833
|
next: nullableString
|
|
4222
4834
|
});
|
|
4223
4835
|
var NekoWorkspaces = contract("neko.workspaces", {
|
|
4224
4836
|
version: 1,
|
|
4225
|
-
input:
|
|
4837
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4226
4838
|
variants: {
|
|
4227
4839
|
ready: {
|
|
4228
4840
|
state: { state: WorkspaceStateSchema },
|
|
4229
4841
|
methods: {
|
|
4230
|
-
add: rpc(
|
|
4842
|
+
add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
|
|
4231
4843
|
browse: rpc(browse, page)
|
|
4232
4844
|
}
|
|
4233
4845
|
}
|
|
4234
4846
|
}
|
|
4235
4847
|
});
|
|
4236
|
-
var provider =
|
|
4848
|
+
var provider = Type5.Enum(["deepseek", "tavily"]);
|
|
4237
4849
|
var NekoCredentials = contract("neko.credentials", {
|
|
4238
4850
|
version: 1,
|
|
4239
|
-
input:
|
|
4851
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4240
4852
|
variants: {
|
|
4241
4853
|
ready: {
|
|
4242
4854
|
state: { state: CredentialStateSchema },
|
|
4243
4855
|
methods: {
|
|
4244
|
-
save: rpc(
|
|
4245
|
-
remove: rpc(
|
|
4856
|
+
save: rpc(Type5.Object({ provider, key: Type5.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
|
|
4857
|
+
remove: rpc(Type5.Object({ provider }, { additionalProperties: false }), nil)
|
|
4246
4858
|
}
|
|
4247
4859
|
}
|
|
4248
4860
|
}
|
|
4249
4861
|
});
|
|
4250
|
-
var FileCandidatesSchema =
|
|
4251
|
-
entries:
|
|
4862
|
+
var FileCandidatesSchema = Type5.Object({
|
|
4863
|
+
entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
|
|
4252
4864
|
limited: bool,
|
|
4253
4865
|
incomplete: bool
|
|
4254
4866
|
});
|
|
4255
4867
|
var NekoWorkspaceFiles = contract("neko.workspace-files", {
|
|
4256
4868
|
version: 1,
|
|
4257
|
-
input:
|
|
4869
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4258
4870
|
variants: {
|
|
4259
4871
|
ready: {
|
|
4260
4872
|
methods: {
|
|
4261
|
-
search: rpc(
|
|
4873
|
+
search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
|
|
4262
4874
|
}
|
|
4263
4875
|
}
|
|
4264
4876
|
}
|
|
4265
4877
|
});
|
|
4266
|
-
var marketSkill =
|
|
4878
|
+
var marketSkill = Type5.Object({
|
|
4267
4879
|
name: str,
|
|
4268
4880
|
description: str,
|
|
4269
4881
|
category: str,
|
|
@@ -4273,38 +4885,38 @@ var marketSkill = Type4.Object({
|
|
|
4273
4885
|
sha256: str,
|
|
4274
4886
|
sizeBytes: integer,
|
|
4275
4887
|
uploadedAt: str,
|
|
4276
|
-
installed:
|
|
4277
|
-
activeLevel:
|
|
4888
|
+
installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
|
|
4889
|
+
activeLevel: Type5.Union([level, nil])
|
|
4278
4890
|
});
|
|
4279
|
-
var preview =
|
|
4280
|
-
var scope =
|
|
4281
|
-
var installResult =
|
|
4891
|
+
var preview = Type5.Object({ body: str, files: Type5.Array(str) });
|
|
4892
|
+
var scope = Type5.Enum(["user", "project"]);
|
|
4893
|
+
var installResult = Type5.Object({ name: str, scope, path: str, shadowedBy: Type5.Optional(scope) });
|
|
4282
4894
|
var NekoSkillMarket = contract("neko.skill-market", {
|
|
4283
4895
|
version: 1,
|
|
4284
|
-
input:
|
|
4896
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4285
4897
|
variants: {
|
|
4286
4898
|
ready: {
|
|
4287
4899
|
methods: {
|
|
4288
|
-
list: rpc(
|
|
4289
|
-
detail: rpc(
|
|
4290
|
-
preview: rpc(
|
|
4291
|
-
install: rpc(
|
|
4292
|
-
uninstall: rpc(
|
|
4900
|
+
list: rpc(Type5.Object({ query: Type5.Optional(str), workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), Type5.Array(marketSkill)),
|
|
4901
|
+
detail: rpc(Type5.Object({ name: str, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), marketSkill),
|
|
4902
|
+
preview: rpc(Type5.Object({ name: str }, { additionalProperties: false }), preview),
|
|
4903
|
+
install: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str), overwrite: Type5.Optional(bool) }, { additionalProperties: false }), installResult),
|
|
4904
|
+
uninstall: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), nil)
|
|
4293
4905
|
}
|
|
4294
4906
|
}
|
|
4295
4907
|
}
|
|
4296
4908
|
});
|
|
4297
|
-
var ApplicationInfoSchema =
|
|
4909
|
+
var ApplicationInfoSchema = Type5.Object({
|
|
4298
4910
|
runningVersion: str,
|
|
4299
4911
|
installedVersion: nullableString,
|
|
4300
|
-
protocolVersion:
|
|
4912
|
+
protocolVersion: Type5.Integer({ minimum: 1 }),
|
|
4301
4913
|
latestVersion: nullableString,
|
|
4302
|
-
checkedAt:
|
|
4303
|
-
checkState:
|
|
4914
|
+
checkedAt: Type5.Union([integer, nil]),
|
|
4915
|
+
checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"])
|
|
4304
4916
|
});
|
|
4305
4917
|
var NekoApplicationInfo = contract("neko.application-info", {
|
|
4306
4918
|
version: 1,
|
|
4307
|
-
input:
|
|
4919
|
+
input: Type5.Object({}),
|
|
4308
4920
|
variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
|
|
4309
4921
|
});
|
|
4310
4922
|
|
|
@@ -4320,9 +4932,9 @@ import {
|
|
|
4320
4932
|
opaque as opaque2,
|
|
4321
4933
|
resource as resource2
|
|
4322
4934
|
} from "@earendil-works/chord";
|
|
4323
|
-
import { withAbortSignal as
|
|
4935
|
+
import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
|
|
4324
4936
|
import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
|
|
4325
|
-
import { Type as
|
|
4937
|
+
import { Type as Type7 } from "typebox";
|
|
4326
4938
|
|
|
4327
4939
|
// packages/gateway/src/image-uploads.ts
|
|
4328
4940
|
var IMAGE_CHUNK_BYTES = 256 * 1024;
|
|
@@ -4429,7 +5041,7 @@ import {
|
|
|
4429
5041
|
resource
|
|
4430
5042
|
} from "@earendil-works/chord";
|
|
4431
5043
|
import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
|
|
4432
|
-
import { Type as
|
|
5044
|
+
import { Type as Type6 } from "typebox";
|
|
4433
5045
|
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
4434
5046
|
const child = await runtime.harness.conversation(childId, context);
|
|
4435
5047
|
if (!child)
|
|
@@ -4554,15 +5166,15 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4554
5166
|
}
|
|
4555
5167
|
var ChildProjection = local("neko.subagent-projection", {
|
|
4556
5168
|
version: 1,
|
|
4557
|
-
input:
|
|
5169
|
+
input: Type6.Object({ parentId: Type6.Integer(), agentId: Type6.String() }),
|
|
4558
5170
|
members: {
|
|
4559
|
-
revision:
|
|
5171
|
+
revision: Type6.Integer(),
|
|
4560
5172
|
reader: opaque("neko.subagent-reader@1")
|
|
4561
5173
|
}
|
|
4562
5174
|
});
|
|
4563
5175
|
function installSubagentHost(host, runtime, context) {
|
|
4564
5176
|
host.serve(ChildProjection, (input, _principal, self) => {
|
|
4565
|
-
const { revision } = self.state({ revision: cell(
|
|
5177
|
+
const { revision } = self.state({ revision: cell(Type6.Integer(), 0) });
|
|
4566
5178
|
const { reader } = self.resources({
|
|
4567
5179
|
reader: resource({
|
|
4568
5180
|
rev: 1,
|
|
@@ -4604,7 +5216,7 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4604
5216
|
};
|
|
4605
5217
|
const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
|
|
4606
5218
|
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
|
|
4607
|
-
const { authorizedRevision } = self.state({ authorizedRevision: cell(
|
|
5219
|
+
const { authorizedRevision } = self.state({ authorizedRevision: cell(Type6.Integer(), 0) });
|
|
4608
5220
|
self.listen({
|
|
4609
5221
|
authorizedView: listener(shared.revision, async (value, ctx) => {
|
|
4610
5222
|
try {
|
|
@@ -4659,7 +5271,7 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4659
5271
|
}
|
|
4660
5272
|
|
|
4661
5273
|
// packages/gateway/src/session-host.ts
|
|
4662
|
-
var OwnerInput =
|
|
5274
|
+
var OwnerInput = Type7.Object({});
|
|
4663
5275
|
var Owner = local2("neko.attachment-owner", {
|
|
4664
5276
|
version: 1,
|
|
4665
5277
|
input: OwnerInput,
|
|
@@ -4667,7 +5279,7 @@ var Owner = local2("neko.attachment-owner", {
|
|
|
4667
5279
|
});
|
|
4668
5280
|
var Projection = local2("neko.projection", {
|
|
4669
5281
|
version: 1,
|
|
4670
|
-
input:
|
|
5282
|
+
input: Type7.Object({ id: Type7.String() }),
|
|
4671
5283
|
members: {
|
|
4672
5284
|
view: ChatViewSchema,
|
|
4673
5285
|
reader: opaque2("neko.projection-reader@1")
|
|
@@ -4683,6 +5295,8 @@ var emptyView = {
|
|
|
4683
5295
|
items: [],
|
|
4684
5296
|
streaming: null
|
|
4685
5297
|
};
|
|
5298
|
+
var JOB_COMMAND_PREVIEW_CHARS = 1000;
|
|
5299
|
+
var clipJob = (job) => job.command.length > JOB_COMMAND_PREVIEW_CHARS ? { ...job, command: `${job.command.slice(0, JOB_COMMAND_PREVIEW_CHARS)}\u2026` } : job;
|
|
4686
5300
|
async function openProjection(runtime, session, context) {
|
|
4687
5301
|
const view = await session.viewState();
|
|
4688
5302
|
const history = new Map;
|
|
@@ -4698,7 +5312,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4698
5312
|
const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
|
|
4699
5313
|
let childIds = new Set;
|
|
4700
5314
|
const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4701
|
-
if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
|
|
5315
|
+
if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents", "app.neko.jobs"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
|
|
4702
5316
|
changed?.(null);
|
|
4703
5317
|
});
|
|
4704
5318
|
return {
|
|
@@ -4724,6 +5338,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4724
5338
|
}, naming)
|
|
4725
5339
|
}
|
|
4726
5340
|
}, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
|
|
5341
|
+
const jobs = (await session.jobs()).map(clipJob);
|
|
4727
5342
|
const subagents = await listDelegations(runtime.harness, session.id, context);
|
|
4728
5343
|
const approvals = await runtime.approvals.list(session.id, context);
|
|
4729
5344
|
for (const entry of subagents)
|
|
@@ -4738,6 +5353,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4738
5353
|
const u = totalUsage(aggregate);
|
|
4739
5354
|
return {
|
|
4740
5355
|
...projected,
|
|
5356
|
+
jobs,
|
|
4741
5357
|
agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
|
|
4742
5358
|
subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
|
|
4743
5359
|
...entry,
|
|
@@ -4909,7 +5525,7 @@ function serveSessions(host, runtime, context) {
|
|
|
4909
5525
|
getImage: async (input, ctx) => {
|
|
4910
5526
|
if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
|
|
4911
5527
|
throw new ChordError2("invalid", "Invalid image range");
|
|
4912
|
-
const entry = await shared.reader.entry(input.entryId,
|
|
5528
|
+
const entry = await shared.reader.entry(input.entryId, withAbortSignal4(ctx.signal, context));
|
|
4913
5529
|
const content = entry?.model?.[0]?.content;
|
|
4914
5530
|
const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
|
|
4915
5531
|
if (image?.type !== "image")
|
|
@@ -4946,6 +5562,21 @@ function serveSessions(host, runtime, context) {
|
|
|
4946
5562
|
return null;
|
|
4947
5563
|
},
|
|
4948
5564
|
skills: () => runtime.listSkills(projection.value.session.id),
|
|
5565
|
+
jobOutput: async ({ jobId, since }) => {
|
|
5566
|
+
try {
|
|
5567
|
+
const { job, output } = await projection.value.session.jobOutput(jobId, since);
|
|
5568
|
+
return { job: clipJob(job), ...output };
|
|
5569
|
+
} catch (error) {
|
|
5570
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot read job output");
|
|
5571
|
+
}
|
|
5572
|
+
},
|
|
5573
|
+
jobStop: async ({ jobId }) => {
|
|
5574
|
+
try {
|
|
5575
|
+
return clipJob(await projection.value.session.stopJob(jobId));
|
|
5576
|
+
} catch (error) {
|
|
5577
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot stop job");
|
|
5578
|
+
}
|
|
5579
|
+
},
|
|
4949
5580
|
usage: async () => {
|
|
4950
5581
|
const usage = totalUsage(await projection.value.session.usage());
|
|
4951
5582
|
return {
|
|
@@ -5680,12 +6311,12 @@ async function listDrives() {
|
|
|
5680
6311
|
// packages/gateway/src/server-services.ts
|
|
5681
6312
|
var ServerState = local3("neko.server-state", {
|
|
5682
6313
|
version: 1,
|
|
5683
|
-
input:
|
|
6314
|
+
input: Type8.Object({}),
|
|
5684
6315
|
members: {
|
|
5685
6316
|
directory: DirectoryStateSchema,
|
|
5686
6317
|
workspaces: WorkspaceStateSchema,
|
|
5687
6318
|
credentials: CredentialStateSchema,
|
|
5688
|
-
refreshCredentials: method2(
|
|
6319
|
+
refreshCredentials: method2(Type8.Null(), Type8.Null())
|
|
5689
6320
|
}
|
|
5690
6321
|
});
|
|
5691
6322
|
async function createNekoServerServices(options) {
|