@alisio/core 0.6.0 → 0.8.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.
Files changed (97) hide show
  1. package/dist/application.d.ts +15 -0
  2. package/dist/application.js +38 -3
  3. package/dist/changelog/data.js +39 -0
  4. package/dist/commands/builtins.js +7 -0
  5. package/dist/config.d.ts +30 -0
  6. package/dist/config.js +32 -0
  7. package/dist/core/runner.d.ts +4 -0
  8. package/dist/core/runner.js +57 -1
  9. package/dist/eval/canonical.d.ts +14 -0
  10. package/dist/eval/canonical.js +40 -0
  11. package/dist/eval/cassette.d.ts +180 -0
  12. package/dist/eval/cassette.js +424 -0
  13. package/dist/eval/compare.d.ts +147 -0
  14. package/dist/eval/compare.js +436 -0
  15. package/dist/eval/engine.d.ts +145 -0
  16. package/dist/eval/engine.js +731 -0
  17. package/dist/eval/export.d.ts +40 -0
  18. package/dist/eval/export.js +90 -0
  19. package/dist/eval/fingerprint.d.ts +88 -0
  20. package/dist/eval/fingerprint.js +186 -0
  21. package/dist/eval/normalize.d.ts +68 -0
  22. package/dist/eval/normalize.js +138 -0
  23. package/dist/eval/paths.d.ts +11 -0
  24. package/dist/eval/paths.js +50 -0
  25. package/dist/eval/plan.d.ts +26 -0
  26. package/dist/eval/plan.js +44 -0
  27. package/dist/eval/report.d.ts +53 -0
  28. package/dist/eval/report.js +258 -0
  29. package/dist/eval/run-suite.d.ts +68 -0
  30. package/dist/eval/run-suite.js +94 -0
  31. package/dist/eval/secrets.d.ts +14 -0
  32. package/dist/eval/secrets.js +55 -0
  33. package/dist/eval/stats.d.ts +17 -0
  34. package/dist/eval/stats.js +50 -0
  35. package/dist/eval/suite.d.ts +20 -0
  36. package/dist/eval/suite.js +165 -0
  37. package/dist/eval/variants.d.ts +35 -0
  38. package/dist/eval/variants.js +137 -0
  39. package/dist/eval/verifiers.d.ts +49 -0
  40. package/dist/eval/verifiers.js +264 -0
  41. package/dist/forge/javascript/hosts/worker-host.d.ts +9 -0
  42. package/dist/forge/javascript/hosts/worker-host.js +16 -1
  43. package/dist/forge/kernel.js +3 -2
  44. package/dist/goal/command.d.ts +7 -2
  45. package/dist/goal/command.js +10 -2
  46. package/dist/goal/format.js +2 -0
  47. package/dist/goal/machine.d.ts +50 -3
  48. package/dist/goal/machine.js +80 -2
  49. package/dist/goal/service.d.ts +22 -2
  50. package/dist/goal/service.js +94 -7
  51. package/dist/goal/store.js +22 -3
  52. package/dist/index.d.ts +18 -2
  53. package/dist/index.js +18 -2
  54. package/dist/plan/criteria.d.ts +30 -0
  55. package/dist/plan/criteria.js +256 -0
  56. package/dist/plan/exit-plan.d.ts +8 -0
  57. package/dist/plan/exit-plan.js +91 -9
  58. package/dist/plan/manifest.d.ts +4 -2
  59. package/dist/plan/manifest.js +6 -1
  60. package/dist/plan/state.d.ts +13 -2
  61. package/dist/plan/state.js +7 -1
  62. package/dist/plugins/host.d.ts +7 -0
  63. package/dist/plugins/host.js +15 -0
  64. package/dist/proof/command.d.ts +24 -0
  65. package/dist/proof/command.js +109 -0
  66. package/dist/proof/coverage.d.ts +40 -0
  67. package/dist/proof/coverage.js +102 -0
  68. package/dist/proof/divergence.d.ts +24 -0
  69. package/dist/proof/divergence.js +120 -0
  70. package/dist/proof/export.d.ts +65 -0
  71. package/dist/proof/export.js +144 -0
  72. package/dist/proof/index.d.ts +12 -0
  73. package/dist/proof/index.js +12 -0
  74. package/dist/proof/ledger.d.ts +38 -0
  75. package/dist/proof/ledger.js +57 -0
  76. package/dist/proof/plugin-verifiers.d.ts +23 -0
  77. package/dist/proof/plugin-verifiers.js +39 -0
  78. package/dist/proof/recorder.d.ts +69 -0
  79. package/dist/proof/recorder.js +347 -0
  80. package/dist/proof/scope.d.ts +12 -0
  81. package/dist/proof/scope.js +121 -0
  82. package/dist/proof/store.d.ts +52 -0
  83. package/dist/proof/store.js +192 -0
  84. package/dist/proof/tree.d.ts +6 -0
  85. package/dist/proof/tree.js +78 -0
  86. package/dist/proof/types.d.ts +21 -0
  87. package/dist/proof/types.js +1 -0
  88. package/dist/proof/verifiers.d.ts +46 -0
  89. package/dist/proof/verifiers.js +176 -0
  90. package/dist/proof/verify.d.ts +36 -0
  91. package/dist/proof/verify.js +171 -0
  92. package/dist/runtime/ids.d.ts +21 -0
  93. package/dist/runtime/ids.js +41 -1
  94. package/dist/runtime/store.js +63 -2
  95. package/dist/tools/goal.d.ts +5 -1
  96. package/dist/tools/goal.js +35 -4
  97. package/package.json +2 -2
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Running a verifier on demand (specs/011 3.1, 3.8). This is a USER-side entry point: no tool
3
+ * exposes it to the model, so the model can neither start a verification nor choose its verdict.
4
+ *
5
+ * - `file`, `data-check` and (opt-in) `coverage` run their canonical core tool call (`verifierCall`) as a real run
6
+ * under the normal gate; the recorder captures the evidence from the raw result like it does
7
+ * for a call the model made (D3, D6).
8
+ * - `plugin` runs the registered verifier. Its calls go through `callTool` (the same gate), the
9
+ * result must carry referenceable evidence, and anything else degrades the criterion to
10
+ * `manual_review` (an evidence of kind `manual`). A verifier that throws or hangs proves
11
+ * nothing: the criterion stays as it was and the outcome says why.
12
+ */
13
+ import { createHash, randomUUID } from "node:crypto";
14
+ import { redactOutput } from "../eval/secrets.js";
15
+ import { excerptOf } from "./recorder.js";
16
+ import { treeFingerprint } from "./tree.js";
17
+ import { verifierCall } from "./verifiers.js";
18
+ const PLUGIN_TIMEOUT_MS = 60_000;
19
+ const HEX_SHA256 = /^[0-9a-f]{64}$/i;
20
+ const DETAIL_MAX = 200;
21
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
22
+ function describeError(error) {
23
+ const text = error instanceof Error ? error.message : String(error);
24
+ return redactOutput(text).slice(0, DETAIL_MAX);
25
+ }
26
+ export async function verifyCriterion(deps, request) {
27
+ const { criterionId, sessionId } = request;
28
+ const statusOf = () => {
29
+ const ledger = deps.store.ledger(sessionId, (deps.now?.() ?? new Date()).toISOString());
30
+ const criterion = ledger.criteria.find((c) => c.criterionId === criterionId);
31
+ return { status: criterion?.status ?? "not_checked" };
32
+ };
33
+ const outcome = (reason, detail, evidenceId) => ({
34
+ criterionId,
35
+ ...statusOf(),
36
+ ...(evidenceId ? { evidenceId } : {}),
37
+ ...(reason ? { reason } : {}),
38
+ ...(detail ? { detail } : {}),
39
+ });
40
+ const record = deps.store.criterion(criterionId);
41
+ if (!record || record.sessionId !== sessionId)
42
+ return outcome("unknown_criterion");
43
+ if (record.notApplicableReason)
44
+ return outcome("not_applicable");
45
+ const verifier = record.verifier;
46
+ if (!verifier)
47
+ return outcome("no_verifier");
48
+ const signal = request.signal ?? new AbortController().signal;
49
+ if (verifier.kind === "coverage" && deps.coverage?.() !== true)
50
+ return outcome("coverage_disabled", "proof.coverage.enabled is false");
51
+ if (verifier.kind === "file" || verifier.kind === "data-check" || verifier.kind === "coverage") {
52
+ const call = verifierCall(verifier);
53
+ if (!call)
54
+ return outcome("unverifiable_spec");
55
+ let done;
56
+ try {
57
+ done = await deps.callTool(sessionId, call.name, call.input, signal);
58
+ }
59
+ catch (error) {
60
+ return outcome("verifier_failed", describeError(error));
61
+ }
62
+ const evidence = deps.store
63
+ .ledger(sessionId)
64
+ .evidence.find((e) => e.criterionId === criterionId && e.callId === done.callId);
65
+ return evidence ? outcome(undefined, undefined, evidence.evidenceId) : outcome("no_evidence");
66
+ }
67
+ if (verifier.kind === "plugin") {
68
+ const entry = deps.registry.get(verifier.verifierId);
69
+ if (!entry)
70
+ return outcome("verifier_unavailable");
71
+ const started = Date.now();
72
+ const timeout = AbortSignal.timeout(deps.timeoutMs ?? PLUGIN_TIMEOUT_MS);
73
+ const combined = AbortSignal.any([signal, timeout]);
74
+ const context = {
75
+ sessionId,
76
+ criterionId,
77
+ workspace: request.workspace,
78
+ signal: combined,
79
+ callTool: async (name, input) => {
80
+ const { callId, result } = await deps.callTool(sessionId, name, input, combined);
81
+ return { callId, result };
82
+ },
83
+ };
84
+ let result;
85
+ try {
86
+ result = await raceAbort(entry.verifier.run(verifier.input, context), combined);
87
+ }
88
+ catch (error) {
89
+ return outcome("verifier_failed", describeError(error));
90
+ }
91
+ const durationMs = Date.now() - started;
92
+ const checked = referenceable(result, deps.db, sessionId);
93
+ const filesTouched = deps.store.edits(sessionId).map((e) => e.path);
94
+ const base = {
95
+ criterionId,
96
+ durationMs,
97
+ treeFingerprint: treeFingerprint(request.workspace, filesTouched),
98
+ filesTouched,
99
+ capturedAt: (deps.now?.() ?? new Date()).toISOString(),
100
+ };
101
+ if (checked) {
102
+ const excerpt = typeof checked.excerpt === "string" ? excerptOf(checked.excerpt) : undefined;
103
+ const evidence = deps.store.appendEvidence({
104
+ ...base,
105
+ verifierKind: "plugin",
106
+ runId: checked.runId,
107
+ callId: checked.callId,
108
+ ok: checked.ok,
109
+ outputHash: checked.outputHash,
110
+ ...(excerpt ? { excerpt } : {}),
111
+ });
112
+ return outcome(undefined, undefined, evidence.evidenceId);
113
+ }
114
+ const why = `verifier ${verifier.verifierId} returned no traceable evidence`;
115
+ const evidence = deps.store.appendEvidence({
116
+ ...base,
117
+ verifierKind: "manual",
118
+ runId: `verify_${randomUUID()}`,
119
+ callId: `verify_${randomUUID()}`,
120
+ ok: false,
121
+ outputHash: sha256(why),
122
+ excerpt: why,
123
+ });
124
+ return outcome("no_referenceable_evidence", undefined, evidence.evidenceId);
125
+ }
126
+ return outcome("unsupported_verifier");
127
+ }
128
+ /** Rejects as soon as `signal` aborts, even when the verifier ignores it. */
129
+ function raceAbort(work, signal) {
130
+ if (signal.aborted)
131
+ return Promise.reject(signal.reason ?? new Error("aborted"));
132
+ return new Promise((resolve, reject) => {
133
+ const onAbort = () => reject(signal.reason ?? new Error("aborted"));
134
+ signal.addEventListener("abort", onAbort, { once: true });
135
+ work.then((value) => {
136
+ signal.removeEventListener("abort", onAbort);
137
+ resolve(value);
138
+ }, (error) => {
139
+ signal.removeEventListener("abort", onAbort);
140
+ reject(error);
141
+ });
142
+ });
143
+ }
144
+ /**
145
+ * A plugin result is evidence only with a boolean verdict and a sha256 `outputHash`; a `callId`
146
+ * must name a call this session journaled (its run id is then the real one). Without a callId the
147
+ * host mints a verification id: the evidence is then the plugin's word plus the hash.
148
+ */
149
+ function referenceable(result, db, sessionId) {
150
+ if (typeof result !== "object" || result === null)
151
+ return undefined;
152
+ const r = result;
153
+ if (typeof r.ok !== "boolean")
154
+ return undefined;
155
+ if (typeof r.outputHash !== "string" || !HEX_SHA256.test(r.outputHash))
156
+ return undefined;
157
+ const base = {
158
+ ok: r.ok,
159
+ outputHash: r.outputHash.toLowerCase(),
160
+ ...(typeof r.excerpt === "string" ? { excerpt: r.excerpt } : {}),
161
+ };
162
+ if (r.callId === undefined)
163
+ return { ...base, runId: `verify_${randomUUID()}`, callId: `verify_${randomUUID()}` };
164
+ if (typeof r.callId !== "string" || !r.callId)
165
+ return undefined;
166
+ const row = db
167
+ .prepare("SELECT run_id FROM tool_calls WHERE session=? AND call_id=?")
168
+ .get(sessionId, r.callId);
169
+ const runId = row ? String(row.run_id ?? "") : "";
170
+ return runId ? { ...base, callId: r.callId, runId } : undefined;
171
+ }
@@ -6,3 +6,24 @@
6
6
  export declare function ulid(now?: number): string;
