Repository navigation
Expand file tree
/
Copy pathSubagentWorktreeSetup.ts
More file actions
90 lines (81 loc) · 3.2 KB
/
Copy pathSubagentWorktreeSetup.ts
File metadata and controls
90 lines (81 loc) · 3.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import type { ThreadId } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Ref from "effect/Ref";
import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
import * as ProjectSetupScriptRunner from "./ProjectSetupScriptRunner.ts";
export interface SubagentWorktreeSetupInput {
/** The T3 thread whose agent started the subagent. */
readonly threadId: ThreadId;
/** Provider id of the subagent, used to name its setup terminal. */
readonly agentId: string;
readonly worktreePath: string;
}
type Prepare = (input: SubagentWorktreeSetupInput) => Effect.Effect<void>;
/**
* Prepares a worktree a provider created for one of its subagents the way T3
* prepares a thread's own worktree: files to copy, then the setup script with
* the project's environment variables. `prepare` returns once a blocking
* (non-async) script exits, so the subagent starts in a ready worktree.
*
* Providers are built below the project layers, so the server installs the
* implementation with `layerInstall` once those exist. Until then, and in
* tests that skip it, `prepare` does nothing.
*/
export class SubagentWorktreeSetup extends Context.Service<
SubagentWorktreeSetup,
{
readonly prepare: Prepare;
readonly install: (prepare: Prepare) => Effect.Effect<void>;
}
>()("t3/project/SubagentWorktreeSetup") {}
export const layer = Layer.effect(
SubagentWorktreeSetup,
Effect.gen(function* () {
const current = yield* Ref.make<Prepare>(() => Effect.void);
return SubagentWorktreeSetup.of({
prepare: (input) => Ref.get(current).pipe(Effect.flatMap((prepare) => prepare(input))),
install: (prepare) => Ref.set(current, prepare),
});
}),
);
export const layerInstall = Layer.effectDiscard(
Effect.gen(function* () {
const setup = yield* SubagentWorktreeSetup;
const projections = yield* ProjectionStore.ProjectionStoreV2;
const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
const prepare = Effect.fn("SubagentWorktreeSetup.prepare")(function* (
input: SubagentWorktreeSetupInput,
) {
const thread = yield* projections.getThreadShell(input.threadId);
if (thread === null) return;
const result = yield* runner.runForThread({
threadId: input.threadId,
projectId: thread.projectId,
worktreePath: input.worktreePath,
preferredTerminalId: `subagent-setup-${input.agentId}`,
observeCompletion: {},
});
if (result.status !== "started" || !result.completion) return;
// Same rule as a thread's own worktree: only a non-async script holds the agent.
if (result.async) {
yield* Effect.forkDetach(result.completion);
} else {
yield* result.completion;
}
});
yield* setup.install((input) =>
prepare(input).pipe(
// Setup is best effort: a failed install must not stop the subagent.
Effect.catchCause((cause) =>
Effect.logWarning("subagent worktree setup failed", {
threadId: input.threadId,
worktreePath: input.worktreePath,
cause,
}),
),
),
);
}),
);