@vitest-agent/engine 3.0.0 → 3.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/index.d.ts +18 -13
- package/layers/DataStoreLive.js +42 -43
- package/layers/PerClientSessionMapLive.js +3 -3
- package/layers/ProjectDiscoveryLive.js +11 -10
- package/layers/ProjectIdentityLive.js +5 -6
- package/package.json +21 -20
- package/platform.js +2 -2
- package/programs/hook-paths.js +1 -1
- package/programs/record-turn.js +11 -16
- package/programs/resolve-session-for-recording.js +4 -3
- package/tsdoc-metadata.json +1 -1
- package/utils/resolve-project-key-from-cwd.js +19 -36
- package/version.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1565,7 +1565,7 @@ declare const ProjectDiscovery_base: Context.ServiceClass<ProjectDiscovery, "vit
|
|
|
1565
1565
|
/** @public */
|
|
1566
1566
|
export declare class ProjectDiscovery extends ProjectDiscovery_base {}
|
|
1567
1567
|
/** @public */
|
|
1568
|
-
export declare const ProjectDiscoveryLive: Layer.Layer<ProjectDiscovery, never, FileSystem.FileSystem>;
|
|
1568
|
+
export declare const ProjectDiscoveryLive: Layer.Layer<ProjectDiscovery, never, FileSystem.FileSystem | Path.Path>;
|
|
1569
1569
|
/** @public */
|
|
1570
1570
|
export declare const ProjectDiscoveryTest: {
|
|
1571
1571
|
readonly layer: (entries: ReadonlyArray<TestFileEntry>) => Layer.Layer<ProjectDiscovery>;
|
|
@@ -1865,8 +1865,8 @@ export interface SqliteStack {
|
|
|
1865
1865
|
}
|
|
1866
1866
|
/**
|
|
1867
1867
|
* The Node platform services every SQLite stack and every Live layer share.
|
|
1868
|
-
* `NodeServices.layer` aggregates
|
|
1869
|
-
*
|
|
1868
|
+
* `NodeServices.layer` aggregates ChildProcessSpawner | FileSystem | Path |
|
|
1869
|
+
* Stdio | Terminal; since Effect 4.1 it carries no crypto service.
|
|
1870
1870
|
* @public
|
|
1871
1871
|
*/
|
|
1872
1872
|
export declare const NodePlatformLayer: Layer.Layer<NodeServices.NodeServices, never, never>;
|
|
@@ -2207,7 +2207,7 @@ export interface RecordTddArtifactResult {
|
|
|
2207
2207
|
* @param input - the artifact to record, keyed by host chat id
|
|
2208
2208
|
* @public
|
|
2209
2209
|
*/
|
|
2210
|
-
export declare const recordTddArtifactEffect: (input: RecordTddArtifactInput) => Effect.Effect<RecordTddArtifactResult, DataStoreError | Error, DataReader | DataStore | FileSystem.FileSystem>;
|
|
2210
|
+
export declare const recordTddArtifactEffect: (input: RecordTddArtifactInput) => Effect.Effect<RecordTddArtifactResult, DataStoreError | Error, DataReader | DataStore | FileSystem.FileSystem | Path.Path>;
|
|
2211
2211
|
/**
|
|
2212
2212
|
* Input for {@link recordTddArtifactByTaskIdEffect}: an artifact keyed by an
|
|
2213
2213
|
* explicit TDD task id.
|
|
@@ -2290,7 +2290,7 @@ export interface DispatchRecordTddArtifactInput {
|
|
|
2290
2290
|
* @param input - the artifact plus either a chat id or a TDD task id
|
|
2291
2291
|
* @public
|
|
2292
2292
|
*/
|
|
2293
|
-
export declare const dispatchRecordTddArtifactEffect: (input: DispatchRecordTddArtifactInput) => Effect.Effect<RecordTddArtifactResult, DataStoreError | Error, DataReader | DataStore | FileSystem.FileSystem>;
|
|
2293
|
+
export declare const dispatchRecordTddArtifactEffect: (input: DispatchRecordTddArtifactInput) => Effect.Effect<RecordTddArtifactResult, DataStoreError | Error, DataReader | DataStore | FileSystem.FileSystem | Path.Path>;
|
|
2294
2294
|
/**
|
|
2295
2295
|
* Outcome of {@link parseAndValidateTurnPayload}: the decoded `TurnPayload`
|
|
2296
2296
|
* on success, or a human-readable error string.
|
|
@@ -2308,6 +2308,9 @@ export type ParseResult = {
|
|
|
2308
2308
|
* Parse a raw JSON string and validate it against the `TurnPayload` schema
|
|
2309
2309
|
* without throwing.
|
|
2310
2310
|
*
|
|
2311
|
+
* Text that is not JSON at all reads `Invalid JSON: …`; JSON whose body is
|
|
2312
|
+
* not a `TurnPayload` reads `Invalid TurnPayload: …`.
|
|
2313
|
+
*
|
|
2311
2314
|
* @param raw - the JSON text delivered by the hook
|
|
2312
2315
|
* @returns the decoded payload, or an error describing why it was rejected
|
|
2313
2316
|
* @public
|
|
@@ -2350,7 +2353,7 @@ export interface RecordTurnInput {
|
|
|
2350
2353
|
*/
|
|
2351
2354
|
export declare const recordTurnEffect: (input: RecordTurnInput) => Effect.Effect<{
|
|
2352
2355
|
turnId: number;
|
|
2353
|
-
}, Error, DataReader | DataStore | FileSystem.FileSystem>;
|
|
2356
|
+
}, Error, DataReader | DataStore | FileSystem.FileSystem | Path.Path>;
|
|
2354
2357
|
/**
|
|
2355
2358
|
* Input for {@link recordRunWorkspaceChangesEffect}: one commit plus the
|
|
2356
2359
|
* files it touched.
|
|
@@ -2485,7 +2488,7 @@ export interface ResolveSessionForRecordingInput {
|
|
|
2485
2488
|
* @returns the resolved (or newly created) session row
|
|
2486
2489
|
* @public
|
|
2487
2490
|
*/
|
|
2488
|
-
export declare const resolveSessionForRecording: (input: ResolveSessionForRecordingInput) => Effect.Effect<SessionDetail, DataStoreError, DataReader | DataStore | FileSystem.FileSystem>;
|
|
2491
|
+
export declare const resolveSessionForRecording: (input: ResolveSessionForRecordingInput) => Effect.Effect<SessionDetail, DataStoreError, DataReader | DataStore | FileSystem.FileSystem | Path.Path>;
|
|
2489
2492
|
/**
|
|
2490
2493
|
* Recovered session attribution context — the canonical UUIDs the
|
|
2491
2494
|
* SessionStart hook wrote to `${CLAUDE_ENV_FILE}` (auto-sourced into the
|
|
@@ -2854,7 +2857,7 @@ export interface ResolveDataPathOptions {
|
|
|
2854
2857
|
* @param options - Optional programmatic overrides.
|
|
2855
2858
|
* @public
|
|
2856
2859
|
*/
|
|
2857
|
-
export declare const resolveDataPath: (projectDir: string, options?: ResolveDataPathOptions) => Effect.Effect<string, import("@effected/xdg").AppDirsError | import("effect/PlatformError").PlatformError | ProjectIdentitySourceError | import("@effected/config-file").ConfigReadError<import("@effected/config-file").ConfigCodecError>, AppDirs$1 | FileSystem.FileSystem | VitestAgentConfigFile>;
|
|
2860
|
+
export declare const resolveDataPath: (projectDir: string, options?: ResolveDataPathOptions) => Effect.Effect<string, import("@effected/xdg").AppDirsError | import("effect/PlatformError").PlatformError | ProjectIdentitySourceError | import("@effected/config-file").ConfigReadError<import("@effected/config-file").ConfigCodecError>, AppDirs$1 | FileSystem.FileSystem | import("effect/Path").Path | VitestAgentConfigFile>;
|
|
2858
2861
|
/**
|
|
2859
2862
|
* Compute the project key for a workspace by reading `package.json`
|
|
2860
2863
|
* fields directly. Returns the canonical `host__path` form when a git
|
|
@@ -2864,15 +2867,17 @@ export declare const resolveDataPath: (projectDir: string, options?: ResolveData
|
|
|
2864
2867
|
* (or `"anonymous-project"`) so callers don't have to handle the
|
|
2865
2868
|
* empty case.
|
|
2866
2869
|
*
|
|
2867
|
-
* Synchronous and reads the real disk through `node:fs
|
|
2868
|
-
*
|
|
2869
|
-
*
|
|
2870
|
+
* Synchronous and reads the real disk through `node:fs`: it runs
|
|
2871
|
+
* {@link resolveProjectKeyFromCwdEffect} under `Effect.runSync` over a
|
|
2872
|
+
* read-only synchronous `FileSystem` and the host's Node `Path`. Effect
|
|
2873
|
+
* callers that already hold a `FileSystem` and `Path` use the Effect form
|
|
2874
|
+
* directly.
|
|
2870
2875
|
* @public
|
|
2871
2876
|
*/
|
|
2872
2877
|
export declare const resolveProjectKeyFromCwd: (cwd: string) => string;
|
|
2873
2878
|
/**
|
|
2874
2879
|
* {@link resolveProjectKeyFromCwd} read through the ambient
|
|
2875
|
-
* `FileSystem`
|
|
2880
|
+
* `FileSystem` and `Path` services instead of `node:fs` — same upward walk to the
|
|
2876
2881
|
* nearest `package.json` (`@effected/walker`'s `Walker.ascend` +
|
|
2877
2882
|
* `Walker.findUpward`), same precedence, same basename fallback.
|
|
2878
2883
|
*
|
|
@@ -2883,7 +2888,7 @@ export declare const resolveProjectKeyFromCwd: (cwd: string) => string;
|
|
|
2883
2888
|
* @param cwd - Absolute directory to start the upward walk from.
|
|
2884
2889
|
* @public
|
|
2885
2890
|
*/
|
|
2886
|
-
export declare const resolveProjectKeyFromCwdEffect: (cwd: string) => Effect.Effect<string, never, FileSystem.FileSystem>;
|
|
2891
|
+
export declare const resolveProjectKeyFromCwdEffect: (cwd: string) => Effect.Effect<string, never, FileSystem.FileSystem | Path.Path>;
|
|
2887
2892
|
/**
|
|
2888
2893
|
* Resolve the normalized workspace key for the workspace containing
|
|
2889
2894
|
* `projectDir`.
|
package/layers/DataStoreLive.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DataStore, INLINE_ATTACHMENT_BODY_CAP_BYTES } from "../services/DataStore.js";
|
|
2
2
|
import { PersistBatch, withPersistSavepoint } from "../utils/persist-transaction.js";
|
|
3
|
-
import { Agent, AgentNotFoundError, BehaviorNotFoundError, DataStoreError, GoalNotFoundError, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, coerceErrorText, extractSqlReason } from "@vitest-agent/sdk";
|
|
4
|
-
import { Clock, Effect, Layer, Option } from "effect";
|
|
3
|
+
import { Agent, AgentNotFoundError, BehaviorNotFoundError, DataStoreError, FileEditPayload, GoalNotFoundError, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, ToolResultPayload, coerceErrorText, extractSqlReason } from "@vitest-agent/sdk";
|
|
4
|
+
import { Clock, Effect, Layer, Option, Schema, Struct } from "effect";
|
|
5
5
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
6
6
|
import { Buffer } from "node:buffer";
|
|
7
|
-
import {
|
|
7
|
+
import { SecureRandom } from "effect/crypto";
|
|
8
8
|
|
|
9
9
|
// src/layers/DataStoreLive.ts
|
|
10
10
|
/**
|
|
@@ -50,6 +50,8 @@ const behaviorRowFromDb = (row) => ({
|
|
|
50
50
|
createdAt: row.created_at
|
|
51
51
|
});
|
|
52
52
|
const boolToInt = (v) => v === void 0 ? null : v ? 1 : 0;
|
|
53
|
+
const decodeFileEditPayload = Schema.decodeUnknownEffect(Schema.fromJsonString(FileEditPayload.mapFields(Struct.omit(["type"]))));
|
|
54
|
+
const decodeToolResultPayload = Schema.decodeUnknownEffect(Schema.fromJsonString(ToolResultPayload.mapFields(Struct.omit(["type"]))));
|
|
53
55
|
/** @public */
|
|
54
56
|
const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
55
57
|
const sql = yield* SqlClient;
|
|
@@ -456,45 +458,42 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
456
458
|
if (input.turnNo !== void 0) yield* sql`INSERT INTO turns (session_id, turn_no, type, payload, occurred_at) VALUES (${input.sessionId}, ${input.turnNo}, ${input.type}, ${input.payload}, ${input.occurredAt})`;
|
|
457
459
|
else yield* sql`INSERT INTO turns (session_id, turn_no, type, payload, occurred_at) SELECT ${input.sessionId}, COALESCE(MAX(turn_no), 0) + 1, ${input.type}, ${input.payload}, ${input.occurredAt} FROM turns WHERE session_id = ${input.sessionId}`;
|
|
458
460
|
const turnId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
)
|
|
496
|
-
`;
|
|
497
|
-
}
|
|
461
|
+
const invalidPayload = (table) => (e) => new DataStoreError({
|
|
462
|
+
operation: "write",
|
|
463
|
+
table,
|
|
464
|
+
reason: `invalid turn payload: ${e.message}`
|
|
465
|
+
});
|
|
466
|
+
if (input.type === "file_edit") {
|
|
467
|
+
const payload = yield* decodeFileEditPayload(input.payload).pipe(Effect.mapError(invalidPayload("file_edits")));
|
|
468
|
+
const filePath = payload.file_path;
|
|
469
|
+
yield* sql`INSERT OR IGNORE INTO files (path) VALUES (${filePath})`;
|
|
470
|
+
const fileId = (yield* sql`SELECT id FROM files WHERE path = ${filePath}`)[0].id;
|
|
471
|
+
yield* sql`
|
|
472
|
+
INSERT INTO file_edits (turn_id, file_id, edit_kind, lines_added, lines_removed, diff)
|
|
473
|
+
VALUES (
|
|
474
|
+
${turnId},
|
|
475
|
+
${fileId},
|
|
476
|
+
${payload.edit_kind},
|
|
477
|
+
${payload.lines_added ?? null},
|
|
478
|
+
${payload.lines_removed ?? null},
|
|
479
|
+
${payload.diff ?? null}
|
|
480
|
+
)
|
|
481
|
+
`;
|
|
482
|
+
} else if (input.type === "tool_result") {
|
|
483
|
+
const payload = yield* decodeToolResultPayload(input.payload).pipe(Effect.mapError(invalidPayload("tool_invocations")));
|
|
484
|
+
const rawToolName = payload.tool_name;
|
|
485
|
+
const toolName = rawToolName.startsWith("mcp__") ? rawToolName.split("__").at(-1) ?? rawToolName : rawToolName;
|
|
486
|
+
yield* sql`
|
|
487
|
+
INSERT INTO tool_invocations (turn_id, tool_name, params_hash, result_summary, duration_ms, success)
|
|
488
|
+
VALUES (
|
|
489
|
+
${turnId},
|
|
490
|
+
${toolName},
|
|
491
|
+
${null},
|
|
492
|
+
${payload.result_summary ?? null},
|
|
493
|
+
${payload.duration_ms ?? null},
|
|
494
|
+
${boolToInt(payload.success) ?? 0}
|
|
495
|
+
)
|
|
496
|
+
`;
|
|
498
497
|
}
|
|
499
498
|
return turnId;
|
|
500
499
|
})).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => e instanceof DataStoreError ? e : new DataStoreError({
|
|
@@ -1295,7 +1294,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
1295
1294
|
const parentSession = parentRows[0].session_id;
|
|
1296
1295
|
if (parentSession !== input.sessionId && parentSession !== registeringParentSessionId) return yield* Effect.fail(new RegistrationConflictError({ reason: `parent agent ${input.parentAgentId} belongs to session ${parentSession}, which is neither the registering session ${input.sessionId} nor its parent session ${registeringParentSessionId ?? "(none)"}` }));
|
|
1297
1296
|
}
|
|
1298
|
-
const agentId = input.agentId ??
|
|
1297
|
+
const agentId = input.agentId ?? SecureRandom.uuidV4Unsafe();
|
|
1299
1298
|
yield* sql`
|
|
1300
1299
|
INSERT INTO agents (
|
|
1301
1300
|
agent_id, session_id, parent_agent_id, conversation_id, agent_type,
|
|
@@ -2,7 +2,7 @@ import { PerClientSessionMapReader, PerClientSessionMapWriter } from "../service
|
|
|
2
2
|
import { DataStoreError, extractSqlReason } from "@vitest-agent/sdk";
|
|
3
3
|
import { Effect, Layer, Option } from "effect";
|
|
4
4
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
5
|
-
import {
|
|
5
|
+
import { SecureRandom } from "effect/crypto";
|
|
6
6
|
import { basename } from "node:path";
|
|
7
7
|
|
|
8
8
|
// src/layers/PerClientSessionMapLive.ts
|
|
@@ -83,7 +83,7 @@ const PerClientSessionMapWriterLive = Layer.effect(PerClientSessionMapWriter, Ef
|
|
|
83
83
|
yield* sql`UPDATE conversation_map SET last_seen_at = ${now} WHERE transcript_uuid = ${uuid}`;
|
|
84
84
|
return existing[0].conversation_id;
|
|
85
85
|
}
|
|
86
|
-
const conversationId =
|
|
86
|
+
const conversationId = SecureRandom.uuidV4Unsafe();
|
|
87
87
|
yield* sql`
|
|
88
88
|
INSERT INTO conversation_map (transcript_uuid, transcript_path, conversation_id, first_seen_at, last_seen_at)
|
|
89
89
|
VALUES (${uuid}, ${transcriptPath}, ${conversationId}, ${now}, ${now})
|
|
@@ -102,7 +102,7 @@ const PerClientSessionMapWriterLive = Layer.effect(PerClientSessionMapWriter, Ef
|
|
|
102
102
|
mainAgentId: existing[0].main_agent_id,
|
|
103
103
|
conversationId: existing[0].conversation_id
|
|
104
104
|
};
|
|
105
|
-
const mainAgentId =
|
|
105
|
+
const mainAgentId = SecureRandom.uuidV4Unsafe();
|
|
106
106
|
const now = Date.now();
|
|
107
107
|
yield* sql`
|
|
108
108
|
INSERT INTO session_map (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ProjectDiscovery } from "../services/ProjectDiscovery.js";
|
|
2
2
|
import { DiscoveryError, NON_DISCOVERABLE_DIRS } from "@vitest-agent/sdk";
|
|
3
3
|
import { Effect, FileSystem, Layer, Path } from "effect";
|
|
4
|
+
import { ErrorText } from "@effected/engine";
|
|
4
5
|
import { GlobPattern, GlobPatternOptions } from "@effected/glob";
|
|
5
6
|
import { descend } from "@effected/walker";
|
|
6
7
|
|
|
@@ -17,39 +18,39 @@ function testFileToSource(testFile) {
|
|
|
17
18
|
return testFile.replace(/\.(test|spec)\.(ts|tsx)$/, ".$2");
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
|
-
* Every test file under `rootDir`,
|
|
21
|
-
* relative path. `@effected/walker`'s `descend` follows directory symlinks
|
|
21
|
+
* Every test file under `rootDir`, each joined to `rootDir` with the ambient
|
|
22
|
+
* `Path` (backslashes on win32), sorted by relative path. `@effected/walker`'s `descend` follows directory symlinks
|
|
22
23
|
* (as the former `fs.stat`-based walk did) under its ancestor-chain cycle
|
|
23
24
|
* guard, so a link back to an ancestor is skipped instead of recursing until
|
|
24
25
|
* the OS answers `ELOOP` and failing the whole discovery.
|
|
25
26
|
*/
|
|
26
27
|
const walkTestFiles = (rootDir) => Effect.gen(function* () {
|
|
27
28
|
const pattern = yield* GlobPattern.compile(TEST_FILE_GLOB, TEST_FILE_GLOB_OPTIONS);
|
|
28
|
-
const
|
|
29
|
+
const path = yield* Path.Path;
|
|
30
|
+
return (yield* descend(pattern, {
|
|
29
31
|
cwd: rootDir,
|
|
30
32
|
prune: SKIP_DIRS,
|
|
31
33
|
followSymlinks: true
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
return relative.map((rel) => `${base}/${rel}`);
|
|
35
|
-
}).pipe(Effect.provide(Path.layer));
|
|
34
|
+
})).map((rel) => path.join(rootDir, rel));
|
|
35
|
+
});
|
|
36
36
|
/** @public */
|
|
37
37
|
const ProjectDiscoveryLive = Layer.effect(ProjectDiscovery, Effect.gen(function* () {
|
|
38
38
|
const fs = yield* FileSystem.FileSystem;
|
|
39
|
+
const path = yield* Path.Path;
|
|
39
40
|
const mapTestToSource = (testFile) => Effect.gen(function* () {
|
|
40
41
|
const sourceFile = testFileToSource(testFile);
|
|
41
42
|
return (yield* fs.exists(sourceFile)) ? [sourceFile] : [];
|
|
42
43
|
}).pipe(Effect.mapError((error) => new DiscoveryError({
|
|
43
44
|
operation: "stat",
|
|
44
45
|
path: testFile,
|
|
45
|
-
reason:
|
|
46
|
+
reason: ErrorText.describe(error)
|
|
46
47
|
})));
|
|
47
48
|
return {
|
|
48
49
|
discoverTestFiles: (rootDir) => Effect.gen(function* () {
|
|
49
|
-
const testFiles = yield* walkTestFiles(rootDir).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.mapError((error) => new DiscoveryError({
|
|
50
|
+
const testFiles = yield* walkTestFiles(rootDir).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), Effect.mapError((error) => new DiscoveryError({
|
|
50
51
|
operation: "glob",
|
|
51
52
|
path: rootDir,
|
|
52
|
-
reason:
|
|
53
|
+
reason: ErrorText.describe(error)
|
|
53
54
|
})));
|
|
54
55
|
const entries = [];
|
|
55
56
|
for (const testFile of testFiles) {
|
|
@@ -6,11 +6,10 @@ import { ProjectIdentityNotResolvableError, ProjectIdentitySourceError, VitestAg
|
|
|
6
6
|
import { Effect, FileSystem, Layer, Option, Result } from "effect";
|
|
7
7
|
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
8
8
|
import { AppDirs } from "@effected/xdg";
|
|
9
|
+
import { ErrorText } from "@effected/engine";
|
|
9
10
|
import { LenientManifest } from "@effected/package-json";
|
|
10
11
|
|
|
11
12
|
// src/layers/ProjectIdentityLive.ts
|
|
12
|
-
/** The one-line text of a kit failure, for `ProjectIdentitySourceError.detail`. */
|
|
13
|
-
const detailOf = (cause) => cause instanceof Error && cause.message.length > 0 ? cause.message : String(cause);
|
|
14
13
|
/**
|
|
15
14
|
* The git remote is an OPTIONAL source: a repository with no
|
|
16
15
|
* `remote.origin.url`, a directory that is not a git checkout, or a machine
|
|
@@ -38,14 +37,14 @@ const readPackageRepoUrl = (packageJsonPath) => Effect.gen(function* () {
|
|
|
38
37
|
const raw = yield* (yield* FileSystem.FileSystem).readFileString(packageJsonPath).pipe(Effect.map(Option.some), Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(new ProjectIdentitySourceError({
|
|
39
38
|
source: "package-json",
|
|
40
39
|
path: packageJsonPath,
|
|
41
|
-
detail:
|
|
40
|
+
detail: ErrorText.describe(error),
|
|
42
41
|
cause: error
|
|
43
42
|
}))));
|
|
44
43
|
if (Option.isNone(raw)) return Option.none();
|
|
45
44
|
const manifest = yield* LenientManifest.parse(raw.value).pipe(Effect.map(Option.some), Effect.catchTag("PackageJsonSyntaxError", (cause) => cause.reason === "not-an-object" ? Effect.succeed(Option.none()) : Effect.fail(new ProjectIdentitySourceError({
|
|
46
45
|
source: "package-json",
|
|
47
46
|
path: packageJsonPath,
|
|
48
|
-
detail:
|
|
47
|
+
detail: ErrorText.describe(cause),
|
|
49
48
|
cause
|
|
50
49
|
}))));
|
|
51
50
|
return Option.flatMap(manifest, (m) => Option.fromNullishOr(repositoryUrlOf(m.repository)));
|
|
@@ -54,14 +53,14 @@ const collectCandidates = (workspaceRoot, options) => Effect.gen(function* () {
|
|
|
54
53
|
const tomlConfig = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({})).pipe(Effect.mapError((cause) => new ProjectIdentitySourceError({
|
|
55
54
|
source: "toml",
|
|
56
55
|
path: cause._tag === "ConfigValidationError" ? Option.getOrElse(cause.path, () => workspaceRoot) : cause.path ?? workspaceRoot,
|
|
57
|
-
detail:
|
|
56
|
+
detail: ErrorText.describe(cause),
|
|
58
57
|
cause
|
|
59
58
|
})));
|
|
60
59
|
const gitRemote = yield* readGitRemote(workspaceRoot);
|
|
61
60
|
const root = (yield* (yield* WorkspaceDiscovery).listPackagesIn(workspaceRoot).pipe(Effect.catchTag("WorkspaceRootNotFoundError", () => Effect.succeed([])), Effect.mapError((cause) => new ProjectIdentitySourceError({
|
|
62
61
|
source: "workspace",
|
|
63
62
|
path: cause._tag === "WorkspacePatternError" ? cause.root : cause.path,
|
|
64
|
-
detail:
|
|
63
|
+
detail: ErrorText.describe(cause),
|
|
65
64
|
cause
|
|
66
65
|
})))).find((pkg) => pkg.isRootWorkspace);
|
|
67
66
|
const packageJsonRepoUrl = root ? yield* readPackageRepoUrl(root.packageJsonPath) : Option.none();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/engine",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared services, layers, SQLite data layer, migrations and platform resolution for the vitest-agent CLI and MCP server.",
|
|
6
6
|
"keywords": [
|
|
@@ -40,25 +40,26 @@
|
|
|
40
40
|
"./package.json": "./package.json"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@effect/platform-node": "
|
|
44
|
-
"@effect/sql-sqlite-node": "
|
|
45
|
-
"@effected/app": "^0.
|
|
46
|
-
"@effected/cli": "^0.
|
|
47
|
-
"@effected/config-file": "^0.
|
|
48
|
-
"@effected/engine": "^0.
|
|
49
|
-
"@effected/env": "^0.
|
|
50
|
-
"@effected/git": "^0.
|
|
51
|
-
"@effected/glob": "^0.
|
|
52
|
-
"@effected/jsonc": "^0.
|
|
53
|
-
"@effected/
|
|
54
|
-
"@effected/
|
|
55
|
-
"@effected/
|
|
56
|
-
"@effected/
|
|
57
|
-
"@effected/
|
|
58
|
-
"@effected/
|
|
59
|
-
"@effected/
|
|
60
|
-
"@
|
|
61
|
-
"
|
|
43
|
+
"@effect/platform-node": "~4.1.1",
|
|
44
|
+
"@effect/sql-sqlite-node": "~4.1.1",
|
|
45
|
+
"@effected/app": "^0.30.0",
|
|
46
|
+
"@effected/cli": "^0.25.0",
|
|
47
|
+
"@effected/config-file": "^0.23.0",
|
|
48
|
+
"@effected/engine": "^0.11.0",
|
|
49
|
+
"@effected/env": "^0.9.0",
|
|
50
|
+
"@effected/git": "^0.29.0",
|
|
51
|
+
"@effected/glob": "^0.17.0",
|
|
52
|
+
"@effected/jsonc": "^0.22.0",
|
|
53
|
+
"@effected/memfs": "^0.22.0",
|
|
54
|
+
"@effected/package-json": "^0.28.0",
|
|
55
|
+
"@effected/store": "^0.22.0",
|
|
56
|
+
"@effected/toml": "^0.18.0",
|
|
57
|
+
"@effected/walker": "^0.24.0",
|
|
58
|
+
"@effected/workspaces": "^0.41.0",
|
|
59
|
+
"@effected/xdg": "^0.18.0",
|
|
60
|
+
"@effected/yaml": "^0.27.0",
|
|
61
|
+
"@vitest-agent/sdk": "9.0.2",
|
|
62
|
+
"effect": "~4.1.1"
|
|
62
63
|
},
|
|
63
64
|
"engines": {
|
|
64
65
|
"node": ">=24.11.0"
|
package/platform.js
CHANGED
|
@@ -15,8 +15,8 @@ import { Store } from "@effected/store";
|
|
|
15
15
|
// src/platform.ts
|
|
16
16
|
/**
|
|
17
17
|
* The Node platform services every SQLite stack and every Live layer share.
|
|
18
|
-
* `NodeServices.layer` aggregates
|
|
19
|
-
*
|
|
18
|
+
* `NodeServices.layer` aggregates ChildProcessSpawner | FileSystem | Path |
|
|
19
|
+
* Stdio | Terminal; since Effect 4.1 it carries no crypto service.
|
|
20
20
|
* @public
|
|
21
21
|
*/
|
|
22
22
|
const NodePlatformLayer = NodeServices.layer;
|
package/programs/hook-paths.js
CHANGED
|
@@ -4,9 +4,9 @@ import { LEDGER_OPTIONS } from "../stores.js";
|
|
|
4
4
|
import { ProjectIdentityNotResolvableError } from "@vitest-agent/sdk";
|
|
5
5
|
import { Effect, FileSystem, Layer, Option, Path } from "effect";
|
|
6
6
|
import { AppDirs, Xdg } from "@effected/xdg";
|
|
7
|
+
import { LaunchContext } from "@effected/engine";
|
|
7
8
|
import { Store } from "@effected/store";
|
|
8
9
|
import { AppStore } from "@effected/app";
|
|
9
|
-
import { LaunchContext } from "@effected/engine";
|
|
10
10
|
|
|
11
11
|
// src/programs/hook-paths.ts
|
|
12
12
|
/**
|
package/programs/record-turn.js
CHANGED
|
@@ -1,36 +1,31 @@
|
|
|
1
1
|
import { DataStore } from "../services/DataStore.js";
|
|
2
2
|
import { resolveSessionForRecording } from "./resolve-session-for-recording.js";
|
|
3
3
|
import { TurnPayload } from "@vitest-agent/sdk";
|
|
4
|
-
import { Effect, Schema } from "effect";
|
|
4
|
+
import { Effect, Result, Schema } from "effect";
|
|
5
5
|
|
|
6
6
|
// src/programs/record-turn.ts
|
|
7
|
+
const decodeTurnPayload = Schema.decodeUnknownResult(Schema.fromJsonString(TurnPayload));
|
|
7
8
|
/**
|
|
8
9
|
* Parse a raw JSON string and validate it against the `TurnPayload` schema
|
|
9
10
|
* without throwing.
|
|
10
11
|
*
|
|
12
|
+
* Text that is not JSON at all reads `Invalid JSON: …`; JSON whose body is
|
|
13
|
+
* not a `TurnPayload` reads `Invalid TurnPayload: …`.
|
|
14
|
+
*
|
|
11
15
|
* @param raw - the JSON text delivered by the hook
|
|
12
16
|
* @returns the decoded payload, or an error describing why it was rejected
|
|
13
17
|
* @public
|
|
14
18
|
*/
|
|
15
19
|
const parseAndValidateTurnPayload = (raw) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
parsed = JSON.parse(raw);
|
|
19
|
-
} catch (e) {
|
|
20
|
-
return {
|
|
21
|
-
ok: false,
|
|
22
|
-
error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const decoded = Schema.decodeUnknownResult(TurnPayload)(parsed);
|
|
26
|
-
if (decoded._tag === "Failure") return {
|
|
27
|
-
ok: false,
|
|
28
|
-
error: `Invalid TurnPayload: ${decoded.failure.message}`
|
|
29
|
-
};
|
|
30
|
-
return {
|
|
20
|
+
const decoded = decodeTurnPayload(raw);
|
|
21
|
+
if (Result.isSuccess(decoded)) return {
|
|
31
22
|
ok: true,
|
|
32
23
|
payload: decoded.success
|
|
33
24
|
};
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: `${decoded.failure.issue._tag === "Encoding" ? "Invalid JSON" : "Invalid TurnPayload"}: ${decoded.failure.message}`
|
|
28
|
+
};
|
|
34
29
|
};
|
|
35
30
|
/**
|
|
36
31
|
* Validate a turn payload and write it as a `turns` row under the session
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DataReader } from "../services/DataReader.js";
|
|
2
2
|
import { DataStore } from "../services/DataStore.js";
|
|
3
|
-
import { Effect, FileSystem, Option } from "effect";
|
|
3
|
+
import { Effect, FileSystem, Option, Path } from "effect";
|
|
4
4
|
import { LenientManifest } from "@effected/package-json";
|
|
5
5
|
|
|
6
6
|
// src/programs/resolve-session-for-recording.ts
|
|
@@ -17,12 +17,13 @@ const resolveSessionForRecording = (input) => Effect.gen(function* () {
|
|
|
17
17
|
const reader = yield* DataReader;
|
|
18
18
|
const store = yield* DataStore;
|
|
19
19
|
const fs = yield* FileSystem.FileSystem;
|
|
20
|
+
const path = yield* Path.Path;
|
|
20
21
|
const exact = yield* reader.getSessionByChatId(input.chatId);
|
|
21
22
|
if (Option.isSome(exact)) return exact.value;
|
|
22
23
|
const synthetic = yield* reader.findSessionsByChatPrefix(`${input.chatId}-subagent-`);
|
|
23
24
|
if (synthetic.length > 0) return synthetic[0];
|
|
24
25
|
const cwd = input.cwd;
|
|
25
|
-
const project = input.project ?? (yield* readPackageName(fs, cwd)) ?? "unknown";
|
|
26
|
+
const project = input.project ?? (yield* readPackageName(fs, path.join(cwd, "package.json"))) ?? "unknown";
|
|
26
27
|
yield* store.upsertSession({
|
|
27
28
|
chatId: input.chatId,
|
|
28
29
|
project,
|
|
@@ -40,6 +41,6 @@ const resolveSessionForRecording = (input) => Effect.gen(function* () {
|
|
|
40
41
|
* Any read or parse failure resolves to `undefined` — the caller falls
|
|
41
42
|
* back to `"unknown"`, exactly as the former sync `readFileSync` probe did.
|
|
42
43
|
*/
|
|
43
|
-
const readPackageName = (fs,
|
|
44
|
+
const readPackageName = (fs, packageJsonPath) => fs.readFileString(packageJsonPath).pipe(Effect.flatMap(LenientManifest.parse), Effect.map((manifest) => manifest.name), Effect.orElseSucceed(() => void 0));
|
|
44
45
|
|
|
45
46
|
export { resolveSessionForRecording };
|
package/tsdoc-metadata.json
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
import { repositoryUrlOf } from "./package-manifest.js";
|
|
2
2
|
import { gitUrlToProjectKey } from "@vitest-agent/sdk";
|
|
3
|
-
import { Effect, FileSystem, Option, Path, Result } from "effect";
|
|
3
|
+
import { Effect, FileSystem, Layer, Option, Path, Result } from "effect";
|
|
4
|
+
import * as NodePath from "@effect/platform-node/NodePath";
|
|
4
5
|
import { AppDirs } from "@effected/xdg";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
6
|
import { Walker } from "@effected/walker";
|
|
7
7
|
import { LenientManifest } from "@effected/package-json";
|
|
8
|
-
import {
|
|
8
|
+
import { NodeSyncFileSystem } from "@effected/memfs/node-sync";
|
|
9
9
|
|
|
10
10
|
// src/utils/resolve-project-key-from-cwd.ts
|
|
11
11
|
const PACKAGE_JSON = "package.json";
|
|
12
|
-
const findNearestPackageJson = (startDir) => {
|
|
13
|
-
let dir = startDir;
|
|
14
|
-
while (true) {
|
|
15
|
-
const candidate = join(dir, PACKAGE_JSON);
|
|
16
|
-
if (existsSync(candidate)) return candidate;
|
|
17
|
-
const parent = dirname(dir);
|
|
18
|
-
if (parent === dir) return null;
|
|
19
|
-
dir = parent;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
12
|
/**
|
|
23
13
|
* The key a `package.json` body names: the canonical `host__path` form of
|
|
24
14
|
* `repository.url` when present and canonicalizable, else the normalized
|
|
@@ -37,10 +27,11 @@ const keyFromPackageJson = (content) => {
|
|
|
37
27
|
if (manifest.name !== void 0) return Result.getOrNull(AppDirs.segment(manifest.name));
|
|
38
28
|
return null;
|
|
39
29
|
};
|
|
40
|
-
const basenameKey = (cwd) => {
|
|
41
|
-
const basename =
|
|
42
|
-
return Result.getOrElse(AppDirs.segment(basename), () => "anonymous-project");
|
|
30
|
+
const basenameKey = (path, cwd) => {
|
|
31
|
+
const basename = path.basename(cwd);
|
|
32
|
+
return basename.length === 0 ? "anonymous-project" : Result.getOrElse(AppDirs.segment(basename), () => "anonymous-project");
|
|
43
33
|
};
|
|
34
|
+
const NodeSyncPlatform = Layer.merge(NodeSyncFileSystem.layer, NodePath.layer);
|
|
44
35
|
/**
|
|
45
36
|
* Compute the project key for a workspace by reading `package.json`
|
|
46
37
|
* fields directly. Returns the canonical `host__path` form when a git
|
|
@@ -50,26 +41,17 @@ const basenameKey = (cwd) => {
|
|
|
50
41
|
* (or `"anonymous-project"`) so callers don't have to handle the
|
|
51
42
|
* empty case.
|
|
52
43
|
*
|
|
53
|
-
* Synchronous and reads the real disk through `node:fs
|
|
54
|
-
*
|
|
55
|
-
*
|
|
44
|
+
* Synchronous and reads the real disk through `node:fs`: it runs
|
|
45
|
+
* {@link resolveProjectKeyFromCwdEffect} under `Effect.runSync` over a
|
|
46
|
+
* read-only synchronous `FileSystem` and the host's Node `Path`. Effect
|
|
47
|
+
* callers that already hold a `FileSystem` and `Path` use the Effect form
|
|
48
|
+
* directly.
|
|
56
49
|
* @public
|
|
57
50
|
*/
|
|
58
|
-
const resolveProjectKeyFromCwd = (cwd) =>
|
|
59
|
-
const pkgPath = findNearestPackageJson(cwd);
|
|
60
|
-
if (pkgPath !== null) {
|
|
61
|
-
let content = null;
|
|
62
|
-
try {
|
|
63
|
-
content = readFileSync(pkgPath, "utf-8");
|
|
64
|
-
} catch {}
|
|
65
|
-
const key = content === null ? null : keyFromPackageJson(content);
|
|
66
|
-
if (key !== null) return key;
|
|
67
|
-
}
|
|
68
|
-
return basenameKey(cwd);
|
|
69
|
-
};
|
|
51
|
+
const resolveProjectKeyFromCwd = (cwd) => Effect.runSync(resolveProjectKeyFromCwdEffect(cwd).pipe(Effect.provide(NodeSyncPlatform)));
|
|
70
52
|
/**
|
|
71
53
|
* {@link resolveProjectKeyFromCwd} read through the ambient
|
|
72
|
-
* `FileSystem`
|
|
54
|
+
* `FileSystem` and `Path` services instead of `node:fs` — same upward walk to the
|
|
73
55
|
* nearest `package.json` (`@effected/walker`'s `Walker.ascend` +
|
|
74
56
|
* `Walker.findUpward`), same precedence, same basename fallback.
|
|
75
57
|
*
|
|
@@ -82,11 +64,12 @@ const resolveProjectKeyFromCwd = (cwd) => {
|
|
|
82
64
|
*/
|
|
83
65
|
const resolveProjectKeyFromCwdEffect = (cwd) => Effect.gen(function* () {
|
|
84
66
|
const fs = yield* FileSystem.FileSystem;
|
|
67
|
+
const path = yield* Path.Path;
|
|
85
68
|
const dirs = yield* Walker.ascend(cwd);
|
|
86
|
-
const found = yield* Walker.findUpward(dirs, (dir) => [join(dir, PACKAGE_JSON)]);
|
|
87
|
-
if (Option.isNone(found)) return basenameKey(cwd);
|
|
69
|
+
const found = yield* Walker.findUpward(dirs, (dir) => [path.join(dir, PACKAGE_JSON)]);
|
|
70
|
+
if (Option.isNone(found)) return basenameKey(path, cwd);
|
|
88
71
|
const content = yield* fs.readFileString(found.value).pipe(Effect.orElseSucceed(() => null));
|
|
89
|
-
return (content === null ? null : keyFromPackageJson(content)) ?? basenameKey(cwd);
|
|
90
|
-
})
|
|
72
|
+
return (content === null ? null : keyFromPackageJson(content)) ?? basenameKey(path, cwd);
|
|
73
|
+
});
|
|
91
74
|
|
|
92
75
|
export { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect };
|
package/version.js
CHANGED