7
7
  /** A prefixed id such as `art_01J…` or `exec_01J…`. */
8
8
  export declare const newId: (prefix: string, now?: number) => string;
9
+ /**
10
+ * A replacement for the random identifiers the runtime generates (session ids, plan ids, forge
11
+ * execution ids, `ulid`-based ids). The reliability lab installs a seeded one while it records or
12
+ * replays a cassette so the requests a run sends do not depend on fresh randomness. Nothing
13
+ * installs one in normal use, and then every function here behaves exactly as before.
14
+ */
15
+ export interface IdSource {
16
+ /** A UUID (the shape of `crypto.randomUUID()`). */
17
+ uuid(): string;
18
+ /** The 26-character body of a `ulid` (the argument is the caller's clock, which a source may ignore). */
19
+ ulid(now?: number): string;
20
+ }
21
+ /** Installs (or, with `undefined`, removes) the process-wide id source. */
22
+ export declare function setIdSource(source?: IdSource): void;
23
+ /** A random v4 UUID, or the next one of the installed id source. */
24
+ export declare const newUuid: () => string;
25
+ /**
26
+ * A deterministic source: the n-th UUID (and the n-th ULID) depends only on `seed`. UUIDs are
27
+ * v4-shaped; ULIDs carry a counter in their time part, so they stay sortable by creation order.
28
+ */
29
+ export declare function seededIdSource(seed: string): IdSource;
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  /** Crockford base32 (no I, L, O, U), the ULID alphabet. */
3
3
  const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4
