neko-harness 0.3.1 → 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 +3 -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 +1061 -350
- package/dist/neko.js +4090 -693
- package/package.json +1 -1
- package/dist/client/assets/index-BTHbU-2p.css +0 -2
- package/dist/client/assets/index-O88LHrzD.js +0 -83
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,
|
|
@@ -560,6 +679,53 @@ function toolResultText(content) {
|
|
|
560
679
|
return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text ?? "").join(`
|
|
561
680
|
`);
|
|
562
681
|
}
|
|
682
|
+
var WRITE_DIFF_MAX_LINES = 2000;
|
|
683
|
+
var WRITE_DIFF_MAX_BYTES = 64 * 1024;
|
|
684
|
+
function countDiffLines(diff) {
|
|
685
|
+
let added = 0;
|
|
686
|
+
let removed = 0;
|
|
687
|
+
for (const line of diff.split(`
|
|
688
|
+
`)) {
|
|
689
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
690
|
+
added++;
|
|
691
|
+
else if (line.startsWith("-") && !line.startsWith("---"))
|
|
692
|
+
removed++;
|
|
693
|
+
}
|
|
694
|
+
return { added, removed };
|
|
695
|
+
}
|
|
696
|
+
function editResultDiff(details) {
|
|
697
|
+
if (typeof details !== "object" || details === null)
|
|
698
|
+
return null;
|
|
699
|
+
const diff = details.diff;
|
|
700
|
+
return typeof diff === "string" ? diff : null;
|
|
701
|
+
}
|
|
702
|
+
function synthesizedWriteDiff(args) {
|
|
703
|
+
const record = args ?? {};
|
|
704
|
+
if (typeof record.content !== "string")
|
|
705
|
+
return null;
|
|
706
|
+
const content = record.content;
|
|
707
|
+
const shown = [];
|
|
708
|
+
let offset = 0;
|
|
709
|
+
let bytes = 0;
|
|
710
|
+
while (offset < content.length && shown.length < WRITE_DIFF_MAX_LINES) {
|
|
711
|
+
const newline = content.indexOf(`
|
|
712
|
+
`, offset);
|
|
713
|
+
const end = newline === -1 ? content.length : newline;
|
|
714
|
+
const overhead = shown.length === 0 ? 2 : 3;
|
|
715
|
+
const remaining = WRITE_DIFF_MAX_BYTES - bytes - overhead;
|
|
716
|
+
if (end - offset > remaining)
|
|
717
|
+
break;
|
|
718
|
+
const line = content.slice(offset, end);
|
|
719
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
720
|
+
if (lineBytes > remaining)
|
|
721
|
+
break;
|
|
722
|
+
shown.push(`+ ${line}`);
|
|
723
|
+
bytes += lineBytes + overhead;
|
|
724
|
+
offset = newline === -1 ? content.length : end + 1;
|
|
725
|
+
}
|
|
726
|
+
return { diff: shown.join(`
|
|
727
|
+
`), added: shown.length, removed: 0, truncated: offset < content.length };
|
|
728
|
+
}
|
|
563
729
|
function totalUsage(state) {
|
|
564
730
|
const total = {
|
|
565
731
|
input: 0,
|
|
@@ -592,6 +758,8 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
592
758
|
if (message.role !== "toolResult")
|
|
593
759
|
continue;
|
|
594
760
|
const diagnostics = entry.data?.diagnostics ?? [];
|
|
761
|
+
const diff = message.toolName === "edit" ? editResultDiff(message.details) : null;
|
|
762
|
+
const counts = diff === null ? null : countDiffLines(diff);
|
|
595
763
|
results.set(message.toolCallId, {
|
|
596
764
|
type: "tool",
|
|
597
765
|
key: `t${message.toolCallId}`,
|
|
@@ -608,7 +776,10 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
608
776
|
].filter(Boolean).join(`
|
|
609
777
|
`),
|
|
610
778
|
durationMs: message.durationMs ?? null,
|
|
611
|
-
note: null
|
|
779
|
+
note: null,
|
|
780
|
+
diff,
|
|
781
|
+
diffAdded: counts?.added ?? null,
|
|
782
|
+
diffRemoved: counts?.removed ?? null
|
|
612
783
|
});
|
|
613
784
|
}
|
|
614
785
|
}
|
|
@@ -639,6 +810,17 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
639
810
|
const items = [];
|
|
640
811
|
for (const entry of view.entries) {
|
|
641
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
|
+
}
|
|
642
824
|
if (message?.role === "user") {
|
|
643
825
|
const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
|
|
644
826
|
items.push({
|
|
@@ -666,6 +848,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
666
848
|
} else {
|
|
667
849
|
const summary = summarizeToolCall(block.name, block.arguments);
|
|
668
850
|
const slot = live.tools?.find((s) => s.callId === block.id);
|
|
851
|
+
const synthesized = block.name === "write" ? synthesizedWriteDiff(block.arguments) : null;
|
|
669
852
|
items.push({
|
|
670
853
|
type: "tool",
|
|
671
854
|
key: `t${block.id}`,
|
|
@@ -678,7 +861,11 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
678
861
|
output: slot?.output ?? null,
|
|
679
862
|
result: null,
|
|
680
863
|
durationMs: null,
|
|
681
|
-
note: ran ? null : "Not run: response interrupted."
|
|
864
|
+
note: ran ? null : "Not run: response interrupted.",
|
|
865
|
+
diff: synthesized?.diff ?? null,
|
|
866
|
+
diffAdded: synthesized?.added ?? null,
|
|
867
|
+
diffRemoved: synthesized?.removed ?? null,
|
|
868
|
+
diffTruncated: synthesized?.truncated ?? false
|
|
682
869
|
});
|
|
683
870
|
}
|
|
684
871
|
}
|
|
@@ -726,7 +913,17 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
726
913
|
}
|
|
727
914
|
function withArgs(item, name, args) {
|
|
728
915
|
const summary = summarizeToolCall(name, args);
|
|
729
|
-
|
|
916
|
+
const synthesized = name === "write" ? synthesizedWriteDiff(args) : null;
|
|
917
|
+
return {
|
|
918
|
+
...item,
|
|
919
|
+
name,
|
|
920
|
+
summary: summary.args,
|
|
921
|
+
detail: summary.detail ?? null,
|
|
922
|
+
diff: synthesized?.diff ?? item.diff,
|
|
923
|
+
diffAdded: synthesized?.added ?? item.diffAdded,
|
|
924
|
+
diffRemoved: synthesized?.removed ?? item.diffRemoved,
|
|
925
|
+
diffTruncated: synthesized?.truncated ?? false
|
|
926
|
+
};
|
|
730
927
|
}
|
|
731
928
|
function assistantItem(message, id) {
|
|
732
929
|
let thinking = "";
|
|
@@ -779,13 +976,18 @@ ${block.text}`;
|
|
|
779
976
|
if (block.type === "toolCall") {
|
|
780
977
|
const slot = live.tools?.find((candidate) => candidate.callId === block.id);
|
|
781
978
|
const summary = summarizeToolCall(block.name, block.arguments);
|
|
979
|
+
const synthesized = block.name === "write" ? synthesizedWriteDiff(block.arguments) : null;
|
|
782
980
|
toolCalls.push({
|
|
783
981
|
callId: block.id,
|
|
784
982
|
name: block.name,
|
|
785
983
|
summary: summary.args,
|
|
786
984
|
detail: summary.detail ?? null,
|
|
787
985
|
phase: slot === undefined || slot.status === "pending" ? "pending" : slot.status,
|
|
788
|
-
output: slot?.status === "running" ? slot.output ?? null : null
|
|
986
|
+
output: slot?.status === "running" ? slot.output ?? null : null,
|
|
987
|
+
diff: synthesized?.diff ?? null,
|
|
988
|
+
diffAdded: synthesized?.added ?? null,
|
|
989
|
+
diffRemoved: synthesized?.removed ?? null,
|
|
990
|
+
diffTruncated: synthesized?.truncated ?? false
|
|
789
991
|
});
|
|
790
992
|
}
|
|
791
993
|
}
|
|
@@ -840,7 +1042,7 @@ import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
|
|
|
840
1042
|
import {
|
|
841
1043
|
AssistantEntry as AssistantEntry2,
|
|
842
1044
|
createRegistry,
|
|
843
|
-
defineDoc as
|
|
1045
|
+
defineDoc as defineDoc5,
|
|
844
1046
|
Harness,
|
|
845
1047
|
LiveDoc as importLiveDoc,
|
|
846
1048
|
MemoryStorage,
|
|
@@ -1079,150 +1281,20 @@ var ImageDetails = defineExtension2({
|
|
|
1079
1281
|
]
|
|
1080
1282
|
});
|
|
1081
1283
|
|
|
1082
|
-
// packages/core/src/
|
|
1083
|
-
import {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
};
|
|
1090
|
-
var isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
1091
|
-
|
|
1092
|
-
// packages/core/src/prompt/skills.ts
|
|
1093
|
-
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
1094
|
-
import { basename, dirname as dirname3, join as join3 } from "path";
|
|
1095
|
-
var MAX_NAME_LENGTH = 64;
|
|
1096
|
-
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
1097
|
-
var parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
|
|
1098
|
-
const skip = (message) => {
|
|
1099
|
-
report({ path: filePath, message });
|
|
1100
|
-
return null;
|
|
1101
|
-
};
|
|
1102
|
-
try {
|
|
1103
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
1104
|
-
const description = frontmatter.description;
|
|
1105
|
-
if (typeof description !== "string" || description.trim() === "")
|
|
1106
|
-
return skip("Skill description is missing or empty; skipped.");
|
|
1107
|
-
if (description.length > MAX_DESCRIPTION_LENGTH)
|
|
1108
|
-
return skip("Skill description is too long; skipped.");
|
|
1109
|
-
const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename(dirname3(filePath));
|
|
1110
|
-
if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
|
|
1111
|
-
return skip("Skill name is too long or contains whitespace/control characters; skipped.");
|
|
1112
|
-
return {
|
|
1113
|
-
name,
|
|
1114
|
-
description,
|
|
1115
|
-
filePath,
|
|
1116
|
-
disableModelInvocation: frontmatter["disable-model-invocation"] === true
|
|
1117
|
-
};
|
|
1118
|
-
} catch {
|
|
1119
|
-
return skip("Invalid skill frontmatter; skipped.");
|
|
1120
|
-
}
|
|
1121
|
-
};
|
|
1122
|
-
var scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
|
|
1123
|
-
let entries;
|
|
1124
|
-
try {
|
|
1125
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
1126
|
-
} catch (error) {
|
|
1127
|
-
if (!isMissing(error))
|
|
1128
|
-
report({ path: dir, message: "Cannot scan skills directory; skipped." });
|
|
1129
|
-
return [];
|
|
1130
|
-
}
|
|
1131
|
-
const skills = [];
|
|
1132
|
-
for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
1133
|
-
if (!entry.isDirectory())
|
|
1134
|
-
continue;
|
|
1135
|
-
const filePath = join3(dir, entry.name, "SKILL.md");
|
|
1136
|
-
let content;
|
|
1137
|
-
try {
|
|
1138
|
-
content = await readFile2(filePath, "utf8");
|
|
1139
|
-
} catch (error) {
|
|
1140
|
-
if (!isMissing(error))
|
|
1141
|
-
report({ path: filePath, message: "Cannot read skill; skipped." });
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
const skill = parseSkillFile(content, filePath, report);
|
|
1145
|
-
if (skill) {
|
|
1146
|
-
skills.push(skill);
|
|
1147
|
-
capture?.(skill, content);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
return skills;
|
|
1151
|
-
};
|
|
1152
|
-
var mergeSkills = (groups) => {
|
|
1153
|
-
const byName = new Map;
|
|
1154
|
-
for (const group of groups)
|
|
1155
|
-
for (const skill of group)
|
|
1156
|
-
byName.set(skill.name, skill);
|
|
1157
|
-
return [...byName.values()];
|
|
1158
|
-
};
|
|
1159
|
-
var escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1160
|
-
var formatSkillsForPrompt = (skills) => {
|
|
1161
|
-
const visible = skills.filter((skill) => !skill.disableModelInvocation);
|
|
1162
|
-
if (visible.length === 0)
|
|
1163
|
-
return "";
|
|
1164
|
-
const lines = [
|
|
1165
|
-
"The following skills provide specialized instructions for specific tasks.",
|
|
1166
|
-
"Use the read tool to load a skill's file when the task matches its description.",
|
|
1167
|
-
"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.",
|
|
1168
|
-
"",
|
|
1169
|
-
"<available_skills>"
|
|
1170
|
-
];
|
|
1171
|
-
for (const skill of visible) {
|
|
1172
|
-
lines.push(" <skill>");
|
|
1173
|
-
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
1174
|
-
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
1175
|
-
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
1176
|
-
lines.push(" </skill>");
|
|
1177
|
-
}
|
|
1178
|
-
lines.push("</available_skills>");
|
|
1179
|
-
return lines.join(`
|
|
1180
|
-
`);
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
// packages/core/src/prompt/invoke.ts
|
|
1184
|
-
var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
|
|
1185
|
-
if (!text.startsWith("/skill:"))
|
|
1186
|
-
return text;
|
|
1187
|
-
const spaceIndex = text.indexOf(" ");
|
|
1188
|
-
const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
|
1189
|
-
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
1190
|
-
const skill = skills.find((s) => s.name === skillName);
|
|
1191
|
-
if (!skill)
|
|
1192
|
-
return text;
|
|
1193
|
-
let body;
|
|
1194
|
-
try {
|
|
1195
|
-
body = stripFrontmatter(await readFile(skill.filePath)).trim();
|
|
1196
|
-
} catch {
|
|
1197
|
-
report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
|
|
1198
|
-
return text;
|
|
1199
|
-
}
|
|
1200
|
-
metadata?.({
|
|
1201
|
-
name: skill.name,
|
|
1202
|
-
location: skill.filePath,
|
|
1203
|
-
body: `References are relative to ${dirname4(skill.filePath)}.
|
|
1204
|
-
|
|
1205
|
-
${body}`,
|
|
1206
|
-
args
|
|
1207
|
-
});
|
|
1208
|
-
const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
|
|
1209
|
-
References are relative to ${dirname4(skill.filePath)}.
|
|
1210
|
-
|
|
1211
|
-
${body}
|
|
1212
|
-
</skill>`;
|
|
1213
|
-
return args ? `${block}
|
|
1214
|
-
|
|
1215
|
-
${args}` : block;
|
|
1216
|
-
};
|
|
1217
|
-
|
|
1218
|
-
// packages/core/src/prompt/prompt.ts
|
|
1219
|
-
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";
|
|
1220
1292
|
|
|
1221
1293
|
// packages/core/src/subagents.ts
|
|
1222
1294
|
import {
|
|
1223
1295
|
AssistantEntry,
|
|
1224
1296
|
configure,
|
|
1225
|
-
defineDoc,
|
|
1297
|
+
defineDoc as defineDoc2,
|
|
1226
1298
|
defineExtension as defineExtension3,
|
|
1227
1299
|
defineTask,
|
|
1228
1300
|
defineTool as defineTool2,
|
|
@@ -1276,7 +1348,7 @@ function subagentUsageTotals(state) {
|
|
|
1276
1348
|
|
|
1277
1349
|
// packages/core/src/subagents.ts
|
|
1278
1350
|
var agentIdFor = (childId) => `agent-${childId}`;
|
|
1279
|
-
var DelegationsDoc =
|
|
1351
|
+
var DelegationsDoc = defineDoc2({
|
|
1280
1352
|
kind: "app.neko.delegations",
|
|
1281
1353
|
version: 1,
|
|
1282
1354
|
scope: "conversation",
|
|
@@ -1284,7 +1356,7 @@ var DelegationsDoc = defineDoc({
|
|
|
1284
1356
|
fork: "initial",
|
|
1285
1357
|
initial: () => ({ items: [] })
|
|
1286
1358
|
});
|
|
1287
|
-
var SubagentsDoc =
|
|
1359
|
+
var SubagentsDoc = defineDoc2({
|
|
1288
1360
|
kind: "app.neko.subagents",
|
|
1289
1361
|
version: 1,
|
|
1290
1362
|
scope: "conversation",
|
|
@@ -1292,7 +1364,7 @@ var SubagentsDoc = defineDoc({
|
|
|
1292
1364
|
fork: "initial",
|
|
1293
1365
|
initial: () => ({ items: [] })
|
|
1294
1366
|
});
|
|
1295
|
-
var SubagentDoc =
|
|
1367
|
+
var SubagentDoc = defineDoc2({
|
|
1296
1368
|
kind: "app.neko.subagent",
|
|
1297
1369
|
version: 1,
|
|
1298
1370
|
scope: "conversation",
|
|
@@ -1300,7 +1372,7 @@ var SubagentDoc = defineDoc({
|
|
|
1300
1372
|
fork: "initial",
|
|
1301
1373
|
initial: () => ({ role: null, parentId: null })
|
|
1302
1374
|
});
|
|
1303
|
-
var SlotsDoc =
|
|
1375
|
+
var SlotsDoc = defineDoc2({
|
|
1304
1376
|
kind: "app.neko.subagent-slots",
|
|
1305
1377
|
version: 1,
|
|
1306
1378
|
scope: "session",
|
|
@@ -1631,7 +1703,7 @@ function createSubagentExtension(prepare, discard) {
|
|
|
1631
1703
|
thinkingLevel: agent.thinkingLevel,
|
|
1632
1704
|
cwd: agent.cwd,
|
|
1633
1705
|
extensions: agent.extensions.filter((e) => e.name !== extension.name),
|
|
1634
|
-
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"))
|
|
1635
1707
|
});
|
|
1636
1708
|
Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
|
|
1637
1709
|
identity = {
|
|
@@ -1772,7 +1844,567 @@ async function installSubagentCoordinator(harness, parents, context) {
|
|
|
1772
1844
|
harness.closed.finally(stop).catch(() => {});
|
|
1773
1845
|
}
|
|
1774
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
|
+
|
|
1775
2406
|
// packages/core/src/prompt/prompt.ts
|
|
2407
|
+
import { defineExtension as defineExtension5, section } from "@earendil-works/pi-durable";
|
|
1776
2408
|
var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
|
|
1777
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.";
|
|
1778
2410
|
var TOOL_GUIDELINES = {
|
|
@@ -1794,6 +2426,9 @@ var TOOL_GUIDELINES = {
|
|
|
1794
2426
|
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."
|
|
1795
2427
|
],
|
|
1796
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
|
+
],
|
|
1797
2432
|
web_search: ["Use web_search for current information from the internet instead of guessing."],
|
|
1798
2433
|
web_extract: ["Use web_extract to read the full content of a web page when you have its URL."]
|
|
1799
2434
|
};
|
|
@@ -1833,7 +2468,7 @@ var createNekoPrompt = (loadResources) => {
|
|
|
1833
2468
|
};
|
|
1834
2469
|
return sections;
|
|
1835
2470
|
};
|
|
1836
|
-
return
|
|
2471
|
+
return defineExtension5({
|
|
1837
2472
|
name: "neko-prompt",
|
|
1838
2473
|
sections: KEYS.map((key) => section(key, async (input, context) => {
|
|
1839
2474
|
let pending = built.get(input);
|
|
@@ -1985,13 +2620,13 @@ var createResourceSnapshots = (load, submission) => {
|
|
|
1985
2620
|
import { stripVTControlCharacters } from "util";
|
|
1986
2621
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1987
2622
|
import {
|
|
1988
|
-
defineDoc as
|
|
1989
|
-
defineExtension as
|
|
1990
|
-
defineTask as
|
|
2623
|
+
defineDoc as defineDoc3,
|
|
2624
|
+
defineExtension as defineExtension6,
|
|
2625
|
+
defineTask as defineTask3,
|
|
1991
2626
|
InboxDoc,
|
|
1992
2627
|
LiveDoc as LiveDoc2
|
|
1993
2628
|
} from "@earendil-works/pi-durable";
|
|
1994
|
-
var SessionMetadataDoc =
|
|
2629
|
+
var SessionMetadataDoc = defineDoc3({
|
|
1995
2630
|
kind: "app.neko.session-metadata",
|
|
1996
2631
|
version: 1,
|
|
1997
2632
|
scope: "conversation",
|
|
@@ -2054,7 +2689,7 @@ function mergeUsage(base, extra) {
|
|
|
2054
2689
|
function namingTask() {
|
|
2055
2690
|
let active = 0;
|
|
2056
2691
|
const waits = new Set;
|
|
2057
|
-
return
|
|
2692
|
+
return defineTask3({
|
|
2058
2693
|
name: "app.neko.name",
|
|
2059
2694
|
version: 1,
|
|
2060
2695
|
initial: () => ({ phase: "wait", until: Date.now() + 100 }),
|
|
@@ -2178,7 +2813,7 @@ class SessionMetadataManager {
|
|
|
2178
2813
|
this.storage = storage;
|
|
2179
2814
|
this.context = context;
|
|
2180
2815
|
this.report = report;
|
|
2181
|
-
registry.install(
|
|
2816
|
+
registry.install(defineExtension6({ name: "neko-naming", tasks: [this.task] }));
|
|
2182
2817
|
this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
|
|
2183
2818
|
const ids = new Set;
|
|
2184
2819
|
const submissions = new Map;
|
|
@@ -2322,14 +2957,14 @@ import { randomUUID } from "crypto";
|
|
|
2322
2957
|
import { realpath } from "fs/promises";
|
|
2323
2958
|
import {
|
|
2324
2959
|
configure as configure2,
|
|
2325
|
-
defineDoc as
|
|
2326
|
-
defineExtension as
|
|
2960
|
+
defineDoc as defineDoc4,
|
|
2961
|
+
defineExtension as defineExtension7,
|
|
2327
2962
|
hook as hook2,
|
|
2328
2963
|
ToolTask as ToolTask2,
|
|
2329
2964
|
wrapTool
|
|
2330
2965
|
} from "@earendil-works/pi-durable";
|
|
2331
2966
|
import { createBashTool } from "@earendil-works/pi-durable/tools";
|
|
2332
|
-
var ApprovalsDoc =
|
|
2967
|
+
var ApprovalsDoc = defineDoc4({
|
|
2333
2968
|
kind: "app.neko.approvals",
|
|
2334
2969
|
version: 1,
|
|
2335
2970
|
scope: "conversation",
|
|
@@ -2437,7 +3072,7 @@ class ToolApprovals {
|
|
|
2437
3072
|
}
|
|
2438
3073
|
}
|
|
2439
3074
|
function createApprovalExtension(service) {
|
|
2440
|
-
return
|
|
3075
|
+
return defineExtension7({
|
|
2441
3076
|
name: "neko-tool-approvals",
|
|
2442
3077
|
hooks: [
|
|
2443
3078
|
hook2(ToolTask2, {
|
|
@@ -2501,8 +3136,8 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
|
|
|
2501
3136
|
}
|
|
2502
3137
|
|
|
2503
3138
|
// packages/core/src/web-search.ts
|
|
2504
|
-
import { Type as
|
|
2505
|
-
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";
|
|
2506
3141
|
var SEARCH_URL = "https://api.tavily.com/search";
|
|
2507
3142
|
var EXTRACT_URL = "https://api.tavily.com/extract";
|
|
2508
3143
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
@@ -2620,15 +3255,15 @@ function urlKey(value) {
|
|
|
2620
3255
|
}
|
|
2621
3256
|
}
|
|
2622
3257
|
function createWebSearchTools(resolveKey, fetchImpl = fetch) {
|
|
2623
|
-
return
|
|
3258
|
+
return defineExtension8({
|
|
2624
3259
|
name: "neko-web-search",
|
|
2625
3260
|
tools: [
|
|
2626
|
-
|
|
3261
|
+
defineTool4({
|
|
2627
3262
|
name: "web_search",
|
|
2628
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.",
|
|
2629
|
-
parameters:
|
|
2630
|
-
query:
|
|
2631
|
-
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})` }))
|
|
2632
3267
|
}),
|
|
2633
3268
|
async execute({ query, maxResults }, _api, context) {
|
|
2634
3269
|
try {
|
|
@@ -2653,11 +3288,11 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
2653
3288
|
}
|
|
2654
3289
|
}
|
|
2655
3290
|
}),
|
|
2656
|
-
|
|
3291
|
+
defineTool4({
|
|
2657
3292
|
name: "web_extract",
|
|
2658
3293
|
description: "Extract bounded markdown excerpts from web pages by URL. Every requested URL has a result or failure entry; long content is truncated.",
|
|
2659
|
-
parameters:
|
|
2660
|
-
urls:
|
|
3294
|
+
parameters: Type4.Object({
|
|
3295
|
+
urls: Type4.Array(Type4.String({ maxLength: 2048, description: "http(s) URL" }), {
|
|
2661
3296
|
minItems: 1,
|
|
2662
3297
|
maxItems: MAX_URLS,
|
|
2663
3298
|
description: `URLs to extract (1-${MAX_URLS})`
|
|
@@ -2704,7 +3339,7 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
2704
3339
|
// packages/core/src/workspace-files.ts
|
|
2705
3340
|
import { realpath as realpath2, stat } from "fs/promises";
|
|
2706
3341
|
import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
|
|
2707
|
-
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as
|
|
3342
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
|
|
2708
3343
|
import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
|
|
2709
3344
|
import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
|
|
2710
3345
|
function relativeQuery(query) {
|
|
@@ -2870,7 +3505,7 @@ class WorkspaceFiles {
|
|
|
2870
3505
|
for (const user of [...current.users])
|
|
2871
3506
|
this.#closeWatcher(user);
|
|
2872
3507
|
}
|
|
2873
|
-
},
|
|
3508
|
+
}, withAbortSignal3(current.controller.signal, BACKGROUND_CONTEXT2));
|
|
2874
3509
|
if (result.ok) {
|
|
2875
3510
|
current.watcher = result.value;
|
|
2876
3511
|
if (current.controller.signal.aborted)
|
|
@@ -2898,7 +3533,7 @@ class WorkspaceFiles {
|
|
|
2898
3533
|
async#scan(item) {
|
|
2899
3534
|
const controller = new AbortController;
|
|
2900
3535
|
item.controller = controller;
|
|
2901
|
-
const context =
|
|
3536
|
+
const context = withAbortSignal3(controller.signal, BACKGROUND_CONTEXT2);
|
|
2902
3537
|
while (this.#active >= this.limits.concurrent) {
|
|
2903
3538
|
await new Promise((resolve, reject) => {
|
|
2904
3539
|
const wake = () => {
|
|
@@ -3064,7 +3699,7 @@ function cancellable(work, signal) {
|
|
|
3064
3699
|
// packages/core/src/runtime.ts
|
|
3065
3700
|
var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
|
|
3066
3701
|
var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
|
|
3067
|
-
var ArchivedDoc =
|
|
3702
|
+
var ArchivedDoc = defineDoc5({
|
|
3068
3703
|
kind: "app.neko.conversation",
|
|
3069
3704
|
version: 1,
|
|
3070
3705
|
scope: "conversation",
|
|
@@ -3072,7 +3707,7 @@ var ArchivedDoc = defineDoc4({
|
|
|
3072
3707
|
fork: "initial",
|
|
3073
3708
|
initial: () => ({ archived: false })
|
|
3074
3709
|
});
|
|
3075
|
-
var WorkspacesDoc =
|
|
3710
|
+
var WorkspacesDoc = defineDoc5({
|
|
3076
3711
|
kind: "app.neko.workspaces",
|
|
3077
3712
|
version: 1,
|
|
3078
3713
|
scope: "conversation",
|
|
@@ -3080,7 +3715,7 @@ var WorkspacesDoc = defineDoc4({
|
|
|
3080
3715
|
fork: "initial",
|
|
3081
3716
|
initial: () => ({ items: [] })
|
|
3082
3717
|
});
|
|
3083
|
-
var ActivityDoc =
|
|
3718
|
+
var ActivityDoc = defineDoc5({
|
|
3084
3719
|
kind: "app.neko.activity",
|
|
3085
3720
|
version: 1,
|
|
3086
3721
|
scope: "conversation",
|
|
@@ -3138,6 +3773,8 @@ async function createNekoRuntime(options = {}) {
|
|
|
3138
3773
|
registry.install(createSubagentExtension(async (id, requestId, cwd) => {
|
|
3139
3774
|
resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
|
|
3140
3775
|
}, resourceSnapshots.discard));
|
|
3776
|
+
const jobs = new JobManager;
|
|
3777
|
+
registry.install(jobs.extension);
|
|
3141
3778
|
for (const extension of options.extensions ?? [])
|
|
3142
3779
|
registry.install(extension);
|
|
3143
3780
|
let approvals;
|
|
@@ -3201,6 +3838,10 @@ async function createNekoRuntime(options = {}) {
|
|
|
3201
3838
|
if (target)
|
|
3202
3839
|
await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
|
|
3203
3840
|
}
|
|
3841
|
+
const jobIo = (id) => ({
|
|
3842
|
+
commit: (change) => harness.commit(change, context),
|
|
3843
|
+
state: () => harness.snapshot(JobsDoc, id, context)
|
|
3844
|
+
});
|
|
3204
3845
|
let lastActivity = 0;
|
|
3205
3846
|
for (const record of existing)
|
|
3206
3847
|
lastActivity = Math.max(lastActivity, (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0);
|
|
@@ -3223,6 +3864,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3223
3864
|
const cleanup = () => {
|
|
3224
3865
|
if (!closing)
|
|
3225
3866
|
closing = (async () => {
|
|
3867
|
+
jobs.shutdown();
|
|
3226
3868
|
await workspaceFiles.close();
|
|
3227
3869
|
await metadata.close();
|
|
3228
3870
|
})();
|
|
@@ -3355,6 +3997,16 @@ async function createNekoRuntime(options = {}) {
|
|
|
3355
3997
|
compactionRecord() {
|
|
3356
3998
|
return readCompaction(harness, storage, target.id, context);
|
|
3357
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
|
+
},
|
|
3358
4010
|
async compact(instructions) {
|
|
3359
4011
|
const agent = await target.agent(context);
|
|
3360
4012
|
if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
|
|
@@ -3546,6 +4198,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3546
4198
|
getConversation,
|
|
3547
4199
|
async archiveConversation(id) {
|
|
3548
4200
|
await (await sessionFor(id)).abort();
|
|
4201
|
+
await jobs.stopAll(jobIo(id), id);
|
|
3549
4202
|
await metadata.archive(id);
|
|
3550
4203
|
await harness.commit(async (tx) => {
|
|
3551
4204
|
(await tx.doc(ArchivedDoc, id)).archived = true;
|
|
@@ -3687,7 +4340,7 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
3687
4340
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3688
4341
|
|
|
3689
4342
|
// packages/core/src/application-version.ts
|
|
3690
|
-
var RUNNING_VERSION = "0.3.
|
|
4343
|
+
var RUNNING_VERSION = "0.3.3";
|
|
3691
4344
|
var APPLICATION_PROTOCOL = 4;
|
|
3692
4345
|
function stableVersion(value) {
|
|
3693
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)));
|
|
@@ -3837,11 +4490,11 @@ class UpdateChecker {
|
|
|
3837
4490
|
}
|
|
3838
4491
|
|
|
3839
4492
|
// packages/gateway/src/server-services.ts
|
|
3840
|
-
import { Type as
|
|
4493
|
+
import { Type as Type8 } from "typebox";
|
|
3841
4494
|
|
|
3842
4495
|
// packages/gateway/src/contract.ts
|
|
3843
4496
|
import { method, remote } from "@earendil-works/chord";
|
|
3844
|
-
import { Type as
|
|
4497
|
+
import { Type as Type5 } from "typebox";
|
|
3845
4498
|
var rpc = (args, result) => [args, result];
|
|
3846
4499
|
function contract(name, def) {
|
|
3847
4500
|
const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
|
|
@@ -3853,19 +4506,19 @@ function contract(name, def) {
|
|
|
3853
4506
|
]));
|
|
3854
4507
|
return remote(name, { ...def, variants });
|
|
3855
4508
|
}
|
|
3856
|
-
var str =
|
|
3857
|
-
var num =
|
|
3858
|
-
var bool =
|
|
3859
|
-
var nil =
|
|
3860
|
-
var integer =
|
|
3861
|
-
var nullableString =
|
|
3862
|
-
var phase =
|
|
3863
|
-
var level =
|
|
3864
|
-
var thinking =
|
|
3865
|
-
var model =
|
|
3866
|
-
var change =
|
|
3867
|
-
var usage =
|
|
3868
|
-
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({
|
|
3869
4522
|
name: str,
|
|
3870
4523
|
mimeType: str,
|
|
3871
4524
|
width: integer,
|
|
@@ -3874,99 +4527,125 @@ var image = Type4.Object({
|
|
|
3874
4527
|
entryId: integer,
|
|
3875
4528
|
contentIndex: integer
|
|
3876
4529
|
});
|
|
3877
|
-
var skill =
|
|
3878
|
-
var call =
|
|
4530
|
+
var skill = Type5.Object({ name: str, location: str, body: str, args: str });
|
|
4531
|
+
var call = Type5.Object({
|
|
3879
4532
|
callId: str,
|
|
3880
4533
|
name: str,
|
|
3881
4534
|
summary: str,
|
|
3882
4535
|
detail: nullableString,
|
|
3883
4536
|
phase,
|
|
3884
|
-
output: nullableString
|
|
4537
|
+
output: nullableString,
|
|
4538
|
+
diff: nullableString,
|
|
4539
|
+
diffAdded: Type5.Union([num, nil]),
|
|
4540
|
+
diffRemoved: Type5.Union([num, nil]),
|
|
4541
|
+
diffTruncated: Type5.Optional(bool)
|
|
3885
4542
|
});
|
|
3886
|
-
var DelegationSchema =
|
|
4543
|
+
var DelegationSchema = Type5.Object({
|
|
3887
4544
|
parentId: integer,
|
|
3888
|
-
agentId:
|
|
3889
|
-
round:
|
|
3890
|
-
runnerId:
|
|
3891
|
-
inputEntryId:
|
|
3892
|
-
answerEntryId:
|
|
3893
|
-
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])),
|
|
3894
4551
|
taskId: integer,
|
|
3895
4552
|
callId: str,
|
|
3896
4553
|
childId: integer,
|
|
3897
|
-
role:
|
|
4554
|
+
role: Type5.Enum(["explore", "plan", "coder"]),
|
|
3898
4555
|
title: str,
|
|
3899
4556
|
createdAt: num,
|
|
3900
|
-
startedAt:
|
|
3901
|
-
endedAt:
|
|
3902
|
-
status:
|
|
4557
|
+
startedAt: Type5.Union([num, nil]),
|
|
4558
|
+
endedAt: Type5.Union([num, nil]),
|
|
4559
|
+
status: Type5.Enum(["queued", "running", "completed", "failed", "cancelled"]),
|
|
3903
4560
|
report: nullableString,
|
|
3904
|
-
approvalCount:
|
|
3905
|
-
historyThrough:
|
|
3906
|
-
historyUsage:
|
|
4561
|
+
approvalCount: Type5.Optional(integer),
|
|
4562
|
+
historyThrough: Type5.Optional(integer),
|
|
4563
|
+
historyUsage: Type5.Optional(usage)
|
|
3907
4564
|
});
|
|
3908
|
-
var SubagentSummarySchema =
|
|
4565
|
+
var SubagentSummarySchema = Type5.Object({
|
|
3909
4566
|
agentId: str,
|
|
3910
4567
|
parentId: integer,
|
|
3911
4568
|
childId: integer,
|
|
3912
|
-
role:
|
|
4569
|
+
role: Type5.Enum(["explore", "plan", "coder"]),
|
|
3913
4570
|
title: str,
|
|
3914
|
-
status:
|
|
3915
|
-
activeRoundId:
|
|
4571
|
+
status: Type5.Enum(["idle", "queued", "running"]),
|
|
4572
|
+
activeRoundId: Type5.Union([integer, nil]),
|
|
3916
4573
|
lastRoundId: integer,
|
|
3917
|
-
lastReport:
|
|
4574
|
+
lastReport: Type5.Optional(nullableString),
|
|
3918
4575
|
roundCount: integer,
|
|
3919
4576
|
taskTitle: str,
|
|
3920
4577
|
approvalCount: integer,
|
|
3921
4578
|
createdAt: num,
|
|
3922
|
-
historyThrough:
|
|
3923
|
-
historyUsage:
|
|
4579
|
+
historyThrough: Type5.Optional(integer),
|
|
4580
|
+
historyUsage: Type5.Optional(usage)
|
|
3924
4581
|
});
|
|
3925
|
-
var
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
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),
|
|
3931
4610
|
model: str,
|
|
3932
|
-
agent:
|
|
4611
|
+
agent: Type5.Object({ model: Type5.Union([model, nil]), thinkingLevel: Type5.Union([thinking, nil]) }),
|
|
3933
4612
|
cwd: str,
|
|
3934
4613
|
busy: bool,
|
|
3935
4614
|
status: nullableString,
|
|
3936
4615
|
usage,
|
|
3937
|
-
contextUsage:
|
|
3938
|
-
compaction:
|
|
3939
|
-
failure:
|
|
3940
|
-
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({
|
|
3941
4620
|
taskId: integer,
|
|
3942
|
-
reason:
|
|
4621
|
+
reason: Type5.Enum(["manual", "threshold", "overflow"]),
|
|
3943
4622
|
blocking: bool,
|
|
3944
4623
|
attempt: integer,
|
|
3945
|
-
retry:
|
|
4624
|
+
retry: Type5.Optional(Type5.Object({ at: num, error: str }))
|
|
3946
4625
|
})),
|
|
3947
|
-
latest:
|
|
4626
|
+
latest: Type5.Union([
|
|
3948
4627
|
nil,
|
|
3949
|
-
|
|
4628
|
+
Type5.Object({
|
|
3950
4629
|
taskId: integer,
|
|
3951
|
-
phase:
|
|
4630
|
+
phase: Type5.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
|
|
3952
4631
|
at: num
|
|
3953
4632
|
})
|
|
3954
4633
|
])
|
|
3955
4634
|
})),
|
|
3956
|
-
items:
|
|
3957
|
-
|
|
3958
|
-
type:
|
|
3959
|
-
order:
|
|
4635
|
+
items: Type5.Array(Type5.Union([
|
|
4636
|
+
Type5.Object({
|
|
4637
|
+
type: Type5.Literal("user"),
|
|
4638
|
+
order: Type5.Optional(num),
|
|
3960
4639
|
key: str,
|
|
3961
4640
|
id: integer,
|
|
3962
4641
|
text: str,
|
|
3963
|
-
images:
|
|
3964
|
-
skill:
|
|
3965
|
-
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 }))
|
|
3966
4645
|
}),
|
|
3967
|
-
|
|
3968
|
-
type:
|
|
3969
|
-
order:
|
|
4646
|
+
Type5.Object({
|
|
4647
|
+
type: Type5.Literal("assistant"),
|
|
4648
|
+
order: Type5.Optional(num),
|
|
3970
4649
|
key: str,
|
|
3971
4650
|
id: integer,
|
|
3972
4651
|
thinking: str,
|
|
@@ -3975,11 +4654,11 @@ var ChatViewSchema = Type4.Object({
|
|
|
3975
4654
|
errorMessage: nullableString,
|
|
3976
4655
|
note: nullableString
|
|
3977
4656
|
}),
|
|
3978
|
-
|
|
3979
|
-
type:
|
|
3980
|
-
order:
|
|
4657
|
+
Type5.Object({
|
|
4658
|
+
type: Type5.Literal("tool"),
|
|
4659
|
+
order: Type5.Optional(num),
|
|
3981
4660
|
key: str,
|
|
3982
|
-
images:
|
|
4661
|
+
images: Type5.Optional(Type5.Array(image)),
|
|
3983
4662
|
callId: str,
|
|
3984
4663
|
name: str,
|
|
3985
4664
|
summary: str,
|
|
@@ -3988,50 +4667,61 @@ var ChatViewSchema = Type4.Object({
|
|
|
3988
4667
|
isError: bool,
|
|
3989
4668
|
output: nullableString,
|
|
3990
4669
|
result: nullableString,
|
|
3991
|
-
durationMs:
|
|
3992
|
-
note: nullableString
|
|
4670
|
+
durationMs: Type5.Union([num, nil]),
|
|
4671
|
+
note: nullableString,
|
|
4672
|
+
diff: nullableString,
|
|
4673
|
+
diffAdded: Type5.Union([num, nil]),
|
|
4674
|
+
diffRemoved: Type5.Union([num, nil]),
|
|
4675
|
+
diffTruncated: Type5.Optional(bool)
|
|
3993
4676
|
}),
|
|
3994
|
-
|
|
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
|
+
})
|
|
3995
4685
|
])),
|
|
3996
|
-
streaming:
|
|
4686
|
+
streaming: Type5.Union([nil, Type5.Object({ thinking: str, text: str, toolCalls: Type5.Array(call) })])
|
|
3997
4687
|
});
|
|
3998
|
-
var SessionSummarySchema =
|
|
4688
|
+
var SessionSummarySchema = Type5.Object({
|
|
3999
4689
|
workspaceRoot: str,
|
|
4000
4690
|
lastUsedAt: num,
|
|
4001
4691
|
title: str,
|
|
4002
4692
|
firstPromptPreview: str,
|
|
4003
4693
|
createdAt: num,
|
|
4004
|
-
lastMessageAt:
|
|
4005
|
-
model:
|
|
4006
|
-
status:
|
|
4694
|
+
lastMessageAt: Type5.Union([num, nil]),
|
|
4695
|
+
model: Type5.Union([model, nil]),
|
|
4696
|
+
status: Type5.Enum(["idle", "running", "error"]),
|
|
4007
4697
|
id: str
|
|
4008
4698
|
});
|
|
4009
|
-
var DirectoryStateSchema =
|
|
4010
|
-
var WorkspaceSummarySchema =
|
|
4011
|
-
var WorkspaceStateSchema =
|
|
4012
|
-
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({
|
|
4013
4703
|
savedConfigured: bool,
|
|
4014
4704
|
environmentConfigured: bool,
|
|
4015
|
-
source:
|
|
4705
|
+
source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
|
|
4016
4706
|
});
|
|
4017
|
-
var CredentialStateSchema =
|
|
4707
|
+
var CredentialStateSchema = Type5.Object({
|
|
4018
4708
|
revision: integer,
|
|
4019
|
-
statuses:
|
|
4709
|
+
statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
|
|
4020
4710
|
});
|
|
4021
|
-
var AttachmentSchema =
|
|
4022
|
-
var chatInput =
|
|
4711
|
+
var AttachmentSchema = Type5.Object({ sessionId: str, attachmentId: str });
|
|
4712
|
+
var chatInput = Type5.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
|
|
4023
4713
|
var NekoSessionDirectory = contract("neko.session-directory", {
|
|
4024
4714
|
version: 1,
|
|
4025
|
-
input:
|
|
4715
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4026
4716
|
variants: { ready: { state: { state: DirectoryStateSchema } } }
|
|
4027
4717
|
});
|
|
4028
4718
|
var NekoSessionManagement = contract("neko.session-management", {
|
|
4029
4719
|
version: 1,
|
|
4030
|
-
input:
|
|
4720
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4031
4721
|
variants: {
|
|
4032
4722
|
ready: {
|
|
4033
4723
|
methods: {
|
|
4034
|
-
create: rpc(
|
|
4724
|
+
create: rpc(Type5.Object({ ...change.properties, cwd: Type5.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
|
|
4035
4725
|
archive: rpc(str, nil),
|
|
4036
4726
|
attach: rpc(str, AttachmentSchema),
|
|
4037
4727
|
detach: rpc(nil, nil),
|
|
@@ -4040,30 +4730,30 @@ var NekoSessionManagement = contract("neko.session-management", {
|
|
|
4040
4730
|
}
|
|
4041
4731
|
}
|
|
4042
4732
|
});
|
|
4043
|
-
var resize =
|
|
4044
|
-
maxWidth:
|
|
4045
|
-
maxHeight:
|
|
4046
|
-
maxBytes:
|
|
4047
|
-
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)
|
|
4048
4738
|
});
|
|
4049
|
-
var inputLimits =
|
|
4050
|
-
maxRequestBytes:
|
|
4051
|
-
images:
|
|
4052
|
-
resize:
|
|
4053
|
-
maxPerMessage:
|
|
4054
|
-
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)
|
|
4055
4745
|
}))
|
|
4056
4746
|
});
|
|
4057
|
-
var modelInfo =
|
|
4747
|
+
var modelInfo = Type5.Object({
|
|
4058
4748
|
provider: str,
|
|
4059
4749
|
modelId: str,
|
|
4060
4750
|
name: str,
|
|
4061
4751
|
contextWindow: num,
|
|
4062
|
-
input:
|
|
4063
|
-
inputLimits:
|
|
4752
|
+
input: Type5.Array(str),
|
|
4753
|
+
inputLimits: Type5.Optional(inputLimits),
|
|
4064
4754
|
reasoning: bool,
|
|
4065
|
-
supportedThinkingLevels:
|
|
4066
|
-
thinkingLevelAdjustments:
|
|
4755
|
+
supportedThinkingLevels: Type5.Array(thinking),
|
|
4756
|
+
thinkingLevelAdjustments: Type5.Object({
|
|
4067
4757
|
off: thinking,
|
|
4068
4758
|
minimal: thinking,
|
|
4069
4759
|
low: thinking,
|
|
@@ -4075,31 +4765,31 @@ var modelInfo = Type4.Object({
|
|
|
4075
4765
|
});
|
|
4076
4766
|
var NekoModels = contract("neko.models", {
|
|
4077
4767
|
version: 1,
|
|
4078
|
-
input:
|
|
4079
|
-
variants: { ready: { methods: { list: rpc(nil,
|
|
4768
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4769
|
+
variants: { ready: { methods: { list: rpc(nil, Type5.Array(modelInfo)) } } }
|
|
4080
4770
|
});
|
|
4081
|
-
var imageChunk =
|
|
4771
|
+
var imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
|
|
4082
4772
|
var NekoSubagent = contract("neko.subagent", {
|
|
4083
4773
|
version: 1,
|
|
4084
|
-
input:
|
|
4774
|
+
input: Type5.Object({
|
|
4085
4775
|
sessionId: str,
|
|
4086
4776
|
attachmentId: str,
|
|
4087
|
-
taskId:
|
|
4088
|
-
agentId:
|
|
4777
|
+
taskId: Type5.Optional(integer),
|
|
4778
|
+
agentId: Type5.Optional(str)
|
|
4089
4779
|
}),
|
|
4090
4780
|
variants: {
|
|
4091
4781
|
ready: {
|
|
4092
4782
|
state: { revision: integer },
|
|
4093
4783
|
methods: {
|
|
4094
|
-
chunk: rpc(
|
|
4095
|
-
kind:
|
|
4096
|
-
before:
|
|
4097
|
-
token:
|
|
4784
|
+
chunk: rpc(Type5.Object({
|
|
4785
|
+
kind: Type5.Enum(["view", "history"]),
|
|
4786
|
+
before: Type5.Optional(integer),
|
|
4787
|
+
token: Type5.Optional(integer),
|
|
4098
4788
|
offset: integer
|
|
4099
|
-
}),
|
|
4100
|
-
getImage: rpc(
|
|
4101
|
-
decideApproval: rpc(
|
|
4102
|
-
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)
|
|
4103
4793
|
}
|
|
4104
4794
|
}
|
|
4105
4795
|
}
|
|
@@ -4111,79 +4801,81 @@ var NekoChat = contract("neko.chat", {
|
|
|
4111
4801
|
ready: {
|
|
4112
4802
|
state: { view: ChatViewSchema },
|
|
4113
4803
|
methods: {
|
|
4114
|
-
submit: rpc(
|
|
4804
|
+
submit: rpc(Type5.Object({
|
|
4115
4805
|
content: str,
|
|
4116
|
-
images:
|
|
4117
|
-
requestId:
|
|
4118
|
-
whenBusy:
|
|
4806
|
+
images: Type5.Optional(Type5.Array(str, { maxItems: 4 })),
|
|
4807
|
+
requestId: Type5.Optional(str),
|
|
4808
|
+
whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
|
|
4119
4809
|
}, { additionalProperties: false }), nil),
|
|
4120
4810
|
abort: rpc(nil, nil),
|
|
4121
4811
|
usage: rpc(nil, usage),
|
|
4122
|
-
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),
|
|
4123
4815
|
configure: rpc(change, nil),
|
|
4124
|
-
compact: rpc(
|
|
4125
|
-
importImage: rpc(str,
|
|
4126
|
-
draftImage: rpc(
|
|
4127
|
-
beginImage: rpc(
|
|
4128
|
-
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),
|
|
4129
4821
|
finishImage: rpc(str, nil),
|
|
4130
4822
|
releaseImage: rpc(str, nil),
|
|
4131
|
-
getImage: rpc(
|
|
4823
|
+
getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
|
|
4132
4824
|
}
|
|
4133
4825
|
}
|
|
4134
4826
|
}
|
|
4135
4827
|
});
|
|
4136
|
-
var browse =
|
|
4137
|
-
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({
|
|
4138
4830
|
path: str,
|
|
4139
4831
|
parent: nullableString,
|
|
4140
|
-
entries:
|
|
4832
|
+
entries: Type5.Array(Type5.Object({ name: str, path: str, error: Type5.Optional(str) })),
|
|
4141
4833
|
next: nullableString
|
|
4142
4834
|
});
|
|
4143
4835
|
var NekoWorkspaces = contract("neko.workspaces", {
|
|
4144
4836
|
version: 1,
|
|
4145
|
-
input:
|
|
4837
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4146
4838
|
variants: {
|
|
4147
4839
|
ready: {
|
|
4148
4840
|
state: { state: WorkspaceStateSchema },
|
|
4149
4841
|
methods: {
|
|
4150
|
-
add: rpc(
|
|
4842
|
+
add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
|
|
4151
4843
|
browse: rpc(browse, page)
|
|
4152
4844
|
}
|
|
4153
4845
|
}
|
|
4154
4846
|
}
|
|
4155
4847
|
});
|
|
4156
|
-
var provider =
|
|
4848
|
+
var provider = Type5.Enum(["deepseek", "tavily"]);
|
|
4157
4849
|
var NekoCredentials = contract("neko.credentials", {
|
|
4158
4850
|
version: 1,
|
|
4159
|
-
input:
|
|
4851
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4160
4852
|
variants: {
|
|
4161
4853
|
ready: {
|
|
4162
4854
|
state: { state: CredentialStateSchema },
|
|
4163
4855
|
methods: {
|
|
4164
|
-
save: rpc(
|
|
4165
|
-
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)
|
|
4166
4858
|
}
|
|
4167
4859
|
}
|
|
4168
4860
|
}
|
|
4169
4861
|
});
|
|
4170
|
-
var FileCandidatesSchema =
|
|
4171
|
-
entries:
|
|
4862
|
+
var FileCandidatesSchema = Type5.Object({
|
|
4863
|
+
entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
|
|
4172
4864
|
limited: bool,
|
|
4173
4865
|
incomplete: bool
|
|
4174
4866
|
});
|
|
4175
4867
|
var NekoWorkspaceFiles = contract("neko.workspace-files", {
|
|
4176
4868
|
version: 1,
|
|
4177
|
-
input:
|
|
4869
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4178
4870
|
variants: {
|
|
4179
4871
|
ready: {
|
|
4180
4872
|
methods: {
|
|
4181
|
-
search: rpc(
|
|
4873
|
+
search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
|
|
4182
4874
|
}
|
|
4183
4875
|
}
|
|
4184
4876
|
}
|
|
4185
4877
|
});
|
|
4186
|
-
var marketSkill =
|
|
4878
|
+
var marketSkill = Type5.Object({
|
|
4187
4879
|
name: str,
|
|
4188
4880
|
description: str,
|
|
4189
4881
|
category: str,
|
|
@@ -4193,38 +4885,38 @@ var marketSkill = Type4.Object({
|
|
|
4193
4885
|
sha256: str,
|
|
4194
4886
|
sizeBytes: integer,
|
|
4195
4887
|
uploadedAt: str,
|
|
4196
|
-
installed:
|
|
4197
|
-
activeLevel:
|
|
4888
|
+
installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
|
|
4889
|
+
activeLevel: Type5.Union([level, nil])
|
|
4198
4890
|
});
|
|
4199
|
-
var preview =
|
|
4200
|
-
var scope =
|
|
4201
|
-
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) });
|
|
4202
4894
|
var NekoSkillMarket = contract("neko.skill-market", {
|
|
4203
4895
|
version: 1,
|
|
4204
|
-
input:
|
|
4896
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
4205
4897
|
variants: {
|
|
4206
4898
|
ready: {
|
|
4207
4899
|
methods: {
|
|
4208
|
-
list: rpc(
|
|
4209
|
-
detail: rpc(
|
|
4210
|
-
preview: rpc(
|
|
4211
|
-
install: rpc(
|
|
4212
|
-
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)
|
|
4213
4905
|
}
|
|
4214
4906
|
}
|
|
4215
4907
|
}
|
|
4216
4908
|
});
|
|
4217
|
-
var ApplicationInfoSchema =
|
|
4909
|
+
var ApplicationInfoSchema = Type5.Object({
|
|
4218
4910
|
runningVersion: str,
|
|
4219
4911
|
installedVersion: nullableString,
|
|
4220
|
-
protocolVersion:
|
|
4912
|
+
protocolVersion: Type5.Integer({ minimum: 1 }),
|
|
4221
4913
|
latestVersion: nullableString,
|
|
4222
|
-
checkedAt:
|
|
4223
|
-
checkState:
|
|
4914
|
+
checkedAt: Type5.Union([integer, nil]),
|
|
4915
|
+
checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"])
|
|
4224
4916
|
});
|
|
4225
4917
|
var NekoApplicationInfo = contract("neko.application-info", {
|
|
4226
4918
|
version: 1,
|
|
4227
|
-
input:
|
|
4919
|
+
input: Type5.Object({}),
|
|
4228
4920
|
variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
|
|
4229
4921
|
});
|
|
4230
4922
|
|
|
@@ -4240,9 +4932,9 @@ import {
|
|
|
4240
4932
|
opaque as opaque2,
|
|
4241
4933
|
resource as resource2
|
|
4242
4934
|
} from "@earendil-works/chord";
|
|
4243
|
-
import { withAbortSignal as
|
|
4935
|
+
import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
|
|
4244
4936
|
import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
|
|
4245
|
-
import { Type as
|
|
4937
|
+
import { Type as Type7 } from "typebox";
|
|
4246
4938
|
|
|
4247
4939
|
// packages/gateway/src/image-uploads.ts
|
|
4248
4940
|
var IMAGE_CHUNK_BYTES = 256 * 1024;
|
|
@@ -4349,7 +5041,7 @@ import {
|
|
|
4349
5041
|
resource
|
|
4350
5042
|
} from "@earendil-works/chord";
|
|
4351
5043
|
import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
|
|
4352
|
-
import { Type as
|
|
5044
|
+
import { Type as Type6 } from "typebox";
|
|
4353
5045
|
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
4354
5046
|
const child = await runtime.harness.conversation(childId, context);
|
|
4355
5047
|
if (!child)
|
|
@@ -4474,15 +5166,15 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4474
5166
|
}
|
|
4475
5167
|
var ChildProjection = local("neko.subagent-projection", {
|
|
4476
5168
|
version: 1,
|
|
4477
|
-
input:
|
|
5169
|
+
input: Type6.Object({ parentId: Type6.Integer(), agentId: Type6.String() }),
|
|
4478
5170
|
members: {
|
|
4479
|
-
revision:
|
|
5171
|
+
revision: Type6.Integer(),
|
|
4480
5172
|
reader: opaque("neko.subagent-reader@1")
|
|
4481
5173
|
}
|
|
4482
5174
|
});
|
|
4483
5175
|
function installSubagentHost(host, runtime, context) {
|
|
4484
5176
|
host.serve(ChildProjection, (input, _principal, self) => {
|
|
4485
|
-
const { revision } = self.state({ revision: cell(
|
|
5177
|
+
const { revision } = self.state({ revision: cell(Type6.Integer(), 0) });
|
|
4486
5178
|
const { reader } = self.resources({
|
|
4487
5179
|
reader: resource({
|
|
4488
5180
|
rev: 1,
|
|
@@ -4524,7 +5216,7 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4524
5216
|
};
|
|
4525
5217
|
const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
|
|
4526
5218
|
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
|
|
4527
|
-
const { authorizedRevision } = self.state({ authorizedRevision: cell(
|
|
5219
|
+
const { authorizedRevision } = self.state({ authorizedRevision: cell(Type6.Integer(), 0) });
|
|
4528
5220
|
self.listen({
|
|
4529
5221
|
authorizedView: listener(shared.revision, async (value, ctx) => {
|
|
4530
5222
|
try {
|
|
@@ -4579,7 +5271,7 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4579
5271
|
}
|
|
4580
5272
|
|
|
4581
5273
|
// packages/gateway/src/session-host.ts
|
|
4582
|
-
var OwnerInput =
|
|
5274
|
+
var OwnerInput = Type7.Object({});
|
|
4583
5275
|
var Owner = local2("neko.attachment-owner", {
|
|
4584
5276
|
version: 1,
|
|
4585
5277
|
input: OwnerInput,
|
|
@@ -4587,7 +5279,7 @@ var Owner = local2("neko.attachment-owner", {
|
|
|
4587
5279
|
});
|
|
4588
5280
|
var Projection = local2("neko.projection", {
|
|
4589
5281
|
version: 1,
|
|
4590
|
-
input:
|
|
5282
|
+
input: Type7.Object({ id: Type7.String() }),
|
|
4591
5283
|
members: {
|
|
4592
5284
|
view: ChatViewSchema,
|
|
4593
5285
|
reader: opaque2("neko.projection-reader@1")
|
|
@@ -4603,6 +5295,8 @@ var emptyView = {
|
|
|
4603
5295
|
items: [],
|
|
4604
5296
|
streaming: null
|
|
4605
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;
|
|
4606
5300
|
async function openProjection(runtime, session, context) {
|
|
4607
5301
|
const view = await session.viewState();
|
|
4608
5302
|
const history = new Map;
|
|
@@ -4618,7 +5312,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4618
5312
|
const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
|
|
4619
5313
|
let childIds = new Set;
|
|
4620
5314
|
const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4621
|
-
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))))
|
|
4622
5316
|
changed?.(null);
|
|
4623
5317
|
});
|
|
4624
5318
|
return {
|
|
@@ -4644,6 +5338,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4644
5338
|
}, naming)
|
|
4645
5339
|
}
|
|
4646
5340
|
}, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
|
|
5341
|
+
const jobs = (await session.jobs()).map(clipJob);
|
|
4647
5342
|
const subagents = await listDelegations(runtime.harness, session.id, context);
|
|
4648
5343
|
const approvals = await runtime.approvals.list(session.id, context);
|
|
4649
5344
|
for (const entry of subagents)
|
|
@@ -4658,6 +5353,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4658
5353
|
const u = totalUsage(aggregate);
|
|
4659
5354
|
return {
|
|
4660
5355
|
...projected,
|
|
5356
|
+
jobs,
|
|
4661
5357
|
agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
|
|
4662
5358
|
subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
|
|
4663
5359
|
...entry,
|
|
@@ -4829,7 +5525,7 @@ function serveSessions(host, runtime, context) {
|
|
|
4829
5525
|
getImage: async (input, ctx) => {
|
|
4830
5526
|
if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
|
|
4831
5527
|
throw new ChordError2("invalid", "Invalid image range");
|
|
4832
|
-
const entry = await shared.reader.entry(input.entryId,
|
|
5528
|
+
const entry = await shared.reader.entry(input.entryId, withAbortSignal4(ctx.signal, context));
|
|
4833
5529
|
const content = entry?.model?.[0]?.content;
|
|
4834
5530
|
const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
|
|
4835
5531
|
if (image?.type !== "image")
|
|
@@ -4866,6 +5562,21 @@ function serveSessions(host, runtime, context) {
|
|
|
4866
5562
|
return null;
|
|
4867
5563
|
},
|
|
4868
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
|
+
},
|
|
4869
5580
|
usage: async () => {
|
|
4870
5581
|
const usage = totalUsage(await projection.value.session.usage());
|
|
4871
5582
|
return {
|
|
@@ -5600,12 +6311,12 @@ async function listDrives() {
|
|
|
5600
6311
|
// packages/gateway/src/server-services.ts
|
|
5601
6312
|
var ServerState = local3("neko.server-state", {
|
|
5602
6313
|
version: 1,
|
|
5603
|
-
input:
|
|
6314
|
+
input: Type8.Object({}),
|
|
5604
6315
|
members: {
|
|
5605
6316
|
directory: DirectoryStateSchema,
|
|
5606
6317
|
workspaces: WorkspaceStateSchema,
|
|
5607
6318
|
credentials: CredentialStateSchema,
|
|
5608
|
-
refreshCredentials: method2(
|
|
6319
|
+
refreshCredentials: method2(Type8.Null(), Type8.Null())
|
|
5609
6320
|
}
|
|
5610
6321
|
});
|
|
5611
6322
|
async function createNekoServerServices(options) {
|