teapot-coding-agent 0.14.11 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent.js +188 -14
- package/dist/agent/tools.js +6 -1
- package/dist/config-schema.js +2 -1
- package/dist/log/events.js +5 -1
- package/dist/master.js +16 -6
- package/dist/server/api.js +55 -6
- package/package.json +1 -1
- package/public/assets/{index-CG4aaYbK.js → index-BqQgmv85.js} +10 -10
- package/public/assets/index-BqQgmv85.js.map +1 -0
- package/public/assets/index-CJThocQP.css +1 -0
- package/public/assets/{shiki-BBO6_R27.js → shiki-DN3doI_D.js} +2 -2
- package/public/assets/{shiki-BBO6_R27.js.map → shiki-DN3doI_D.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-BXyhr5r5.css +0 -1
- package/public/assets/index-CG4aaYbK.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -469,7 +469,17 @@ export class Agent {
|
|
|
469
469
|
break;
|
|
470
470
|
body = stripped;
|
|
471
471
|
}
|
|
472
|
-
|
|
472
|
+
// verification contract (pi-goal-x style): a `verify:` block at the tail
|
|
473
|
+
let verify = "";
|
|
474
|
+
const vm = body.match(/\nverify:\s*\n([\s\S]+)$/i) ?? body.match(/^verify:\s*(.+)$/im);
|
|
475
|
+
if (vm) {
|
|
476
|
+
verify = vm[1].trim();
|
|
477
|
+
body = (body.slice(0, vm.index).trimEnd());
|
|
478
|
+
}
|
|
479
|
+
const out = { text: body, status, updatedAt: new Date().toISOString() };
|
|
480
|
+
if (verify)
|
|
481
|
+
out.verify = verify;
|
|
482
|
+
return out;
|
|
473
483
|
}
|
|
474
484
|
async writeGoalFile() {
|
|
475
485
|
// don't let previously-appended bookkeeping lines accumulate in the body
|
|
@@ -480,7 +490,8 @@ export class Agent {
|
|
|
480
490
|
break;
|
|
481
491
|
body = stripped;
|
|
482
492
|
}
|
|
483
|
-
await fs.writeFile(this.goalFile, `${body}\n\nstatus: ${this.goal.status}\nupdated: ${this.goal.updatedAt}\n
|
|
493
|
+
await fs.writeFile(this.goalFile, `${body}\n\nstatus: ${this.goal.status}\nupdated: ${this.goal.updatedAt}\n` +
|
|
494
|
+
(this.goal.verify ? `verify:\n${this.goal.verify}\n` : ""), "utf8");
|
|
484
495
|
}
|
|
485
496
|
async setGoal(text) {
|
|
486
497
|
this.goal = { text, status: "active", updatedAt: new Date().toISOString() };
|
|
@@ -546,6 +557,33 @@ export class Agent {
|
|
|
546
557
|
await this.writeGoalFile();
|
|
547
558
|
await this.log.append("goal", this.currentSession, this.currentBranch, { event: "status", status });
|
|
548
559
|
}
|
|
560
|
+
/** Set the goal's verification contract (completion requirements). */
|
|
561
|
+
async setGoalVerify(verify) {
|
|
562
|
+
this.goal = { ...this.goal, verify: verify.slice(0, 4000), updatedAt: new Date().toISOString() };
|
|
563
|
+
await this.writeGoalFile();
|
|
564
|
+
await this.log.append("goal", this.currentSession, this.currentBranch, {
|
|
565
|
+
event: "verify-set",
|
|
566
|
+
verify: verify.slice(0, 2000),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** Record a verification-contract audit outcome (pi-goal-x style review). */
|
|
570
|
+
async setGoalAudit(verdict, feedback) {
|
|
571
|
+
this.goal = {
|
|
572
|
+
...this.goal,
|
|
573
|
+
updatedAt: new Date().toISOString(),
|
|
574
|
+
audit: { verdict, feedback: feedback.slice(0, 4000), at: new Date().toISOString() },
|
|
575
|
+
};
|
|
576
|
+
if (verdict === "approved")
|
|
577
|
+
this.goal.status = "done";
|
|
578
|
+
else if (this.goal.status === "done")
|
|
579
|
+
this.goal.status = "active"; // reopened
|
|
580
|
+
await this.writeGoalFile();
|
|
581
|
+
await this.log.append("goal", this.currentSession, this.currentBranch, {
|
|
582
|
+
event: "audit",
|
|
583
|
+
verdict,
|
|
584
|
+
feedback: feedback.slice(0, 2000),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
549
587
|
snapshot() {
|
|
550
588
|
return {
|
|
551
589
|
id: this.opts.id,
|
|
@@ -554,7 +592,12 @@ export class Agent {
|
|
|
554
592
|
workspace: this.workspace,
|
|
555
593
|
session: this.currentSession,
|
|
556
594
|
branch: this.currentBranch,
|
|
557
|
-
goal: {
|
|
595
|
+
goal: {
|
|
596
|
+
status: this.goal.status,
|
|
597
|
+
text: this.goal.text.slice(0, 400),
|
|
598
|
+
...(this.goal.verify ? { verify: this.goal.verify.slice(0, 1000) } : {}),
|
|
599
|
+
...(this.goal.audit ? { audit: this.goal.audit } : {}),
|
|
600
|
+
},
|
|
558
601
|
latestProgress: this.latestProgress,
|
|
559
602
|
stats: { ...this.stats },
|
|
560
603
|
model: this.opts.llm.model,
|
|
@@ -595,20 +638,69 @@ export class Agent {
|
|
|
595
638
|
this.activityChars = 0;
|
|
596
639
|
this.turnsSinceProgress = 0;
|
|
597
640
|
}
|
|
641
|
+
// stable id shared by the log event and the UI's pending echo — the UI
|
|
642
|
+
// flips its echo to "sent" when prompt-delivered carries this id back
|
|
643
|
+
const promptId = source === "user" ? `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}` : "";
|
|
598
644
|
void this.ensureReady()
|
|
599
645
|
.then(() => {
|
|
600
|
-
this.pendingPrompts.push({ source, text });
|
|
601
|
-
return this.log.append("prompt", this.currentSession, this.currentBranch, {
|
|
646
|
+
this.pendingPrompts.push({ source, text, ...(promptId ? { id: promptId } : {}) });
|
|
647
|
+
return this.log.append("prompt", this.currentSession, this.currentBranch, {
|
|
648
|
+
source,
|
|
649
|
+
text,
|
|
650
|
+
...(promptId ? { promptId } : {}),
|
|
651
|
+
});
|
|
602
652
|
})
|
|
603
653
|
.then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
|
|
604
654
|
.catch(() => { });
|
|
655
|
+
return promptId;
|
|
605
656
|
}
|
|
606
|
-
/**
|
|
657
|
+
/**
|
|
658
|
+
* Withdraw a still-pending user prompt (before it reaches an LLM call).
|
|
659
|
+
* Returns the text so the UI can put it back into the composer draft.
|
|
660
|
+
*/
|
|
661
|
+
cancelPrompt(promptId) {
|
|
662
|
+
const i = this.pendingPrompts.findIndex((p) => p.id === promptId);
|
|
663
|
+
if (i === -1)
|
|
664
|
+
return null; // already delivered or unknown
|
|
665
|
+
const [p] = this.pendingPrompts.splice(i, 1);
|
|
666
|
+
void this.log
|
|
667
|
+
.append("system_note", this.currentSession, this.currentBranch, {
|
|
668
|
+
event: "prompt-cancelled",
|
|
669
|
+
promptId,
|
|
670
|
+
preview: p.text.slice(0, 80),
|
|
671
|
+
})
|
|
672
|
+
.then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
|
|
673
|
+
.catch(() => { });
|
|
674
|
+
return p.text;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Hand queued user prompts to the model at a turn boundary. Each consumed
|
|
678
|
+
* prompt is logged as a system_note (prompt-delivered) so the UI can flip
|
|
679
|
+
* its pending echo to "sent" at exactly the moment the text enters an LLM
|
|
680
|
+
* call payload — not merely when it was logged.
|
|
681
|
+
*/
|
|
607
682
|
drainPendingPrompts() {
|
|
608
683
|
for (const p of this.pendingPrompts.splice(0)) {
|
|
609
684
|
this.messages.push({ role: "user", content: p.text });
|
|
685
|
+
if (p.source === "user") {
|
|
686
|
+
const id = p.id ?? `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
687
|
+
this.deliveredPrompts.push(id);
|
|
688
|
+
// cap: UI only needs recent confirmations
|
|
689
|
+
if (this.deliveredPrompts.length > 64)
|
|
690
|
+
this.deliveredPrompts.shift();
|
|
691
|
+
void this.log
|
|
692
|
+
.append("system_note", this.currentSession, this.currentBranch, {
|
|
693
|
+
event: "prompt-delivered",
|
|
694
|
+
promptId: id,
|
|
695
|
+
preview: p.text.slice(0, 80),
|
|
696
|
+
})
|
|
697
|
+
.then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
|
|
698
|
+
.catch(() => { });
|
|
699
|
+
}
|
|
610
700
|
}
|
|
611
701
|
}
|
|
702
|
+
/** ids of user prompts that have entered an LLM call payload (recent first-capped) */
|
|
703
|
+
deliveredPrompts = [];
|
|
612
704
|
/** Resolves when all queued work (including a running loop) has settled. */
|
|
613
705
|
settled() {
|
|
614
706
|
return this.runChain.catch(() => { });
|
|
@@ -873,11 +965,20 @@ export class Agent {
|
|
|
873
965
|
const text = String(a.text ?? "").trim();
|
|
874
966
|
if (text)
|
|
875
967
|
await this.setGoal(text);
|
|
968
|
+
// pi-goal-x-style verification contract: plain-text completion
|
|
969
|
+
// requirements audited before finish(goalComplete=true) is honored
|
|
970
|
+
if (typeof a.verify === "string" && a.verify.trim())
|
|
971
|
+
await this.setGoalVerify(a.verify.trim().slice(0, 4000));
|
|
876
972
|
await this.answerMeta(call, text ? "goal updated" : "empty goal rejected");
|
|
877
973
|
continue;
|
|
878
974
|
}
|
|
879
975
|
if (call.function.name === "get_goal") {
|
|
880
|
-
await this.answerMeta(call, JSON.stringify({
|
|
976
|
+
await this.answerMeta(call, JSON.stringify({
|
|
977
|
+
goal: this.goal.text || "(none set)",
|
|
978
|
+
status: this.goal.status,
|
|
979
|
+
...(this.goal.verify ? { verify: this.goal.verify } : {}),
|
|
980
|
+
...(this.goal.audit ? { lastAudit: this.goal.audit } : {}),
|
|
981
|
+
}, null, 1));
|
|
881
982
|
continue;
|
|
882
983
|
}
|
|
883
984
|
if (call.function.name === "ask_user") {
|
|
@@ -1054,8 +1155,59 @@ export class Agent {
|
|
|
1054
1155
|
}
|
|
1055
1156
|
async handleFinish(argsJson) {
|
|
1056
1157
|
const args = safeParse(argsJson);
|
|
1057
|
-
if (args.goalComplete === true)
|
|
1058
|
-
|
|
1158
|
+
if (args.goalComplete === true && this.goal.status !== "done") {
|
|
1159
|
+
if (this.goal.verify?.trim()) {
|
|
1160
|
+
// pi-goal-x-style INDEPENDENT COMPLETION REVIEW: a separate LLM call
|
|
1161
|
+
// checks the verification contract against the conversation + summary
|
|
1162
|
+
// before "done" is honored. changes-required reopens the goal and
|
|
1163
|
+
// queues the auditor's feedback as the next user prompt.
|
|
1164
|
+
await this.log.append("goal", this.currentSession, this.currentBranch, {
|
|
1165
|
+
event: "audit-started",
|
|
1166
|
+
verify: this.goal.verify.slice(0, 2000),
|
|
1167
|
+
});
|
|
1168
|
+
try {
|
|
1169
|
+
const auditPrompt = `You are an independent completion AUDITOR (not the worker). Decide whether the goal is genuinely complete.\n\n` +
|
|
1170
|
+
`GOAL:\n${this.goal.text.slice(0, 4000)}\n\nVERIFICATION CONTRACT (must ALL hold):\n${this.goal.verify.slice(0, 2000)}\n\n` +
|
|
1171
|
+
`WORKER'S FINAL SUMMARY:\n${String(args.summary ?? "").slice(0, 3000)}\n\n` +
|
|
1172
|
+
`Conversation evidence follows. Reply with EXACTLY one line starting either\n` +
|
|
1173
|
+
`"APPROVED: <one-sentence justification>" or\n` +
|
|
1174
|
+
`"CHANGES-REQUIRED: <the specific gaps that must be addressed>".`;
|
|
1175
|
+
const res = await this.callLlm([
|
|
1176
|
+
...this.buildMessages(),
|
|
1177
|
+
{ role: "user", content: auditPrompt },
|
|
1178
|
+
], []);
|
|
1179
|
+
const text = String(res.message.content ?? "");
|
|
1180
|
+
const approved = /^APPROVED\b/i.test(text.trim());
|
|
1181
|
+
const feedback = text.replace(/^(APPROVED|CHANGES-REQUIRED)\s*[:—-]?\s*/i, "").trim();
|
|
1182
|
+
await this.setGoalAudit(approved ? "approved" : "changes-required", feedback || "(no detail)");
|
|
1183
|
+
await this.log.append("message", this.currentSession, this.currentBranch, {
|
|
1184
|
+
role: "assistant",
|
|
1185
|
+
content: approved
|
|
1186
|
+
? `✅ completion audit: APPROVED — ${feedback}`
|
|
1187
|
+
: `🔍 completion audit: CHANGES REQUIRED — ${feedback}`,
|
|
1188
|
+
});
|
|
1189
|
+
if (!approved) {
|
|
1190
|
+
// hand the gaps back to the worker as its next instruction
|
|
1191
|
+
this.pendingPrompts.push({
|
|
1192
|
+
source: "harness",
|
|
1193
|
+
text: `[harness] The completion audit REJECTED this finish. Address these gaps, then finish again with goalComplete=true:\n\n${feedback.slice(0, 2000)}`,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
catch (err) {
|
|
1198
|
+
// auditor unavailable → fail open (record it, honor the finish) so a
|
|
1199
|
+
// flaky provider can't trap work in an unauditable loop
|
|
1200
|
+
await this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
1201
|
+
event: "audit-failed",
|
|
1202
|
+
detail: String(err.message).slice(0, 300),
|
|
1203
|
+
});
|
|
1204
|
+
await this.setGoalStatus("done");
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
await this.setGoalStatus("done");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1059
1211
|
await this.log.append("message", this.currentSession, this.currentBranch, {
|
|
1060
1212
|
role: "assistant",
|
|
1061
1213
|
final: true,
|
|
@@ -1420,10 +1572,19 @@ function allToolSpecs() {
|
|
|
1420
1572
|
type: "function",
|
|
1421
1573
|
function: {
|
|
1422
1574
|
name: "set_goal",
|
|
1423
|
-
description: "Replace the harness-managed goal text. Use when the objective itself changes — not for routine updates."
|
|
1575
|
+
description: "Replace the harness-managed goal text. Use when the objective itself changes — not for routine updates. " +
|
|
1576
|
+
"Optionally attach a verification contract (plain-text completion requirements); finish(goalComplete=true) " +
|
|
1577
|
+
"is then audited against it by an independent reviewer before the goal counts as done.",
|
|
1424
1578
|
parameters: {
|
|
1425
1579
|
type: "object",
|
|
1426
|
-
properties: {
|
|
1580
|
+
properties: {
|
|
1581
|
+
text: { type: "string" },
|
|
1582
|
+
verify: {
|
|
1583
|
+
type: "string",
|
|
1584
|
+
description: "completion requirements that must verifiably hold, e.g. 'npm test passes with zero failures; " +
|
|
1585
|
+
"the new endpoint appears in README'. Omit for no audit.",
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1427
1588
|
required: ["text"],
|
|
1428
1589
|
},
|
|
1429
1590
|
},
|
|
@@ -1617,6 +1778,13 @@ function rebuildMessagesFrom(list) {
|
|
|
1617
1778
|
"get_feedback", "add_feedback", "record_decision", "get_decisions",
|
|
1618
1779
|
]);
|
|
1619
1780
|
const openCalls = new Map(); // real tool_call id -> name
|
|
1781
|
+
// callId -> the tool_call entry on its assistant message (fast arg enrichment)
|
|
1782
|
+
const assistantCalls = new Map();
|
|
1783
|
+
const rememberAssistantCalls = (m) => {
|
|
1784
|
+
if (Array.isArray(m.tool_calls))
|
|
1785
|
+
for (const t of m.tool_calls)
|
|
1786
|
+
assistantCalls.set(t.id, t);
|
|
1787
|
+
};
|
|
1620
1788
|
// meta answers are now logged as regular tool_results; the legacy progress
|
|
1621
1789
|
// synthesizer below must not duplicate them
|
|
1622
1790
|
const loggedResults = new Set(list.filter((e) => e.type === "tool_result").map((e) => String(e.data.callId ?? "")));
|
|
@@ -1654,14 +1822,20 @@ function rebuildMessagesFrom(list) {
|
|
|
1654
1822
|
if (!META_TOOLS.has(t.function.name))
|
|
1655
1823
|
openCalls.set(t.id, t.function.name);
|
|
1656
1824
|
}
|
|
1825
|
+
rememberAssistantCalls(m);
|
|
1657
1826
|
msgs.push(m);
|
|
1658
1827
|
}
|
|
1659
1828
|
else if (e.type === "tool_call") {
|
|
1660
1829
|
// enrich the preceding assistant tool_calls with real arguments —
|
|
1661
1830
|
// prefer the provider's raw string so restored requests stay
|
|
1662
|
-
// byte-identical (prefix caches stay warm across restarts)
|
|
1663
|
-
|
|
1664
|
-
|
|
1831
|
+
// byte-identical (prefix caches stay warm across restarts).
|
|
1832
|
+
// Indexed by call id: the old [...msgs].reverse().find() scanned the
|
|
1833
|
+
// whole message list per call — O(n²) on multi-thousand-event logs.
|
|
1834
|
+
let tc = assistantCalls.get(String(d.callId ?? ""));
|
|
1835
|
+
if (!tc) {
|
|
1836
|
+
const prev = [...msgs].reverse().find((x) => x.role === "assistant" && x.tool_calls?.some((t) => t.id === d.callId));
|
|
1837
|
+
tc = prev?.tool_calls?.find((t) => t.id === d.callId);
|
|
1838
|
+
}
|
|
1665
1839
|
if (tc) {
|
|
1666
1840
|
if (typeof d.argsRaw === "string")
|
|
1667
1841
|
tc.function.arguments = d.argsRaw;
|
package/dist/agent/tools.js
CHANGED
|
@@ -16,7 +16,12 @@ function clip(s, max) {
|
|
|
16
16
|
async function readText(p) {
|
|
17
17
|
return fs.readFile(p, "utf8");
|
|
18
18
|
}
|
|
19
|
-
/** Resolve a path inside the workspace; reject escapes (incl. symlink targets).
|
|
19
|
+
/** Resolve a path inside the workspace; reject escapes (incl. symlink targets).
|
|
20
|
+
* KNOWN LIMIT (review finding, accepted): the check-then-open pattern has a
|
|
21
|
+
* TOCTOU window — a symlink swapped in between safeJoin() and fs.open() could
|
|
22
|
+
* point outside the workspace. Closing it needs openat(2)/O_NOFOLLOW at every
|
|
23
|
+
* call site; the operator-facing risk is low (requires workspace write access
|
|
24
|
+
* already), so we document rather than rewrite all call sites. */
|
|
20
25
|
export function safeJoin(cwd, p) {
|
|
21
26
|
const abs = path.resolve(cwd, p);
|
|
22
27
|
const rel = path.relative(cwd, abs);
|
package/dist/config-schema.js
CHANGED
|
@@ -37,7 +37,8 @@ export const ConfigPatchSchema = z.object({
|
|
|
37
37
|
defaultProvider: z.string().optional(),
|
|
38
38
|
progressIntervalMs: z.number().int().min(10_000, "progress interval must be ≥ 10s").optional(),
|
|
39
39
|
progressMinChars: z.number().int().min(100).optional(),
|
|
40
|
-
|
|
40
|
+
// null clears a previously pinned compact budget → back to per-model derivation
|
|
41
|
+
contextTokenBudget: z.number().int().min(1_000).nullable().optional(),
|
|
41
42
|
contextWindowTokens: z.number().int().min(1_000).optional(),
|
|
42
43
|
maxSpawnDepth: z.number().int().min(0).max(8).optional(),
|
|
43
44
|
tasks: z.array(TaskSchema).optional(),
|
package/dist/log/events.js
CHANGED
|
@@ -97,8 +97,12 @@ export class EventLog {
|
|
|
97
97
|
}
|
|
98
98
|
const p = new Promise((resolve, reject) => {
|
|
99
99
|
this.chain = this.chain.then(() => {
|
|
100
|
-
if (!this.stream)
|
|
100
|
+
if (!this.stream) {
|
|
101
|
+
// log closed (dispose) — resolve so callers don't await forever,
|
|
102
|
+
// but say so: silent drops made post-shutdown writes look logged
|
|
103
|
+
console.error(`[teapot] event dropped after log close (${this.agentId}): ${type}`);
|
|
101
104
|
return resolve(evt);
|
|
105
|
+
}
|
|
102
106
|
this.stream.write(JSON.stringify(evt) + "\n", "utf8", (err) => err ? reject(err) : resolve(evt));
|
|
103
107
|
}, () => resolve(evt));
|
|
104
108
|
});
|
package/dist/master.js
CHANGED
|
@@ -250,11 +250,17 @@ export class Master {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
for (const t of this.config.tasks ?? []) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
try {
|
|
254
|
+
this.tasks.push({
|
|
255
|
+
task: t,
|
|
256
|
+
schedule: parseSchedule(t.schedule),
|
|
257
|
+
lastRunMin: t.lastRunMin ?? -1,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
// a hand-edited schedule typo must not stop the server from listening
|
|
262
|
+
console.error(`[teapot] skipping task "${t.id}": ${err.message}`);
|
|
263
|
+
}
|
|
258
264
|
}
|
|
259
265
|
// single low-frequency tick for everything periodic (idle cost ≈ 0)
|
|
260
266
|
setInterval(() => void this.tick(), 15_000).unref();
|
|
@@ -401,7 +407,11 @@ export class Master {
|
|
|
401
407
|
})),
|
|
402
408
|
stop: (ids) => this.stopChildrenFor(ac.id, ids),
|
|
403
409
|
message: (id, text) => this.messageChild(ac.id, id, text),
|
|
404
|
-
wait: (ids, ms) =>
|
|
410
|
+
wait: (ids, ms) =>
|
|
411
|
+
// wire the parent's tool-abort signal: a user stop / dispose must
|
|
412
|
+
// interrupt the park immediately instead of leaving the run chain
|
|
413
|
+
// blocked until the full timeout (up to 1h)
|
|
414
|
+
this.waitChildren(ac.id, ids, ms, self.toolCtx.signal),
|
|
405
415
|
};
|
|
406
416
|
agent.toolCtx.subAgents = hooks;
|
|
407
417
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -77,7 +77,17 @@ export function buildApp(master) {
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
send({ kind: "hello", agents: [...master.agents.values()].map((a) => a.snapshot()) });
|
|
80
|
-
onUpdate = (
|
|
80
|
+
onUpdate = (raw) => {
|
|
81
|
+
const ev = raw;
|
|
82
|
+
// attach the fresh snapshot to agent-update events so clients can
|
|
83
|
+
// update their state WITHOUT a follow-up GET /api/agents poll
|
|
84
|
+
if (ev?.kind === "agent-update" && ev.agentId) {
|
|
85
|
+
const a = master.agents.get(ev.agentId);
|
|
86
|
+
send({ ...ev, snapshot: a?.snapshot() });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
send(ev);
|
|
90
|
+
};
|
|
81
91
|
bus.on("update", onUpdate);
|
|
82
92
|
// app-level liveness ping every 30s
|
|
83
93
|
ka = setInterval(() => send({ kind: "ping" }), 30_000);
|
|
@@ -205,7 +215,9 @@ export function buildApp(master) {
|
|
|
205
215
|
// collision, auto-suffix so creating ~/a/proj and ~/b/proj just works
|
|
206
216
|
let id = base;
|
|
207
217
|
let n = 2;
|
|
208
|
-
|
|
218
|
+
// ids must be unique among RUNNING agents AND persisted config entries —
|
|
219
|
+
// a config-only agent (not yet loaded) would otherwise collide on addAgent
|
|
220
|
+
while (master.agents.has(id) || master.config.agents.some((a) => a.id === id))
|
|
209
221
|
id = `${base.slice(0, 38)}-${n++}`;
|
|
210
222
|
try {
|
|
211
223
|
const agent = await master.addAgent({ id, workspace: ws, provider: body.provider, model: body.model }, { persist: true, fresh: true });
|
|
@@ -451,11 +463,25 @@ export function buildApp(master) {
|
|
|
451
463
|
if (!body.text?.trim())
|
|
452
464
|
return c.json({ error: "text required" }, 400);
|
|
453
465
|
// returns immediately: the prompt is logged + broadcast now, delivered to
|
|
454
|
-
// the model at the next turn boundary (never blocks on a running agent)
|
|
455
|
-
|
|
466
|
+
// the model at the next turn boundary (never blocks on a running agent).
|
|
467
|
+
// promptId ties the UI's pending echo to the later prompt-delivered note.
|
|
468
|
+
const promptId = a.enqueuePrompt(body.text, "user");
|
|
456
469
|
if (body.start !== false && a.status !== "running")
|
|
457
470
|
a.start("prompt");
|
|
458
|
-
return c.json({ ok: true, queued: a.snapshot().pendingPrompts });
|
|
471
|
+
return c.json({ ok: true, promptId, queued: a.snapshot().pendingPrompts });
|
|
472
|
+
});
|
|
473
|
+
// Withdraw a still-pending prompt; returns its text for the composer draft.
|
|
474
|
+
app.post("/api/agents/:id/prompt/cancel", async (c) => {
|
|
475
|
+
const a = master.agents.get(c.req.param("id"));
|
|
476
|
+
if (!a)
|
|
477
|
+
return c.json({ error: "not found" }, 404);
|
|
478
|
+
const body = await c.req.json();
|
|
479
|
+
if (!body.promptId)
|
|
480
|
+
return c.json({ error: "promptId required" }, 400);
|
|
481
|
+
const text = a.cancelPrompt(body.promptId);
|
|
482
|
+
if (text === null)
|
|
483
|
+
return c.json({ error: "not pending (already delivered?)" }, 409);
|
|
484
|
+
return c.json({ ok: true, text });
|
|
459
485
|
});
|
|
460
486
|
app.post("/api/agents/:id/start", (c) => {
|
|
461
487
|
const a = master.agents.get(c.req.param("id"));
|
|
@@ -534,6 +560,8 @@ export function buildApp(master) {
|
|
|
534
560
|
const body = await c.req.json();
|
|
535
561
|
if (body.text) {
|
|
536
562
|
await a.setGoal(body.text);
|
|
563
|
+
if (typeof body.verify === "string" && body.verify.trim())
|
|
564
|
+
await a.setGoalVerify(body.verify.trim());
|
|
537
565
|
// goals live behind get_goal(), so a silent save would go unnoticed —
|
|
538
566
|
// queue a harness prompt unless the caller explicitly declines
|
|
539
567
|
if (body.notify !== false)
|
|
@@ -701,12 +729,33 @@ export function buildApp(master) {
|
|
|
701
729
|
return c.json({ ok: true, ...r });
|
|
702
730
|
});
|
|
703
731
|
// ---- event log access (human/inspect friendly) ----
|
|
732
|
+
// mtime-keyed parse cache: the web UI polls this endpoint every ~400ms and
|
|
733
|
+
// re-reading + re-parsing the WHOLE chat.jsonl each time dominated CPU on
|
|
734
|
+
// multi-MB sessions. The file only ever appends, so an unchanged mtime lets
|
|
735
|
+
// us serve the previous parse verbatim.
|
|
736
|
+
const eventsCache = new Map();
|
|
737
|
+
async function readEventsCached(filePath) {
|
|
738
|
+
try {
|
|
739
|
+
const st = await fs.stat(filePath);
|
|
740
|
+
const hit = eventsCache.get(filePath);
|
|
741
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
|
|
742
|
+
return hit.events;
|
|
743
|
+
const events = await readEvents(filePath);
|
|
744
|
+
if (eventsCache.size > 64)
|
|
745
|
+
eventsCache.clear();
|
|
746
|
+
eventsCache.set(filePath, { mtimeMs: st.mtimeMs, size: st.size, events });
|
|
747
|
+
return events;
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return readEvents(filePath);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
704
753
|
app.get("/api/agents/:id/events", async (c) => {
|
|
705
754
|
const a = master.agents.get(c.req.param("id"));
|
|
706
755
|
if (!a)
|
|
707
756
|
return c.json({ error: "not found" }, 404);
|
|
708
757
|
const limit = Math.min(Number(c.req.query("limit") ?? 200), 5000);
|
|
709
|
-
let events = await
|
|
758
|
+
let events = await readEventsCached(a.log.filePath);
|
|
710
759
|
const branch = c.req.query("branch");
|
|
711
760
|
const session = c.req.query("session");
|
|
712
761
|
if (branch)
|