4
  /**
@@ -7,6 +7,8 @@ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
7
7
  * millisecond sort arbitrarily, which is fine for row ids and folder names.
8
8
  */
9
9
  export function ulid(now = Date.now()) {
10
+ if (installed)
11
+ return installed.ulid(now);
10
12
  let time = "";
11
13
  let t = Math.max(0, Math.floor(now));
12
14
  for (let i = 0; i < 10; i++) {
@@ -30,3 +32,41 @@ export function ulid(now = Date.now()) {
30
32
  }
31
33
  /** A prefixed id such as `art_01J…` or `exec_01J…`. */
32
34
  export const newId = (prefix, now) => `${prefix}_${ulid(now)}`;
35
+ let installed;
36
+ /** Installs (or, with `undefined`, removes) the process-wide id source. */
37
+ export function setIdSource(source) {
38
+ installed = source;
39
+ }
40
+ /** A random v4 UUID, or the next one of the installed id source. */
41
+ export const newUuid = () => (installed ? installed.uuid() : randomUUID());
42
+ /**
43
+ * A deterministic source: the n-th UUID (and the n-th ULID) depends only on `seed`. UUIDs are
44
+ * v4-shaped; ULIDs carry a counter in their time part, so they stay sortable by creation order.
45
+ */
46
+ export function seededIdSource(seed) {
47
+ let uuids = 0;
48
+ let ulids = 0;
49
+ const digest = (kind, n) => createHash("sha256").update(`${seed}\0${kind}\0${n}`).digest();
50
+ return {
51
+ uuid() {
52
+ const bytes = digest("uuid", uuids++).subarray(0, 16);
53
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
54
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
55
+ const hex = bytes.toString("hex");
56
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
57
+ },
58
+ ulid() {
59
+ let time = "";
60
+ let t = ulids;
61
+ for (let i = 0; i < 10; i++) {
62
+ time = (ALPHABET[t % 32] ?? "0") + time;
63
+ t = Math.floor(t / 32);
64
+ }
65
+ const bytes = digest("ulid", ulids++);
66
+ let random = "";
67
+ for (let i = 0; i < 16; i++)
68
+ random += ALPHABET[(bytes[i] ?? 0) & 31];
69
+ return time + random;
70
+ },
71
+ };
72
+ }
@@ -1,4 +1,5 @@
1
1
  import { textResult, } from "@alisio/sdk";
2
+ import { newUuid } from "./ids.js";
2
3
  import { isProcessAlive } from "./process.js";
3
4
  import { openDatabase } from "./sqlite.js";
4
5
  const alive = isProcessAlive;
@@ -311,6 +312,66 @@ export class SQLiteStore {
311
312
  this.db.exec("PRAGMA foreign_keys=ON");
312
313
  }
313
314
  }
315
+ if (!has(11))
316
+ this.db.transaction(() => {
317
+ // v11: the Proof ledger (specs/011). Criteria and evidence die with their session;
318
+ // evidence is append-only (the trigger rejects UPDATE). The status is never stored.
319
+ this.db.exec(`
320
+ CREATE TABLE IF NOT EXISTS proof_criteria(
321
+ criterion_id TEXT PRIMARY KEY,
322
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
323
+ goal_id TEXT,
324
+ text TEXT NOT NULL,
325
+ required INTEGER NOT NULL DEFAULT 0,
326
+ origin TEXT NOT NULL,
327
+ verifier_json TEXT,
328
+ scope_json TEXT,
329
+ na_reason TEXT,
330
+ created_at INTEGER NOT NULL);
331
+ CREATE INDEX IF NOT EXISTS proof_criteria_session ON proof_criteria(session_id, created_at);
332
+ CREATE TABLE IF NOT EXISTS proof_evidence(
333
+ evidence_id TEXT PRIMARY KEY,
334
+ criterion_id TEXT NOT NULL REFERENCES proof_criteria(criterion_id) ON DELETE CASCADE,
335
+ run_id TEXT NOT NULL,
336
+ call_id TEXT NOT NULL,
337
+ ok INTEGER NOT NULL,
338
+ verifier_kind TEXT NOT NULL,
339
+ fingerprint TEXT,
340
+ exit_code INTEGER,
341
+ duration_ms INTEGER NOT NULL,
342
+ output_hash TEXT NOT NULL,
343
+ excerpt TEXT,
344
+ tree_fingerprint TEXT NOT NULL,
345
+ files_json TEXT NOT NULL,
346
+ captured_at TEXT NOT NULL);
347
+ CREATE INDEX IF NOT EXISTS proof_evidence_criterion ON proof_evidence(criterion_id);
348
+ CREATE TRIGGER IF NOT EXISTS proof_evidence_append_only
349
+ BEFORE UPDATE ON proof_evidence
350
+ BEGIN SELECT RAISE(ABORT, 'proof_evidence is append-only'); END;
351
+ INSERT OR IGNORE INTO schema_migrations VALUES(11);`);
352
+ });
353
+ if (!has(12))
354
+ this.db.transaction(() => {
355
+ // v12: the newest successful write_file/edit_file per path for sessions with Proof
356
+ // criteria (specs/011 3.4, D5). One row per path keeps it bounded; it dies with the session.
357
+ this.db.exec(`
358
+ CREATE TABLE IF NOT EXISTS proof_edits(
359
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
360
+ path TEXT NOT NULL,
361
+ edited_at TEXT NOT NULL,
362
+ PRIMARY KEY(session_id, path));
363
+ INSERT OR IGNORE INTO schema_migrations VALUES(12);`);
364
+ });
365
+ if (!has(13))
366
+ this.db.transaction(() => {
367
+ // v13 (additive): the Proof snapshot a completed goal keeps (specs/011 3.6, O6): which
368
+ // required criteria were unmet and the user's override reason, as JSON. Old binaries
369
+ // ignore the column; goals without criteria keep it NULL.
370
+ const columns = new Set(this.db.prepare("PRAGMA table_info(session_goals)").all().map((c) => c.name));
371
+ if (!columns.has("proof_json"))
372
+ this.db.exec("ALTER TABLE session_goals ADD COLUMN proof_json TEXT");
373
+ this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(13);");
374
+ });
314
375
  }
