om-memory-system 4.35.0-next.193 → 5.1.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/core/skill-operations.js +7 -3
- package/dist/core/skills-tool.js +1 -1
- package/dist/services/skill-learning/apply.d.ts +30 -2
- package/dist/services/skill-learning/apply.js +39 -24
- package/dist/services/skill-learning/drafting.d.ts +13 -0
- package/dist/services/skill-learning/drafting.js +1 -0
- package/dist/services/skill-learning/failure-explain.d.ts +52 -0
- package/dist/services/skill-learning/failure-explain.js +131 -0
- package/dist/services/skill-learning/failures.d.ts +57 -0
- package/dist/services/skill-learning/failures.js +103 -0
- package/dist/services/skill-learning/history.d.ts +9 -9
- package/dist/services/skill-learning/history.js +66 -45
- package/dist/services/skill-learning/line-diff.d.ts +5 -0
- package/dist/services/skill-learning/line-diff.js +14 -0
- package/dist/services/skill-learning/proposal-store.d.ts +9 -2
- package/dist/services/skill-learning/proposal-store.js +13 -12
- package/dist/services/skill-learning/proposal-summaries.d.ts +52 -0
- package/dist/services/skill-learning/proposal-summaries.js +73 -0
- package/dist/services/skill-learning/turn-summaries.d.ts +8 -0
- package/dist/services/skill-learning/turn-summaries.js +9 -4
- package/dist/services/skill-routes.js +60 -7
- package/dist/web/assets/index-D6UWwxhU.css +2 -0
- package/dist/web/assets/index-DXmCX7Ga.js +170 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-ExMzA2L6.css +0 -2
- package/dist/web/assets/index-OPIzwy09.js +0 -169
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { applyProposal } from "../services/skill-learning/apply.js";
|
|
2
2
|
import { draftImprovement, draftNewSkill, findSkillIdeas, } from "../services/skill-learning/drafting.js";
|
|
3
|
-
import {
|
|
3
|
+
import { withCanUndo } from "../services/skill-learning/history.js";
|
|
4
|
+
import { getProposal, updateProposalState, } from "../services/skill-learning/proposal-store.js";
|
|
5
|
+
import { listProposalSummaries, } from "../services/skill-learning/proposal-summaries.js";
|
|
4
6
|
import { SKILL_ORDERS, rankSkills, } from "../services/skill-learning/ranking.js";
|
|
5
7
|
import { getSkillIndex } from "../services/skill-learning/skill-index.js";
|
|
6
8
|
import { evidence, propose, undo } from "./skill-agent-modes.js";
|
|
@@ -193,11 +195,13 @@ async function proposals(args, deps) {
|
|
|
193
195
|
if (state && !PROPOSAL_STATES.includes(state)) {
|
|
194
196
|
return reply("proposals", false, "invalid-args", `state must be one of: ${PROPOSAL_STATES.join(", ")}.`);
|
|
195
197
|
}
|
|
196
|
-
|
|
198
|
+
// Summaries, not full proposals: the JSON goes into the agent's context. `show` gives the text.
|
|
199
|
+
const { proposals: found } = await listProposalSummaries(deps.store, state ? { state } : {});
|
|
197
200
|
const text = found.length === 0
|
|
198
201
|
? `No ${state ? `${state} ` : ""}proposals.`
|
|
199
202
|
: ["Proposals, newest first:", ...found.map(proposalLine)].join("\n");
|
|
200
|
-
|
|
203
|
+
const listed = await withCanUndo({ store: deps.store, folders: deps.folders }, found);
|
|
204
|
+
return reply("proposals", true, "ok", text, { proposals: listed });
|
|
201
205
|
}
|
|
202
206
|
/** The proposal for `args.id`, or the reply that explains why there is none. */
|
|
203
207
|
async function findProposal(mode, args, deps) {
|
package/dist/core/skills-tool.js
CHANGED
|
@@ -23,7 +23,7 @@ export const SKILLS_TOOL_DESCRIPTION = [
|
|
|
23
23
|
'"draft" (skill, or an idea from "ideas" {name, description, reason, evidenceTurnIds} with folder): the external API drafts a pending proposal; it changes no file.',
|
|
24
24
|
'To fix a skill yourself: call "evidence" (skill, limit) for its SKILL.md and recorded turns, write the fixed SKILL.md, then call "propose" (skill, or idea with folder; content, summary, evidence turn ids). Show the user the diff.',
|
|
25
25
|
"This path makes no external API call, but the recorded turn text enters this session.",
|
|
26
|
-
'"proposals" (state), "show" (id):
|
|
26
|
+
'"proposals" (state): list proposals as summaries, with line counts and no content or diff. "show" (id): one proposal with its diff and content.',
|
|
27
27
|
'"apply" (id): write a pending proposal to its SKILL.md, only when the user asks. "reject" (id), "undo" (id), "help".',
|
|
28
28
|
].join(" ");
|
|
29
29
|
/** JSON Schema for the tool input; the V2 adapter passes it as is. */
|
|
@@ -42,8 +42,35 @@ export declare function hashSkillContent(content: string | Buffer): string;
|
|
|
42
42
|
* previous content before the write. Expected outcomes return a result code.
|
|
43
43
|
*/
|
|
44
44
|
export declare function applyProposal(id: string, by: SkillAppliedBy, deps: ApplyDeps): Promise<ApplyResult>;
|
|
45
|
-
/**
|
|
46
|
-
export
|
|
45
|
+
/** What `checkUndo` needs. A list can read these for many proposals in one query. */
|
|
46
|
+
export interface UndoFacts {
|
|
47
|
+
id: string;
|
|
48
|
+
readOnly: boolean;
|
|
49
|
+
/** The proposal's state. */
|
|
50
|
+
state: string;
|
|
51
|
+
/** The newest saved version of the proposal, or null when it has none. */
|
|
52
|
+
version: {
|
|
53
|
+
path: string;
|
|
54
|
+
writtenHash: string;
|
|
55
|
+
} | null;
|
|
56
|
+
folders: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
type UndoCheck = {
|
|
59
|
+
ok: true;
|
|
60
|
+
real: string;
|
|
61
|
+
folders: string[];
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
code: "read-only" | "not-applied" | "no-version" | "outside-folder" | "changed";
|
|
65
|
+
message: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Every undo check that needs no database read: the store is writable, the
|
|
69
|
+
* proposal is applied, a version exists, its file is a `SKILL.md` inside a
|
|
70
|
+
* watched folder, and the file still holds what OMMS wrote. Reads one file.
|
|
71
|
+
* `planUndo` and the `canUndo` of a list both run it, so they cannot disagree.
|
|
72
|
+
*/
|
|
73
|
+
export declare function checkUndo(facts: UndoFacts): UndoCheck;
|
|
47
74
|
/**
|
|
48
75
|
* Restores the content saved before the last apply, or deletes the file the
|
|
49
76
|
* apply created. Only when the file still matches what OMMS wrote; otherwise
|
|
@@ -51,3 +78,4 @@ export declare function canUndoProposal(id: string, deps: ApplyDeps): Promise<bo
|
|
|
51
78
|
* later change has rewritten the file, until that later change is undone.
|
|
52
79
|
*/
|
|
53
80
|
export declare function undoProposal(id: string, deps: ApplyDeps): Promise<UndoResult>;
|
|
81
|
+
export {};
|
|
@@ -209,42 +209,57 @@ function removeEmptyFolder(folder, roots) {
|
|
|
209
209
|
// The folder is gone or not empty; leave it.
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
return { ok: false, code: "not-
|
|
225
|
-
if (proposal.state !== "applied") {
|
|
226
|
-
return { ok: false, code: "not-applied", message: `Proposal ${id} is ${proposal.state}` };
|
|
212
|
+
const READ_ONLY_MESSAGE = "skills.db is from a newer OMMS and is read-only";
|
|
213
|
+
/**
|
|
214
|
+
* Every undo check that needs no database read: the store is writable, the
|
|
215
|
+
* proposal is applied, a version exists, its file is a `SKILL.md` inside a
|
|
216
|
+
* watched folder, and the file still holds what OMMS wrote. Reads one file.
|
|
217
|
+
* `planUndo` and the `canUndo` of a list both run it, so they cannot disagree.
|
|
218
|
+
*/
|
|
219
|
+
export function checkUndo(facts) {
|
|
220
|
+
const { id, state, version } = facts;
|
|
221
|
+
if (facts.readOnly)
|
|
222
|
+
return { ok: false, code: "read-only", message: READ_ONLY_MESSAGE };
|
|
223
|
+
if (state !== "applied") {
|
|
224
|
+
return { ok: false, code: "not-applied", message: `Proposal ${id} is ${state}` };
|
|
227
225
|
}
|
|
228
|
-
const version = await getLatestVersion(store, id);
|
|
229
226
|
if (!version)
|
|
230
227
|
return { ok: false, code: "no-version", message: `No saved version for ${id}` };
|
|
231
|
-
const target = checkTarget(version.path,
|
|
228
|
+
const target = checkTarget(version.path, facts.folders);
|
|
232
229
|
if (!target.ok)
|
|
233
230
|
return { ok: false, code: "outside-folder", message: target.message };
|
|
234
|
-
|
|
235
|
-
if (readHash(real) !== version.writtenHash) {
|
|
231
|
+
if (readHash(target.real) !== version.writtenHash) {
|
|
236
232
|
return {
|
|
237
233
|
ok: false,
|
|
238
234
|
code: "changed",
|
|
239
235
|
message: "The file changed since OMMS wrote it, so undo would lose that change",
|
|
240
|
-
savedContent: version.previousContent,
|
|
241
236
|
};
|
|
242
237
|
}
|
|
243
|
-
return { ok: true,
|
|
238
|
+
return { ok: true, real: target.real, folders: target.folders };
|
|
244
239
|
}
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
/** Every check undo makes before it writes. Reads only; changes nothing. */
|
|
241
|
+
async function planUndo(id, deps) {
|
|
242
|
+
const { store } = deps;
|
|
243
|
+
// A read-only store refuses before it looks the proposal up.
|
|
244
|
+
if (store.isReadOnly())
|
|
245
|
+
return { ok: false, code: "read-only", message: READ_ONLY_MESSAGE };
|
|
246
|
+
const proposal = await getProposal(store, id);
|
|
247
|
+
if (!proposal)
|
|
248
|
+
return { ok: false, code: "not-found", message: `No proposal ${id}` };
|
|
249
|
+
const version = await getLatestVersion(store, id);
|
|
250
|
+
const check = checkUndo({
|
|
251
|
+
id,
|
|
252
|
+
readOnly: false,
|
|
253
|
+
state: proposal.state,
|
|
254
|
+
version,
|
|
255
|
+
folders: deps.folders,
|
|
256
|
+
});
|
|
257
|
+
if (!check.ok) {
|
|
258
|
+
return check.code === "changed"
|
|
259
|
+
? { ...check, savedContent: version?.previousContent ?? null }
|
|
260
|
+
: check;
|
|
261
|
+
}
|
|
262
|
+
return { ok: true, proposal, version: version, real: check.real, folders: check.folders };
|
|
248
263
|
}
|
|
249
264
|
/**
|
|
250
265
|
* Restores the content saved before the last apply, or deletes the file the
|
|
@@ -50,6 +50,19 @@ export type IdeasResult = {
|
|
|
50
50
|
ok: true;
|
|
51
51
|
ideas: SkillIdea[];
|
|
52
52
|
} | DraftFailure;
|
|
53
|
+
declare function tool(name: string, description: string, properties: Record<string, unknown>): {
|
|
54
|
+
type: string;
|
|
55
|
+
function: {
|
|
56
|
+
name: string;
|
|
57
|
+
description: string;
|
|
58
|
+
parameters: {
|
|
59
|
+
type: string;
|
|
60
|
+
properties: Record<string, unknown>;
|
|
61
|
+
required: string[];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export { tool };
|
|
53
66
|
export declare function withFinalNewline(text: string): string;
|
|
54
67
|
export declare function sameText(a: string, b: string): boolean;
|
|
55
68
|
export declare const READ_ONLY: DraftFailure;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ProfileFailureCode } from "../../core/profile-failure.js";
|
|
2
|
+
import { type SkillModel } from "./drafting.js";
|
|
3
|
+
import { type FailureGroup } from "./failures.js";
|
|
4
|
+
import type { SkillIndex } from "./skill-index.js";
|
|
5
|
+
import type { SkillStore } from "./store.js";
|
|
6
|
+
/** At most this many groups go to the model, most calls first. */
|
|
7
|
+
export declare const MAX_EXPLAIN_GROUPS = 10;
|
|
8
|
+
/** One input excerpt per group, cut to this many characters. */
|
|
9
|
+
export declare const EXPLAIN_EXCERPT_CHARS = 500;
|
|
10
|
+
/** What a cause is blamed on; the panel shows the label as a lead. */
|
|
11
|
+
export type ExplainCauseKind = "skill" | "harness" | "environment" | "unknown";
|
|
12
|
+
export interface ExplainCause {
|
|
13
|
+
kind: ExplainCauseKind;
|
|
14
|
+
explanation: string;
|
|
15
|
+
actions: string[];
|
|
16
|
+
}
|
|
17
|
+
/** The model's reply: a summary and labelled causes. Shown, never saved. */
|
|
18
|
+
export interface FailureExplanation {
|
|
19
|
+
summary: string;
|
|
20
|
+
causes: ExplainCause[];
|
|
21
|
+
}
|
|
22
|
+
export type ExplainFailureCode = ProfileFailureCode | "not-found" | "invalid-args";
|
|
23
|
+
export type ExplainResult = {
|
|
24
|
+
ok: true;
|
|
25
|
+
explanation: FailureExplanation;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
code: ExplainFailureCode;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
export interface ExplainDeps {
|
|
32
|
+
store: SkillStore;
|
|
33
|
+
index: SkillIndex;
|
|
34
|
+
model: SkillModel;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds the request the test asserts on: the skill name, its `SKILL.md` when
|
|
38
|
+
* a watched folder holds it, and the top groups. No prompts, no replies.
|
|
39
|
+
*/
|
|
40
|
+
export declare function explainUserPrompt(skill: {
|
|
41
|
+
name: string;
|
|
42
|
+
path: string | null;
|
|
43
|
+
}, skillFile: string | null, groups: readonly FailureGroup[]): string;
|
|
44
|
+
/**
|
|
45
|
+
* Asks the external API model to explain one skill's failure groups (design
|
|
46
|
+
* §9). The reply is returned, never saved. Reuses the drafting model path and
|
|
47
|
+
* its failure codes, including `not-configured`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function explainFailures(skill: {
|
|
50
|
+
name: string;
|
|
51
|
+
path: string | null;
|
|
52
|
+
}, deps: ExplainDeps): Promise<ExplainResult>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { profileFailureCode, profileResultCode, } from "../../core/profile-failure.js";
|
|
4
|
+
import { log } from "../logger.js";
|
|
5
|
+
import { tool } from "./drafting.js";
|
|
6
|
+
import { skillFailureGroups } from "./failures.js";
|
|
7
|
+
/** At most this many groups go to the model, most calls first. */
|
|
8
|
+
export const MAX_EXPLAIN_GROUPS = 10;
|
|
9
|
+
/** One input excerpt per group, cut to this many characters. */
|
|
10
|
+
export const EXPLAIN_EXCERPT_CHARS = 500;
|
|
11
|
+
const ExplainReply = z.object({
|
|
12
|
+
summary: z.string(),
|
|
13
|
+
causes: z
|
|
14
|
+
.array(z.object({
|
|
15
|
+
kind: z.enum(["skill", "harness", "environment", "unknown"]),
|
|
16
|
+
explanation: z.string(),
|
|
17
|
+
actions: z.array(z.string()).default([]),
|
|
18
|
+
}))
|
|
19
|
+
.default([]),
|
|
20
|
+
});
|
|
21
|
+
const EXPLAIN_TOOL = tool("explain_skill_failures", "Explain a skill's failed tool calls", {
|
|
22
|
+
summary: { type: "string", description: "One or two plain sentences on what fails and why" },
|
|
23
|
+
causes: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
kind: {
|
|
29
|
+
type: "string",
|
|
30
|
+
enum: ["skill", "harness", "environment", "unknown"],
|
|
31
|
+
description: "skill: the SKILL.md should change; harness: a host or harness bug; environment: machine, setup or credentials; unknown",
|
|
32
|
+
},
|
|
33
|
+
explanation: { type: "string", description: "What the error text shows" },
|
|
34
|
+
actions: {
|
|
35
|
+
type: "array",
|
|
36
|
+
items: { type: "string" },
|
|
37
|
+
description: "Suggested actions, as leads from the error text only",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["kind", "explanation", "actions"],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const EXPLAIN_SYSTEM = [
|
|
45
|
+
"You explain why a coding agent's tool calls failed while it used one skill (SKILL.md).",
|
|
46
|
+
"You see the skill file and its failure groups: tool name, first error line, counts, the last time seen and one short input excerpt. You cannot see the host's code.",
|
|
47
|
+
"Label each cause skill, harness, environment or unknown. Treat your labels and suggested actions as leads from error text only, not conclusions.",
|
|
48
|
+
"Write plain British English. Call the explain_skill_failures tool.",
|
|
49
|
+
].join(" ");
|
|
50
|
+
const FAILURE_TEXT = {
|
|
51
|
+
"not-configured": "The external API is not set up. Set it up in the External API card.",
|
|
52
|
+
"invalid-args": "This skill has no failed tool calls to explain.",
|
|
53
|
+
"invalid-reply": "The model reply was not valid, so nothing is shown. Try again.",
|
|
54
|
+
timeout: "The model did not reply in time",
|
|
55
|
+
};
|
|
56
|
+
/** Log one metadata record with the reason code only, and return the failure. */
|
|
57
|
+
function fail(step, code) {
|
|
58
|
+
log(`Skill ${step} failed`, { host: null, reason: code }, "error");
|
|
59
|
+
return { ok: false, code, message: FAILURE_TEXT[code] ?? `Explaining failed: ${code}` };
|
|
60
|
+
}
|
|
61
|
+
function groupLine(group) {
|
|
62
|
+
const parts = [
|
|
63
|
+
`- Tool: ${group.name}`,
|
|
64
|
+
`Error: ${group.errorLine ?? "(no error text)"}`,
|
|
65
|
+
`Calls: ${group.calls} over ${group.turns} turns`,
|
|
66
|
+
`Last seen: ${new Date(group.lastSeenAt).toISOString()}`,
|
|
67
|
+
];
|
|
68
|
+
const call = group.latest[0];
|
|
69
|
+
if (call) {
|
|
70
|
+
parts.push(`Input excerpt: "${call.input.slice(0, EXPLAIN_EXCERPT_CHARS)}"`);
|
|
71
|
+
}
|
|
72
|
+
return parts.join("; ");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Builds the request the test asserts on: the skill name, its `SKILL.md` when
|
|
76
|
+
* a watched folder holds it, and the top groups. No prompts, no replies.
|
|
77
|
+
*/
|
|
78
|
+
export function explainUserPrompt(skill, skillFile, groups) {
|
|
79
|
+
const file = skillFile === null ? "(not in a watched folder; no SKILL.md)" : `<<<\n${skillFile}\n>>>`;
|
|
80
|
+
return [
|
|
81
|
+
`Skill: ${skill.name}`,
|
|
82
|
+
`SKILL.md:\n${file}`,
|
|
83
|
+
`Failure groups, most calls first:`,
|
|
84
|
+
groups.map(groupLine).join("\n") || "(none)",
|
|
85
|
+
].join("\n\n");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Asks the external API model to explain one skill's failure groups (design
|
|
89
|
+
* §9). The reply is returned, never saved. Reuses the drafting model path and
|
|
90
|
+
* its failure codes, including `not-configured`.
|
|
91
|
+
*/
|
|
92
|
+
export async function explainFailures(skill, deps) {
|
|
93
|
+
if (!deps.model.ready)
|
|
94
|
+
return fail("explain failures", "not-configured");
|
|
95
|
+
const groups = (await skillFailureGroups(deps.store, skill, deps.index)).slice(0, MAX_EXPLAIN_GROUPS);
|
|
96
|
+
if (groups.length === 0)
|
|
97
|
+
return fail("explain failures", "invalid-args");
|
|
98
|
+
let skillFile = null;
|
|
99
|
+
if (skill.path) {
|
|
100
|
+
try {
|
|
101
|
+
skillFile = readFileSync(skill.path, "utf8");
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
skillFile = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const user = explainUserPrompt(skill, skillFile, groups);
|
|
108
|
+
let result;
|
|
109
|
+
try {
|
|
110
|
+
result = await deps.model.callTool(EXPLAIN_SYSTEM, user, EXPLAIN_TOOL);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return fail("explain failures", profileFailureCode(error));
|
|
114
|
+
}
|
|
115
|
+
if (!result.success)
|
|
116
|
+
return fail("explain failures", profileResultCode(result));
|
|
117
|
+
const parsed = ExplainReply.safeParse(result.data);
|
|
118
|
+
if (!parsed.success)
|
|
119
|
+
return fail("explain failures", "invalid-reply");
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
explanation: {
|
|
123
|
+
summary: parsed.data.summary.trim(),
|
|
124
|
+
causes: parsed.data.causes.map((cause) => ({
|
|
125
|
+
kind: cause.kind,
|
|
126
|
+
explanation: cause.explanation.trim(),
|
|
127
|
+
actions: cause.actions.map((action) => action.trim()).filter(Boolean),
|
|
128
|
+
})),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SkillIndex } from "./skill-index.js";
|
|
2
|
+
import type { SkillStore } from "./store.js";
|
|
3
|
+
/** The server sends at most this many characters of one call's input (design §3). */
|
|
4
|
+
export declare const FAILURE_INPUT_CAP = 20000;
|
|
5
|
+
/** A group key is the first line of the error text, cut to this length (design §4). */
|
|
6
|
+
export declare const FAILURE_LINE_CHARS = 200;
|
|
7
|
+
/** How many of a group's latest calls the reply carries. */
|
|
8
|
+
export declare const FAILURE_SAMPLE = 5;
|
|
9
|
+
/** How many groups the reply carries, most calls first. */
|
|
10
|
+
export declare const FAILURE_GROUP_LIMIT = 50;
|
|
11
|
+
/** One failed tool call of a turn, as the dialog shows it. */
|
|
12
|
+
export interface TurnFailure {
|
|
13
|
+
position: number;
|
|
14
|
+
name: string;
|
|
15
|
+
input: string;
|
|
16
|
+
/** True when the stored input was longer than `FAILURE_INPUT_CAP`. */
|
|
17
|
+
inputCut: boolean;
|
|
18
|
+
/** Null when the host reported no error text. */
|
|
19
|
+
error: string | null;
|
|
20
|
+
}
|
|
21
|
+
/** One failed call of a group: the call fields plus its turn's time and project. */
|
|
22
|
+
export interface FailureGroupCall extends TurnFailure {
|
|
23
|
+
turnId: number;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
projectName: string | null;
|
|
26
|
+
}
|
|
27
|
+
/** The failed calls of one skill that count against it, grouped by tool and first error line. */
|
|
28
|
+
export interface FailureGroup {
|
|
29
|
+
/** The tool name. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** The first line of the error text, cut to `FAILURE_LINE_CHARS`; null when no error text. */
|
|
32
|
+
errorLine: string | null;
|
|
33
|
+
/** Calls in the group. */
|
|
34
|
+
calls: number;
|
|
35
|
+
/** Turns the calls span. */
|
|
36
|
+
turns: number;
|
|
37
|
+
/** When the newest call of the group happened. */
|
|
38
|
+
lastSeenAt: number;
|
|
39
|
+
/** The latest `FAILURE_SAMPLE` calls, newest first. */
|
|
40
|
+
latest: FailureGroupCall[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Every failed call of one turn, ordered by position. Null when the turn does
|
|
44
|
+
* not exist; an empty list when it has no failed call. A missing `skills.db`
|
|
45
|
+
* gives an empty list and stays missing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function turnFailures(store: SkillStore, turnId: number): Promise<TurnFailure[] | null>;
|
|
48
|
+
/**
|
|
49
|
+
* The failed calls of one skill that count against it by the ranking rule,
|
|
50
|
+
* grouped by tool name and the first line of the error text, most calls first.
|
|
51
|
+
* At most `FAILURE_GROUP_LIMIT` groups come back, each with its latest calls.
|
|
52
|
+
* A missing `skills.db` gives an empty list and stays missing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function skillFailureGroups(store: SkillStore, skill: {
|
|
55
|
+
name: string;
|
|
56
|
+
path: string | null;
|
|
57
|
+
}, index: SkillIndex): Promise<FailureGroup[]>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { skillUsesFor } from "./turn-summaries.js";
|
|
2
|
+
/** The server sends at most this many characters of one call's input (design §3). */
|
|
3
|
+
export const FAILURE_INPUT_CAP = 20_000;
|
|
4
|
+
/** A group key is the first line of the error text, cut to this length (design §4). */
|
|
5
|
+
export const FAILURE_LINE_CHARS = 200;
|
|
6
|
+
/** How many of a group's latest calls the reply carries. */
|
|
7
|
+
export const FAILURE_SAMPLE = 5;
|
|
8
|
+
/** How many groups the reply carries, most calls first. */
|
|
9
|
+
export const FAILURE_GROUP_LIMIT = 50;
|
|
10
|
+
// An empty error text is no error text, as a null one is: both show as
|
|
11
|
+
// "No error text recorded". The first line of the error is the group key; a
|
|
12
|
+
// trailing carriage return from CRLF text is not part of the key.
|
|
13
|
+
const NO_ERROR = "tc.error IS NULL OR tc.error = ''";
|
|
14
|
+
const LINE = `CASE WHEN ${NO_ERROR} THEN NULL
|
|
15
|
+
ELSE rtrim(substr(substr(tc.error, 1, instr(tc.error || char(10), char(10)) - 1), 1, ${FAILURE_LINE_CHARS}), char(13)) END`;
|
|
16
|
+
const CALL_FIELDS = `
|
|
17
|
+
tc.position, tc.name,
|
|
18
|
+
substr(tc.input, 1, ${FAILURE_INPUT_CAP}) AS input,
|
|
19
|
+
length(tc.input) > ${FAILURE_INPUT_CAP} AS input_cut,
|
|
20
|
+
CASE WHEN ${NO_ERROR} THEN NULL ELSE tc.error END AS error`;
|
|
21
|
+
function callRow(row) {
|
|
22
|
+
return {
|
|
23
|
+
position: Number(row.position),
|
|
24
|
+
name: String(row.name),
|
|
25
|
+
input: String(row.input),
|
|
26
|
+
inputCut: Number(row.input_cut) === 1,
|
|
27
|
+
error: row.error == null ? null : String(row.error),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Every failed call of one turn, ordered by position. Null when the turn does
|
|
32
|
+
* not exist; an empty list when it has no failed call. A missing `skills.db`
|
|
33
|
+
* gives an empty list and stays missing.
|
|
34
|
+
*/
|
|
35
|
+
export async function turnFailures(store, turnId) {
|
|
36
|
+
const db = await store.existingDb();
|
|
37
|
+
if (!db)
|
|
38
|
+
return [];
|
|
39
|
+
const turn = await db.get("SELECT id FROM turns WHERE id = ?", [turnId]);
|
|
40
|
+
if (!turn)
|
|
41
|
+
return null;
|
|
42
|
+
const rows = await db.all(`SELECT ${CALL_FIELDS} FROM tool_calls tc
|
|
43
|
+
WHERE tc.turn_id = ? AND tc.outcome = 'error' ORDER BY tc.position, tc.id`, [turnId]);
|
|
44
|
+
return rows.map(callRow);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The failed calls of one skill that count against it by the ranking rule,
|
|
48
|
+
* grouped by tool name and the first line of the error text, most calls first.
|
|
49
|
+
* At most `FAILURE_GROUP_LIMIT` groups come back, each with its latest calls.
|
|
50
|
+
* A missing `skills.db` gives an empty list and stays missing.
|
|
51
|
+
*/
|
|
52
|
+
export async function skillFailureGroups(store, skill, index) {
|
|
53
|
+
const db = await store.existingDb();
|
|
54
|
+
if (!db)
|
|
55
|
+
return [];
|
|
56
|
+
const uses = skillUsesFor(skill, index);
|
|
57
|
+
// The earliest use of this skill only, as the ranking measures it; another
|
|
58
|
+
// skill used earlier in the same turn does not move the line.
|
|
59
|
+
const firstUse = `SELECT u.turn_id, MIN(COALESCE(u.position, -1)) AS position
|
|
60
|
+
FROM (${uses.sql}) u GROUP BY u.turn_id`;
|
|
61
|
+
const failed = `SELECT tc.id, tc.turn_id, tc.position, tc.name, t.created_at, t.project_name, ${LINE} AS line,
|
|
62
|
+
substr(tc.input, 1, ${FAILURE_INPUT_CAP}) AS input,
|
|
63
|
+
length(tc.input) > ${FAILURE_INPUT_CAP} AS input_cut,
|
|
64
|
+
CASE WHEN ${NO_ERROR} THEN NULL ELSE tc.error END AS error
|
|
65
|
+
FROM tool_calls tc
|
|
66
|
+
JOIN turns t ON t.id = tc.turn_id
|
|
67
|
+
JOIN (${firstUse}) f ON f.turn_id = tc.turn_id
|
|
68
|
+
WHERE tc.outcome = 'error' AND tc.position > f.position`;
|
|
69
|
+
const top = `SELECT name, line, COUNT(*) AS calls, COUNT(DISTINCT turn_id) AS turn_count,
|
|
70
|
+
MAX(created_at) AS last_seen
|
|
71
|
+
FROM (${failed}) GROUP BY name, line
|
|
72
|
+
ORDER BY calls DESC, last_seen DESC, name, line LIMIT ${FAILURE_GROUP_LIMIT}`;
|
|
73
|
+
const groups = await db.all(top, uses.args);
|
|
74
|
+
if (groups.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
// The latest calls of the returned groups in one query: window functions work in the
|
|
77
|
+
// store's libSQL (checked for this change), so no JavaScript fallback is needed.
|
|
78
|
+
const latest = await db.all(`SELECT turn_id, created_at, project_name, position, name, line, input, input_cut, error
|
|
79
|
+
FROM (SELECT g.*, ROW_NUMBER() OVER (PARTITION BY g.name, g.line ORDER BY g.created_at DESC, g.id DESC) AS rn
|
|
80
|
+
FROM (${failed}) g
|
|
81
|
+
JOIN (${top}) k ON k.name = g.name AND k.line IS g.line)
|
|
82
|
+
WHERE rn <= ${FAILURE_SAMPLE} ORDER BY name, line, rn`, [...uses.args, ...uses.args]);
|
|
83
|
+
const byGroup = new Map();
|
|
84
|
+
for (const row of latest) {
|
|
85
|
+
const key = JSON.stringify([String(row.name), row.line == null ? null : String(row.line)]);
|
|
86
|
+
const list = byGroup.get(key) ?? [];
|
|
87
|
+
list.push({
|
|
88
|
+
...callRow(row),
|
|
89
|
+
turnId: Number(row.turn_id),
|
|
90
|
+
createdAt: Number(row.created_at),
|
|
91
|
+
projectName: row.project_name == null ? null : String(row.project_name),
|
|
92
|
+
});
|
|
93
|
+
byGroup.set(key, list);
|
|
94
|
+
}
|
|
95
|
+
return groups.map((group) => ({
|
|
96
|
+
name: String(group.name),
|
|
97
|
+
errorLine: group.line == null ? null : String(group.line),
|
|
98
|
+
calls: Number(group.calls),
|
|
99
|
+
turns: Number(group.turn_count),
|
|
100
|
+
lastSeenAt: Number(group.last_seen),
|
|
101
|
+
latest: byGroup.get(JSON.stringify([String(group.name), group.line == null ? null : String(group.line)])) ?? [],
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ApplyDeps } from "./apply.js";
|
|
2
|
-
import {
|
|
2
|
+
import { countDiffLines } from "./line-diff.js";
|
|
3
3
|
export type SkillHistoryKind = "created" | "applied" | "rejected" | "undone" | "stale";
|
|
4
4
|
export interface SkillHistoryEntry {
|
|
5
5
|
/** `<proposal id>:created` or `<proposal id>:<state>`. */
|
|
@@ -19,11 +19,7 @@ export interface SkillHistoryEntry {
|
|
|
19
19
|
added: number;
|
|
20
20
|
removed: number;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
export declare function countDiffLines(diff: string | null): {
|
|
24
|
-
added: number;
|
|
25
|
-
removed: number;
|
|
26
|
-
};
|
|
22
|
+
export { countDiffLines };
|
|
27
23
|
/**
|
|
28
24
|
* The change history of one skill, newest first: a `created` entry for each
|
|
29
25
|
* proposal and a second entry once it was applied, rejected, undone or found
|
|
@@ -40,9 +36,13 @@ export declare function skillHistory(deps: ApplyDeps, skill: {
|
|
|
40
36
|
}>;
|
|
41
37
|
/**
|
|
42
38
|
* Each proposal with `canUndo`: true only for an applied proposal that undo
|
|
43
|
-
* would accept now, so false once a later change rewrote the file. Reads
|
|
44
|
-
* saved
|
|
39
|
+
* would accept now, so false once a later change rewrote the file. Reads the
|
|
40
|
+
* newest saved versions of all applied proposals in one query, then one file
|
|
41
|
+
* hash per applied proposal, and nothing for the rest.
|
|
45
42
|
*/
|
|
46
|
-
export declare function withCanUndo
|
|
43
|
+
export declare function withCanUndo<T extends {
|
|
44
|
+
id: string;
|
|
45
|
+
state: string;
|
|
46
|
+
}>(deps: ApplyDeps, proposals: readonly T[]): Promise<(T & {
|
|
47
47
|
canUndo: boolean;
|
|
48
48
|
})[]>;
|