@evo-dev/core 0.0.1-alpha.3 → 0.0.1-alpha.4
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/config/index.js +164 -125
- package/dist/index.js +8999 -7914
- package/package.json +1 -1
- package/src/agents/index.ts +1 -1
- package/src/code-agent-traces/index.ts +3 -10
- package/src/config/settings.ts +14 -0
- package/src/config/store.ts +1 -1
- package/src/daemon/index.ts +1 -1
- package/src/evolution/candidates/index.ts +235 -0
- package/src/evolution/control/index.ts +19 -0
- package/src/evolution/evidence/analysis.ts +529 -0
- package/src/evolution/evidence/index.ts +3 -0
- package/src/evolution/evidence/session-memory/constants.ts +12 -0
- package/src/evolution/evidence/session-memory/index.ts +4 -0
- package/src/evolution/evidence/session-memory/paths.ts +29 -0
- package/src/evolution/evidence/session-memory/policy.ts +39 -0
- package/src/evolution/evidence/session-memory/segment.ts +192 -0
- package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
- package/src/evolution/evidence/session-memory/storage.ts +145 -0
- package/src/evolution/evidence/session-memory/types.ts +207 -0
- package/src/evolution/evidence/session-memory/updater.ts +184 -0
- package/src/evolution/formatters.ts +169 -0
- package/src/evolution/index.ts +12 -2827
- package/src/{knowledge → evolution/knowledge}/index.ts +7 -7
- package/src/evolution/paths.ts +41 -0
- package/src/evolution/processor/distillation.ts +436 -0
- package/src/evolution/processor/index.ts +3 -0
- package/src/evolution/processor/process.ts +306 -0
- package/src/evolution/schema.ts +522 -0
- package/src/evolution/shared.ts +677 -0
- package/src/evolution/triggers/classification.ts +102 -0
- package/src/evolution/triggers/index.ts +295 -0
- package/src/hooks/index.ts +54 -7
- package/src/index.ts +10 -2
- package/src/runtime-logs/index.ts +4 -12
- package/src/team/index.ts +1 -1
- package/src/utils/errors.ts +13 -0
- package/src/utils/fs.ts +40 -0
- package/src/utils/hash.ts +9 -0
- package/src/utils/ids.ts +12 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/parsing.ts +11 -0
- package/src/utils/text.ts +18 -0
- package/src/utils/time.ts +5 -0
- /package/src/{learning → evolution/review}/index.ts +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EvolutionEvidenceEvent,
|
|
3
|
+
EvolutionEvidenceEventKind,
|
|
4
|
+
EvolutionEvidenceWindow,
|
|
5
|
+
EvolutionEvosCase,
|
|
6
|
+
EvolutionNormalizedEventType,
|
|
7
|
+
EvolutionTriggerPolicy,
|
|
8
|
+
EvolutionTriggerReason,
|
|
9
|
+
EvolutionTriggerStrength,
|
|
10
|
+
} from "../schema.ts";
|
|
11
|
+
import { NORMALIZED_EVENT_TYPES, sanitizeText } from "../shared.ts";
|
|
12
|
+
|
|
13
|
+
export function normalizeEvolutionEventType(
|
|
14
|
+
value: string | null | undefined,
|
|
15
|
+
): EvolutionNormalizedEventType {
|
|
16
|
+
if (value === "AgentStop") return "SubagentStop";
|
|
17
|
+
if (typeof value === "string" && (NORMALIZED_EVENT_TYPES as readonly string[]).includes(value)) {
|
|
18
|
+
return value as EvolutionNormalizedEventType;
|
|
19
|
+
}
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function decideEvolutionTrigger(input: {
|
|
24
|
+
eventType: EvolutionNormalizedEventType;
|
|
25
|
+
summary: string;
|
|
26
|
+
kind: EvolutionEvidenceEventKind;
|
|
27
|
+
}): { strength: EvolutionTriggerStrength; reason: EvolutionTriggerReason } {
|
|
28
|
+
if (input.eventType === "TaskCompleted") return { strength: "strong", reason: "task-completed" };
|
|
29
|
+
if (input.eventType === "StopFailure") return { strength: "strong", reason: "stop-failure" };
|
|
30
|
+
if (input.eventType === "PostToolUseFailure")
|
|
31
|
+
return { strength: "strong", reason: "tool-failure" };
|
|
32
|
+
if (input.eventType === "PermissionDenied")
|
|
33
|
+
return { strength: "strong", reason: "permission-denied" };
|
|
34
|
+
if (input.eventType === "Stop") return { strength: "conditional", reason: "turn-completed" };
|
|
35
|
+
if (input.eventType === "SessionEnd") return { strength: "conditional", reason: "session-ended" };
|
|
36
|
+
if (input.eventType === "SubagentStop")
|
|
37
|
+
return { strength: "conditional", reason: "role-completed" };
|
|
38
|
+
if (input.kind === "error" || /fail|failed|failure|error|issue/i.test(input.summary)) {
|
|
39
|
+
return { strength: "strong", reason: "failure-signal" };
|
|
40
|
+
}
|
|
41
|
+
return { strength: "none", reason: "none" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createTriggerPolicy(events: EvolutionEvidenceEvent[]): EvolutionTriggerPolicy {
|
|
45
|
+
const failures = events.filter((event) => event.kind === "error").length;
|
|
46
|
+
const verifications = events.filter((event) => event.kind === "verification").length;
|
|
47
|
+
const roleLifecycle = events.filter((event) => event.kind === "subagent").length;
|
|
48
|
+
const skillCalls = events.filter((event) => event.kind === "skill-call").length;
|
|
49
|
+
const strongest = strongestTrigger(events);
|
|
50
|
+
const reasons = uniqueTriggerReasons(
|
|
51
|
+
events.flatMap((event) => (event.triggerReason === "none" ? [] : [event.triggerReason])),
|
|
52
|
+
);
|
|
53
|
+
const hasSignal = failures > 0 || verifications > 0 || roleLifecycle > 0 || skillCalls > 0;
|
|
54
|
+
return {
|
|
55
|
+
strongest: strongest.strength,
|
|
56
|
+
reasons,
|
|
57
|
+
distillRecommended:
|
|
58
|
+
strongest.strength === "strong" || (strongest.strength === "conditional" && hasSignal),
|
|
59
|
+
evidenceSignals: { failures, verifications, roleLifecycle, skillCalls },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function strongestTrigger(events: EvolutionEvidenceEvent[]): {
|
|
64
|
+
strength: EvolutionTriggerStrength;
|
|
65
|
+
reason: EvolutionTriggerReason;
|
|
66
|
+
} {
|
|
67
|
+
const strong = events.find((event) => event.triggerStrength === "strong");
|
|
68
|
+
if (strong !== undefined) return { strength: "strong", reason: strong.triggerReason };
|
|
69
|
+
const conditional = events.find((event) => event.triggerStrength === "conditional");
|
|
70
|
+
if (conditional !== undefined) {
|
|
71
|
+
return { strength: "conditional", reason: conditional.triggerReason };
|
|
72
|
+
}
|
|
73
|
+
return { strength: "none", reason: "none" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function uniqueTriggerReasons(values: EvolutionTriggerReason[]): EvolutionTriggerReason[] {
|
|
77
|
+
return [...new Set(values)].filter((reason) => reason !== "none");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function chooseEvosTriggerKind(
|
|
81
|
+
evidenceWindow: EvolutionEvidenceWindow,
|
|
82
|
+
): EvolutionEvosCase["trigger"]["kind"] {
|
|
83
|
+
if (evidenceWindow.triggerPolicy.reasons.includes("task-completed")) return "task-completion";
|
|
84
|
+
if (evidenceWindow.triggerPolicy.reasons.includes("turn-completed")) return "session-completion";
|
|
85
|
+
if (evidenceWindow.triggerPolicy.reasons.includes("session-ended")) return "session-completion";
|
|
86
|
+
return "manual";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createTriggerSummary(evidenceWindow: EvolutionEvidenceWindow): string {
|
|
90
|
+
const reasons = evidenceWindow.triggerPolicy.reasons.join(", ") || "manual";
|
|
91
|
+
return sanitizeText(
|
|
92
|
+
`Evolution distillation used ${evidenceWindow.triggerPolicy.strongest} trigger evidence: ${reasons}.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createTriggerDecisionSummary(
|
|
97
|
+
eventType: EvolutionNormalizedEventType,
|
|
98
|
+
strength: EvolutionTriggerStrength,
|
|
99
|
+
reason: EvolutionTriggerReason,
|
|
100
|
+
): string {
|
|
101
|
+
return `Hook event ${eventType} classified as ${strength} evolution trigger (${reason}).`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveEvoDevPaths } from "../../config/paths.ts";
|
|
4
|
+
import {
|
|
5
|
+
createStableId,
|
|
6
|
+
isFileExistsError,
|
|
7
|
+
normalizeTimestamp,
|
|
8
|
+
readJsonFiles,
|
|
9
|
+
writeJsonFile as writeJson,
|
|
10
|
+
} from "../../utils/index.ts";
|
|
11
|
+
import { resolveEvolutionPaths } from "../paths.ts";
|
|
12
|
+
import type {
|
|
13
|
+
EvolutionTriggerDecision,
|
|
14
|
+
EvolutionTriggerInput,
|
|
15
|
+
EvolutionTriggerListInput,
|
|
16
|
+
EvolutionTriggerRecord,
|
|
17
|
+
EvolutionTriggerStatus,
|
|
18
|
+
SegmentEvolutionTriggerInput,
|
|
19
|
+
SegmentEvolutionTriggerListInput,
|
|
20
|
+
SegmentEvolutionTriggerRecord,
|
|
21
|
+
} from "../schema.ts";
|
|
22
|
+
import {
|
|
23
|
+
createPrivacyFields,
|
|
24
|
+
listDirectoryNames,
|
|
25
|
+
parseSegmentTrigger,
|
|
26
|
+
parseTrigger,
|
|
27
|
+
sanitizeId,
|
|
28
|
+
sanitizeStorageId,
|
|
29
|
+
sanitizeText,
|
|
30
|
+
validateEvolutionTriggerRecord,
|
|
31
|
+
validateSegmentEvolutionTriggerRecord,
|
|
32
|
+
} from "../shared.ts";
|
|
33
|
+
import {
|
|
34
|
+
createTriggerDecisionSummary,
|
|
35
|
+
decideEvolutionTrigger,
|
|
36
|
+
normalizeEvolutionEventType,
|
|
37
|
+
} from "./classification.ts";
|
|
38
|
+
|
|
39
|
+
export function resolveEvolutionTriggerDecision(input: {
|
|
40
|
+
eventType: string;
|
|
41
|
+
summary?: string | null;
|
|
42
|
+
}): EvolutionTriggerDecision {
|
|
43
|
+
const eventType = normalizeEvolutionEventType(input.eventType);
|
|
44
|
+
const trigger = decideEvolutionTrigger({
|
|
45
|
+
eventType,
|
|
46
|
+
summary: input.summary ?? "",
|
|
47
|
+
kind: "trace",
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
eventType,
|
|
51
|
+
strength: trigger.strength,
|
|
52
|
+
reason: trigger.reason,
|
|
53
|
+
shouldQueue: trigger.strength !== "none",
|
|
54
|
+
summary: createTriggerDecisionSummary(eventType, trigger.strength, trigger.reason),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function enqueueEvolutionTrigger(
|
|
59
|
+
input: EvolutionTriggerInput,
|
|
60
|
+
): Promise<EvolutionTriggerRecord | null> {
|
|
61
|
+
const decision = resolveEvolutionTriggerDecision({
|
|
62
|
+
eventType: input.eventType,
|
|
63
|
+
summary: input.summary,
|
|
64
|
+
});
|
|
65
|
+
if (!decision.shouldQueue) return null;
|
|
66
|
+
|
|
67
|
+
const now = normalizeTimestamp(input.now);
|
|
68
|
+
const projectKey = sanitizeId(input.projectKey);
|
|
69
|
+
const runId = sanitizeId(input.runId);
|
|
70
|
+
const id = createStableId("trigger", [
|
|
71
|
+
projectKey,
|
|
72
|
+
runId,
|
|
73
|
+
input.roleId ?? "",
|
|
74
|
+
input.eventId ?? "",
|
|
75
|
+
decision.eventType,
|
|
76
|
+
now,
|
|
77
|
+
]);
|
|
78
|
+
const trigger: EvolutionTriggerRecord = {
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
id,
|
|
81
|
+
kind: "evolution-trigger",
|
|
82
|
+
projectKey,
|
|
83
|
+
runId,
|
|
84
|
+
roleId: input.roleId === undefined || input.roleId === null ? null : sanitizeId(input.roleId),
|
|
85
|
+
taskId: input.taskId === undefined || input.taskId === null ? null : sanitizeId(input.taskId),
|
|
86
|
+
eventType: decision.eventType,
|
|
87
|
+
eventId:
|
|
88
|
+
input.eventId === undefined || input.eventId === null ? null : sanitizeId(input.eventId),
|
|
89
|
+
evidenceRef:
|
|
90
|
+
input.evidenceRef === undefined || input.evidenceRef === null
|
|
91
|
+
? null
|
|
92
|
+
: sanitizeText(input.evidenceRef),
|
|
93
|
+
summary: sanitizeText(input.summary ?? decision.summary),
|
|
94
|
+
triggerStrength: decision.strength,
|
|
95
|
+
triggerReason: decision.reason,
|
|
96
|
+
status: "pending",
|
|
97
|
+
attempts: 0,
|
|
98
|
+
createdAt: now,
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
processedBatchId: null,
|
|
101
|
+
lastError: null,
|
|
102
|
+
rawContentStored: false,
|
|
103
|
+
privacy: createPrivacyFields(),
|
|
104
|
+
};
|
|
105
|
+
validateEvolutionTriggerRecord(trigger);
|
|
106
|
+
const paths = resolveEvolutionPaths({ homeDir: input.homeDir, projectKey, runId });
|
|
107
|
+
await writeJson(join(paths.triggersDir, `${trigger.id}.json`), trigger, { overwrite: false });
|
|
108
|
+
return trigger;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function listEvolutionTriggers(
|
|
112
|
+
input: EvolutionTriggerListInput,
|
|
113
|
+
): Promise<EvolutionTriggerRecord[]> {
|
|
114
|
+
const paths = resolveEvoDevPaths(input.homeDir);
|
|
115
|
+
const evolutionStateDir = join(paths.stateDir, "evolution");
|
|
116
|
+
const projectKeys =
|
|
117
|
+
input.projectKey === undefined
|
|
118
|
+
? await listDirectoryNames(evolutionStateDir)
|
|
119
|
+
: [sanitizeStorageId("projectKey", input.projectKey)];
|
|
120
|
+
const triggers: EvolutionTriggerRecord[] = [];
|
|
121
|
+
for (const projectKey of projectKeys) {
|
|
122
|
+
const projectStateDir = join(evolutionStateDir, projectKey);
|
|
123
|
+
const runIds =
|
|
124
|
+
input.runId === undefined
|
|
125
|
+
? await listDirectoryNames(projectStateDir)
|
|
126
|
+
: [sanitizeStorageId("runId", input.runId)];
|
|
127
|
+
for (const runId of runIds) {
|
|
128
|
+
const runTriggers = await readJsonFiles(
|
|
129
|
+
join(projectStateDir, runId, "triggers"),
|
|
130
|
+
parseTrigger,
|
|
131
|
+
);
|
|
132
|
+
triggers.push(
|
|
133
|
+
...runTriggers.filter(
|
|
134
|
+
(trigger) => input.status === undefined || trigger.status === input.status,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return triggers.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function enqueueSegmentEvolutionTrigger(
|
|
143
|
+
input: SegmentEvolutionTriggerInput,
|
|
144
|
+
): Promise<SegmentEvolutionTriggerRecord> {
|
|
145
|
+
const now = normalizeTimestamp(input.now);
|
|
146
|
+
const projectKey = sanitizeId(input.projectKey);
|
|
147
|
+
const sessionKey = sanitizeId(input.sessionKey);
|
|
148
|
+
const segmentId = sanitizeId(input.segmentId);
|
|
149
|
+
const id = createStableId("segment-trigger", [projectKey, sessionKey, segmentId]);
|
|
150
|
+
const trigger: SegmentEvolutionTriggerRecord = {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
id,
|
|
153
|
+
kind: "segment-evolution-trigger",
|
|
154
|
+
projectKey,
|
|
155
|
+
sessionKey,
|
|
156
|
+
runId: input.runId === undefined || input.runId === null ? null : sanitizeId(input.runId),
|
|
157
|
+
roleId: input.roleId === undefined || input.roleId === null ? null : sanitizeId(input.roleId),
|
|
158
|
+
segmentId,
|
|
159
|
+
segmentPath: sanitizeText(input.segmentPath),
|
|
160
|
+
strength: input.strength,
|
|
161
|
+
reason: input.reason,
|
|
162
|
+
summary: sanitizeText(input.summary),
|
|
163
|
+
status: "pending",
|
|
164
|
+
attempts: 0,
|
|
165
|
+
createdAt: now,
|
|
166
|
+
updatedAt: now,
|
|
167
|
+
processedBatchId: null,
|
|
168
|
+
lastError: null,
|
|
169
|
+
rawContentStored: false,
|
|
170
|
+
};
|
|
171
|
+
validateSegmentEvolutionTriggerRecord(trigger);
|
|
172
|
+
const path = resolveSegmentEvolutionTriggerPath(input.homeDir, trigger);
|
|
173
|
+
try {
|
|
174
|
+
await writeJson(path, trigger, { overwrite: false });
|
|
175
|
+
return trigger;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (!isFileExistsError(error)) throw error;
|
|
178
|
+
return parseSegmentTrigger(JSON.parse(await readFile(path, "utf8")) as unknown);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function listSegmentEvolutionTriggers(
|
|
183
|
+
input: SegmentEvolutionTriggerListInput,
|
|
184
|
+
): Promise<SegmentEvolutionTriggerRecord[]> {
|
|
185
|
+
const paths = resolveEvoDevPaths(input.homeDir);
|
|
186
|
+
const evolutionStateDir = join(paths.stateDir, "evolution");
|
|
187
|
+
const projectKeys =
|
|
188
|
+
input.projectKey === undefined
|
|
189
|
+
? await listDirectoryNames(evolutionStateDir)
|
|
190
|
+
: [sanitizeStorageId("projectKey", input.projectKey)];
|
|
191
|
+
const triggers: SegmentEvolutionTriggerRecord[] = [];
|
|
192
|
+
for (const projectKey of projectKeys) {
|
|
193
|
+
const projectSegmentsDir = join(evolutionStateDir, projectKey, "segments");
|
|
194
|
+
const projectTriggers = await readJsonFiles(projectSegmentsDir, parseSegmentTrigger);
|
|
195
|
+
triggers.push(
|
|
196
|
+
...projectTriggers.filter((trigger) => {
|
|
197
|
+
if (input.status !== undefined && trigger.status !== input.status) return false;
|
|
198
|
+
if (
|
|
199
|
+
input.runId !== undefined &&
|
|
200
|
+
trigger.runId !== sanitizeStorageId("runId", input.runId)
|
|
201
|
+
) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return triggers.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function groupTriggersByRun(triggers: EvolutionTriggerRecord[]): EvolutionTriggerRecord[][] {
|
|
212
|
+
const groups = new Map<string, EvolutionTriggerRecord[]>();
|
|
213
|
+
for (const trigger of triggers) {
|
|
214
|
+
const key = `${trigger.projectKey}\0${trigger.runId}`;
|
|
215
|
+
groups.set(key, [...(groups.get(key) ?? []), trigger]);
|
|
216
|
+
}
|
|
217
|
+
return [...groups.values()];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function updateTriggers(
|
|
221
|
+
homeDir: string,
|
|
222
|
+
triggers: EvolutionTriggerRecord[],
|
|
223
|
+
patch: {
|
|
224
|
+
status: EvolutionTriggerStatus;
|
|
225
|
+
updatedAt: string;
|
|
226
|
+
attempts?: ((trigger: EvolutionTriggerRecord) => number) | number;
|
|
227
|
+
processedBatchId?: string | null;
|
|
228
|
+
lastError?: string | null;
|
|
229
|
+
},
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
for (const trigger of triggers) {
|
|
232
|
+
const next: EvolutionTriggerRecord = {
|
|
233
|
+
...trigger,
|
|
234
|
+
status: patch.status,
|
|
235
|
+
updatedAt: patch.updatedAt,
|
|
236
|
+
attempts:
|
|
237
|
+
typeof patch.attempts === "function"
|
|
238
|
+
? patch.attempts(trigger)
|
|
239
|
+
: (patch.attempts ?? trigger.attempts),
|
|
240
|
+
processedBatchId:
|
|
241
|
+
patch.processedBatchId === undefined ? trigger.processedBatchId : patch.processedBatchId,
|
|
242
|
+
lastError: patch.lastError === undefined ? trigger.lastError : patch.lastError,
|
|
243
|
+
};
|
|
244
|
+
validateEvolutionTriggerRecord(next);
|
|
245
|
+
const paths = resolveEvolutionPaths({
|
|
246
|
+
homeDir,
|
|
247
|
+
projectKey: next.projectKey,
|
|
248
|
+
runId: next.runId,
|
|
249
|
+
});
|
|
250
|
+
await writeJson(join(paths.triggersDir, `${next.id}.json`), next, { overwrite: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function updateSegmentTriggers(
|
|
255
|
+
homeDir: string,
|
|
256
|
+
triggers: SegmentEvolutionTriggerRecord[],
|
|
257
|
+
patch: {
|
|
258
|
+
status: EvolutionTriggerStatus;
|
|
259
|
+
updatedAt: string;
|
|
260
|
+
attempts?: ((trigger: SegmentEvolutionTriggerRecord) => number) | number;
|
|
261
|
+
processedBatchId?: string | null;
|
|
262
|
+
lastError?: string | null;
|
|
263
|
+
},
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
for (const trigger of triggers) {
|
|
266
|
+
const next: SegmentEvolutionTriggerRecord = {
|
|
267
|
+
...trigger,
|
|
268
|
+
status: patch.status,
|
|
269
|
+
updatedAt: patch.updatedAt,
|
|
270
|
+
attempts:
|
|
271
|
+
typeof patch.attempts === "function"
|
|
272
|
+
? patch.attempts(trigger)
|
|
273
|
+
: (patch.attempts ?? trigger.attempts),
|
|
274
|
+
processedBatchId:
|
|
275
|
+
patch.processedBatchId === undefined ? trigger.processedBatchId : patch.processedBatchId,
|
|
276
|
+
lastError: patch.lastError === undefined ? trigger.lastError : patch.lastError,
|
|
277
|
+
};
|
|
278
|
+
validateSegmentEvolutionTriggerRecord(next);
|
|
279
|
+
await writeJson(resolveSegmentEvolutionTriggerPath(homeDir, next), next, { overwrite: true });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function resolveSegmentEvolutionTriggerPath(
|
|
284
|
+
homeDir: string,
|
|
285
|
+
trigger: Pick<SegmentEvolutionTriggerRecord, "projectKey" | "id">,
|
|
286
|
+
): string {
|
|
287
|
+
const paths = resolveEvoDevPaths(homeDir);
|
|
288
|
+
return join(
|
|
289
|
+
paths.stateDir,
|
|
290
|
+
"evolution",
|
|
291
|
+
sanitizeStorageId("projectKey", trigger.projectKey),
|
|
292
|
+
"segments",
|
|
293
|
+
`${sanitizeStorageId("segmentTrigger", trigger.id)}.json`,
|
|
294
|
+
);
|
|
295
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type SessionMemoryPolicySnapshot,
|
|
5
|
+
updateSessionMemoryFromHook,
|
|
6
|
+
} from "../evolution/evidence/session-memory/index.ts";
|
|
5
7
|
import {
|
|
6
8
|
createScopedKnowledgeContextPack,
|
|
7
9
|
formatScopedKnowledgePromptBlock,
|
|
8
10
|
hasContextInjectionReceipt,
|
|
9
11
|
writeContextInjectionReceipt,
|
|
10
|
-
} from "../knowledge/index.ts";
|
|
12
|
+
} from "../evolution/knowledge/index.ts";
|
|
13
|
+
import {
|
|
14
|
+
enqueueEvolutionTrigger,
|
|
15
|
+
resolveEvolutionTriggerDecision,
|
|
16
|
+
} from "../evolution/triggers/index.ts";
|
|
11
17
|
import { resolveTraceTeamContext } from "../runtime-logs/index.ts";
|
|
12
18
|
import {
|
|
13
19
|
type TaskContract,
|
|
@@ -24,6 +30,7 @@ import {
|
|
|
24
30
|
resolveTeamRunPaths,
|
|
25
31
|
updateTeamAgentHookState,
|
|
26
32
|
} from "../team/index.ts";
|
|
33
|
+
import { sha256Short } from "../utils/index.ts";
|
|
27
34
|
|
|
28
35
|
export const CANONICAL_HOOK_EVENT_TYPES = [
|
|
29
36
|
"SessionStart",
|
|
@@ -180,6 +187,7 @@ export interface HandleHookRuntimeInput {
|
|
|
180
187
|
receivedAt?: string;
|
|
181
188
|
environment?: Record<string, string | undefined>;
|
|
182
189
|
runtimeInjectionEnabled?: boolean;
|
|
190
|
+
sessionMemoryPolicy?: Partial<SessionMemoryPolicySnapshot>;
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
const DEFAULT_EVENT_SETTINGS: Record<CanonicalHookEventType, boolean> = {
|
|
@@ -468,7 +476,12 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
|
|
|
468
476
|
} else if (result === undefined) {
|
|
469
477
|
result = await handleAdditionalContext(input, input.event.type);
|
|
470
478
|
}
|
|
471
|
-
const
|
|
479
|
+
const sessionMemoryDiagnostics = await recordSessionMemoryFromHook(input);
|
|
480
|
+
const evolutionDiagnostics =
|
|
481
|
+
sessionMemoryDiagnostics.queuedSegmentTriggerId !== null ||
|
|
482
|
+
shouldSuppressLegacyEvolutionTrigger(input.event.type)
|
|
483
|
+
? { stateWrites: [], warnings: [] }
|
|
484
|
+
: await recordEvolutionTriggerFromHook(input);
|
|
472
485
|
const messageDiagnostics = await deliverPendingTeamMessagesFromHook(input, result);
|
|
473
486
|
const scopedContextDiagnostics = await injectScopedKnowledgeContextFromHook(input, result);
|
|
474
487
|
const teamStateDiagnostics = await recordTeamAgentHookStateFromHook(input);
|
|
@@ -476,7 +489,10 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
|
|
|
476
489
|
appendRuntimeDiagnostics(
|
|
477
490
|
appendRuntimeDiagnostics(
|
|
478
491
|
appendRuntimeDiagnostics(
|
|
479
|
-
appendRuntimeDiagnostics(
|
|
492
|
+
appendRuntimeDiagnostics(
|
|
493
|
+
appendRuntimeDiagnostics(result, diagnostics),
|
|
494
|
+
sessionMemoryDiagnostics,
|
|
495
|
+
),
|
|
480
496
|
evolutionDiagnostics,
|
|
481
497
|
),
|
|
482
498
|
messageDiagnostics,
|
|
@@ -611,6 +627,37 @@ async function recordEvolutionTriggerFromHook(
|
|
|
611
627
|
}
|
|
612
628
|
}
|
|
613
629
|
|
|
630
|
+
async function recordSessionMemoryFromHook(
|
|
631
|
+
input: HandleHookRuntimeInput,
|
|
632
|
+
): Promise<HookRuntimeDiagnostics & { queuedSegmentTriggerId: string | null }> {
|
|
633
|
+
try {
|
|
634
|
+
const result = await updateSessionMemoryFromHook({
|
|
635
|
+
homeDir: input.homeDir,
|
|
636
|
+
target: input.target,
|
|
637
|
+
event: input.event,
|
|
638
|
+
rawPayload: input.rawPayload,
|
|
639
|
+
environment: input.environment,
|
|
640
|
+
receivedAt: input.receivedAt,
|
|
641
|
+
policy: input.sessionMemoryPolicy,
|
|
642
|
+
});
|
|
643
|
+
return {
|
|
644
|
+
stateWrites: result.stateWrites,
|
|
645
|
+
warnings: result.warnings,
|
|
646
|
+
queuedSegmentTriggerId: result.queuedSegmentTriggerId,
|
|
647
|
+
};
|
|
648
|
+
} catch {
|
|
649
|
+
return {
|
|
650
|
+
stateWrites: [],
|
|
651
|
+
warnings: ["Session Memory state could not be updated at this hook safe point."],
|
|
652
|
+
queuedSegmentTriggerId: null,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function shouldSuppressLegacyEvolutionTrigger(eventType: CanonicalHookEventType): boolean {
|
|
658
|
+
return eventType === "Stop" || eventType === "SubagentStop" || eventType === "SessionEnd";
|
|
659
|
+
}
|
|
660
|
+
|
|
614
661
|
async function deliverPendingTeamMessagesFromHook(
|
|
615
662
|
input: HandleHookRuntimeInput,
|
|
616
663
|
result: HookRuntimeResult | undefined,
|
|
@@ -1139,7 +1186,7 @@ function resolveHookSessionKey(payload: Record<string, unknown>): string {
|
|
|
1139
1186
|
const sessionId = optionalPayloadString(payload.session_id ?? payload.sessionId);
|
|
1140
1187
|
const cwd = optionalPayloadString(payload.cwd);
|
|
1141
1188
|
const source = sessionId ?? cwd ?? "local";
|
|
1142
|
-
return `session-${
|
|
1189
|
+
return `session-${sha256Short(source)}`;
|
|
1143
1190
|
}
|
|
1144
1191
|
|
|
1145
1192
|
async function writeJsonFile(path: string, value: unknown): Promise<void> {
|
|
@@ -1297,7 +1344,7 @@ function stableEventId(
|
|
|
1297
1344
|
|
|
1298
1345
|
function hashOptionalIdentifier(value: unknown): string | null {
|
|
1299
1346
|
if (typeof value !== "string" || value.length === 0) return null;
|
|
1300
|
-
return `sha256-${
|
|
1347
|
+
return `sha256-${sha256Short(value)}`;
|
|
1301
1348
|
}
|
|
1302
1349
|
|
|
1303
1350
|
function optionalBoolean(value: unknown, fallback: boolean, path: string): boolean {
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,17 @@ export * from "./code-agent-traces/index.ts";
|
|
|
4
4
|
export * from "./config/index.ts";
|
|
5
5
|
export * from "./daemon/index.ts";
|
|
6
6
|
export * from "./evolution/index.ts";
|
|
7
|
+
export * as evolutionCandidates from "./evolution/candidates/index.ts";
|
|
8
|
+
export * as evolutionControl from "./evolution/control/index.ts";
|
|
9
|
+
export * as evolutionEvidence from "./evolution/evidence/index.ts";
|
|
10
|
+
export * from "./evolution/evidence/session-memory/index.ts";
|
|
11
|
+
export * from "./evolution/knowledge/index.ts";
|
|
12
|
+
export * as evolutionKnowledge from "./evolution/knowledge/index.ts";
|
|
13
|
+
export * as evolutionProcessor from "./evolution/processor/index.ts";
|
|
14
|
+
export * from "./evolution/review/index.ts";
|
|
15
|
+
export * as evolutionReview from "./evolution/review/index.ts";
|
|
16
|
+
export * as evolutionTriggers from "./evolution/triggers/index.ts";
|
|
7
17
|
export * from "./hooks/index.ts";
|
|
8
|
-
export * from "./knowledge/index.ts";
|
|
9
|
-
export * from "./learning/index.ts";
|
|
10
18
|
export * from "./observability/index.ts";
|
|
11
19
|
export * from "./pack/index.ts";
|
|
12
20
|
export * from "./plugins/index.ts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
2
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
4
3
|
import { resolveEvoDevPaths } from "../config/paths.ts";
|
|
4
|
+
import { normalizeTimestamp, sha256Short } from "../utils/index.ts";
|
|
5
5
|
|
|
6
6
|
export type EvoDevLogPhase = "started" | "completed" | "failed";
|
|
7
7
|
export type EvoDevDebugLogLevel = "verbose" | "debug" | "info" | "warn" | "error";
|
|
@@ -559,7 +559,7 @@ export function resolveTraceSessionKey(payload: unknown): string {
|
|
|
559
559
|
const sessionId = optionalString(record.session_id ?? record.sessionId);
|
|
560
560
|
const cwd = optionalString(record.cwd);
|
|
561
561
|
const source = sessionId ?? cwd ?? "local";
|
|
562
|
-
return `session-${
|
|
562
|
+
return `session-${sha256Short(source)}`;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
function sanitizeTraceLogEntryForAppend(entry: TraceLogEntryV1): TraceLogEntryV1 {
|
|
@@ -644,12 +644,6 @@ async function appendJsonLine(path: string, value: unknown): Promise<void> {
|
|
|
644
644
|
await appendFile(path, `${JSON.stringify(value)}\n`, "utf8");
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
-
function normalizeTimestamp(value?: Date | string): string {
|
|
648
|
-
if (value instanceof Date) return value.toISOString();
|
|
649
|
-
if (typeof value === "string" && value.trim() !== "") return new Date(value).toISOString();
|
|
650
|
-
return new Date().toISOString();
|
|
651
|
-
}
|
|
652
|
-
|
|
653
647
|
function formatLocalDateKey(timestamp: string): string {
|
|
654
648
|
const date = new Date(timestamp);
|
|
655
649
|
const year = date.getFullYear();
|
|
@@ -727,8 +721,7 @@ function createExecutionEventId(input: {
|
|
|
727
721
|
summary: string;
|
|
728
722
|
metadata: Record<string, EvoDevExecutionEventMetadataValue>;
|
|
729
723
|
}): string {
|
|
730
|
-
|
|
731
|
-
return `exec-${hash}`;
|
|
724
|
+
return `exec-${sha256Short(JSON.stringify(input))}`;
|
|
732
725
|
}
|
|
733
726
|
|
|
734
727
|
function sanitizeDebugValue(value: unknown, depth = 0): unknown {
|
|
@@ -775,8 +768,7 @@ function sanitizePersistentIdentifier(value: string, prefix: string): string {
|
|
|
775
768
|
if (!SENSITIVE_EVENT_TEXT_PATTERN.test(value) && !SENSITIVE_EVENT_TEXT_PATTERN.test(pathSafe)) {
|
|
776
769
|
return pathSafe;
|
|
777
770
|
}
|
|
778
|
-
|
|
779
|
-
return `${prefix}-${hash}`;
|
|
771
|
+
return `${prefix}-${sha256Short(value)}`;
|
|
780
772
|
}
|
|
781
773
|
|
|
782
774
|
function stripTrailingSlash(path: string): string {
|
package/src/team/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
formatScopedKnowledgePromptBlock,
|
|
13
13
|
hasContextInjectionReceipt,
|
|
14
14
|
writeContextInjectionReceipt,
|
|
15
|
-
} from "../knowledge/index.ts";
|
|
15
|
+
} from "../evolution/knowledge/index.ts";
|
|
16
16
|
import { resolveProjectLogKey } from "../runtime-logs/index.ts";
|
|
17
17
|
import { renderTeamRoleStartupPrompt } from "./prompts.ts";
|
|
18
18
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function isNotFoundError(error: unknown): boolean {
|
|
2
|
+
return (
|
|
3
|
+
error instanceof Error &&
|
|
4
|
+
(("code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") ||
|
|
5
|
+
error.message.includes("ENOENT"))
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isFileExistsError(error: unknown): boolean {
|
|
10
|
+
return (
|
|
11
|
+
error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EEXIST"
|
|
12
|
+
);
|
|
13
|
+
}
|
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { isNotFoundError } from "./errors.ts";
|
|
4
|
+
|
|
5
|
+
export async function pathExists(path: string): Promise<boolean> {
|
|
6
|
+
try {
|
|
7
|
+
await stat(path);
|
|
8
|
+
return true;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (isNotFoundError(error)) return false;
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function readJsonFile<T>(path: string, parse: (value: unknown) => T): Promise<T> {
|
|
16
|
+
return parse(JSON.parse(await readFile(path, "utf8")) as unknown);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readJsonFiles<T>(dir: string, parse: (value: unknown) => T): Promise<T[]> {
|
|
20
|
+
if (!(await pathExists(dir))) return [];
|
|
21
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
22
|
+
const values: T[] = [];
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
25
|
+
values.push(await readJsonFile(join(dir, entry.name), parse));
|
|
26
|
+
}
|
|
27
|
+
return values;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeJsonFile(
|
|
31
|
+
path: string,
|
|
32
|
+
value: unknown,
|
|
33
|
+
options: { overwrite?: boolean } = {},
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
await mkdir(dirname(path), { recursive: true });
|
|
36
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
flag: options.overwrite === false ? "wx" : "w",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function sha256Hex(value: string): string {
|
|
4
|
+
return createHash("sha256").update(value).digest("hex");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sha256Short(value: string, length = 16): string {
|
|
8
|
+
return sha256Hex(value).slice(0, length);
|
|
9
|
+
}
|
package/src/utils/ids.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { sha256Short } from "./hash.ts";
|
|
2
|
+
|
|
3
|
+
export function createStableId(prefix: string, parts: string[]): string {
|
|
4
|
+
return `${prefix}-${sha256Short(parts.join("\0"))}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sanitizeStorageId(value: string, fallbackPrefix: string): string {
|
|
8
|
+
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120);
|
|
9
|
+
return sanitized === "" || sanitized === "." || sanitized === ".."
|
|
10
|
+
? `${fallbackPrefix}-local`
|
|
11
|
+
: sanitized;
|
|
12
|
+
}
|