315
376
  /**
316
377
  * The root of a session (itself for roots), following `parent_id`. Unknown ids are their own
@@ -327,7 +388,7 @@ export class SQLiteStore {
327
388
  return current;
328
389
  }
329
390
  create(workspace, provider, model) {
330
- const id = crypto.randomUUID();
391
+ const id = newUuid();
331
392
  const now = Date.now();
332
393
  this.db
333
394
  .prepare("INSERT INTO sessions(id,workspace,provider,model,created_at,updated_at) VALUES(?,?,?,?,?,?)")
@@ -367,7 +428,7 @@ export class SQLiteStore {
367
428
  .map((r) => this.row(r));
368
429
  }
369
430
  createChild(record) {
370
- const id = record.id ?? crypto.randomUUID();
431
+ const id = record.id ?? newUuid();
371
432
  const now = Date.now();
372
433
  this.db
373
434
  .prepare(`INSERT INTO sessions(id,workspace,provider,model,parent_id,depth,agent,status,title,usage,options,created_at,updated_at)
@@ -12,7 +12,11 @@ import type { ToolRegistry } from "../core/registry.ts";
12
12
  import type { GoalService } from "../goal/service.ts";
13
13
  export declare const GET_GOAL_TOOL = "get_goal";
14
14
  export declare const UPDATE_GOAL_TOOL = "update_goal";
15
- /** Parses the evidence list defensively (the schema is advisory for some providers). */
15
+ /**
16
+ * Parses the evidence list defensively (the schema is advisory for some providers). An optional
17
+ * `evidenceId` is kept as declared here; the goal service later drops every id that does not
18
+ * resolve to captured evidence of the session (the declaration is then plain text).
19
+ */
16
20
  export declare function parseEvidence(raw: unknown): GoalEvidence[] | string;
17
21
  export declare function registerGoalTools(registry: ToolRegistry, deps: {
18
22
  goals: GoalService;
@@ -35,7 +35,13 @@ function brief(goal) {
35
35
  max_minutes: Math.round(goal.maxWallMs / 60_000),
36
36
  };
37
37
  }
38
- /** Parses the evidence list defensively (the schema is advisory for some providers). */
38
+ const MAX_EVIDENCE_ID = 100;
39
+ const MAX_UNMET_LISTED = 20;
40
+ /**
41
+ * Parses the evidence list defensively (the schema is advisory for some providers). An optional
42
+ * `evidenceId` is kept as declared here; the goal service later drops every id that does not
43
+ * resolve to captured evidence of the session (the declaration is then plain text).
44
+ */
39
45
  export function parseEvidence(raw) {
40
46
  if (!Array.isArray(raw) || !raw.length)
41
47
  return "evidence must list at least one item.";
@@ -48,7 +54,12 @@ export function parseEvidence(raw) {
48
54
  if (!detail)
49
55
  return "every evidence item needs a detail (a path, a test name, a log line).";
50
56
  const kind = KINDS.find((candidate) => candidate === item.kind) ?? "other";
51
- items.push({ kind, detail: detail.slice(0, MAX_DETAIL) });
57
+ const evidenceId = typeof item?.evidenceId === "string" ? item.evidenceId.trim() : "";
58
+ items.push({
59
+ kind,
60
+ detail: detail.slice(0, MAX_DETAIL),
61
+ ...(evidenceId && evidenceId.length <= MAX_EVIDENCE_ID ? { evidenceId } : {}),
62
+ });
52
63
  }
53
64
  return items;
54
65
  }
@@ -85,7 +96,15 @@ export function registerGoalTools(registry, deps) {
85
96
  type: "array",
86
97
  minItems: 1,
87
98
  maxItems: MAX_EVIDENCE,
88
- items: objectSchema({ kind: { enum: [...KINDS] }, detail: { type: "string", maxLength: MAX_DETAIL } }, ["kind", "detail"]),
99
+ items: objectSchema({
100
+ kind: { enum: [...KINDS] },
101
+ detail: { type: "string", maxLength: MAX_DETAIL },
102
+ evidenceId: {
103
+ type: "string",
104
+ maxLength: MAX_EVIDENCE_ID,
105
+ description: "Optional: the id of captured Proof evidence this item refers to. Without a valid id the item is plain text and verifies nothing.",
106
+ },
107
+ }, ["kind", "detail"]),
89
108
  },
90
109
  }, ["status", "summary", "evidence"]),
91
110
  async execute(input, context) {
@@ -101,7 +120,19 @@ export function registerGoalTools(registry, deps) {
101
120
  return textResult(JSON.stringify({ ok: false, error: evidence }));
102
121
  const result = goals.report(sessionId, context.runId, { status, summary, evidence });
103
122
  if (!result.ok)
104
- return textResult(JSON.stringify({ ok: false, error: result.message }));
123
+ return textResult(JSON.stringify({
124
+ ok: false,
125
+ error: result.message,
126
+ ...(result.unmet
127
+ ? {
128
+ unmet: result.unmet.slice(0, MAX_UNMET_LISTED).map((u) => ({
129
+ criterionId: u.criterionId,
130
+ text: u.text,
131
+ status: u.status,
132
+ })),
133
+ }
134
+ : {}),
135
+ }));
105
136
  if (status === "complete")
106
137
  return textResult(JSON.stringify({
107
138
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/core",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
5
5
  "author": "Gustavo Gutiérrez",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "node": ">=22.16"
46
46
  },
47
47
  "dependencies": {
48
- "@alisio/sdk": "0.6.0",
48
+ "@alisio/sdk": "0.8.0",
49
49
  "@mixmark-io/domino": "2.2.0",
50
50
  "@modelcontextprotocol/client": "2.1.0",
51
51
  "ajv": "8.20.0",