Skip to content
Source
Sign inSign up

AgentScope.ts

121 lines4.0 KB

1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3​
4export interface AgentScopeShape {
5 readonly wrap: (input: {
6 readonly command: string;
7 readonly args: ReadonlyArray<string>;
8 readonly name: string;
9 readonly threadId?: string | undefined;
10 readonly env?: NodeJS.ProcessEnv | undefined;
11 /** Terminals may survive restarts; agents must remain eligible for orphan cleanup. */
12 readonly survivesRestart?: boolean | undefined;
13 /**
14 * Keep a shared process running after a tool OOM when supported.
15 * Falls back to ordinary scope behavior on older systemd.
16 */
17 readonly shared?: boolean | undefined;
18 }) => Effect.Effect<AgentScopeCommand>;
19 readonly oomKilled: (threadId: string) => Effect.Effect<boolean>;
20 /**
21 * Reaps processes left after the owning agent exits, including setsid
22 * descendants outside its process group. Preserves systemd OOM diagnosis.
23 */
24 readonly stop: (unit: string) => Effect.Effect<void>;
25 /**
26 * SIGKILLs every process in the thread's own scope, which works even while
27 * the kernel throttles them. Shared scopes serve other threads and are never
28 * killed here. Succeeds with whether a scope was killed.
29 */
30 readonly kill: (threadId: string) => Effect.Effect<boolean>;
31 /**
32 * Forgets the thread's scope. The session manager calls it before it opens
33 * a provider session, so a scope only answers for the session that launched
34 * it, even when the next session runs without one (Cursor, OpenCode).
35 */
36 readonly clear: (threadId: string) => Effect.Effect<void>;
38​
39export interface AgentScopeCommand {
40 readonly command: string;
41 readonly args: ReadonlyArray<string>;
42 /** The systemd scope the command runs in, for `stop`. */
43 readonly unit?: string | undefined;
45​
46export const AgentScope = Context.Reference<AgentScopeShape>("@t3tools/shared/AgentScope", {
47 defaultValue: () => ({
48 wrap: ({ command, args }) => Effect.succeed({ command, args }),
49 oomKilled: () => Effect.succeed(false),
50 stop: () => Effect.void,
51 kill: () => Effect.succeed(false),
52 clear: () => Effect.void,
53 }),
54});
55​
56export const AgentScopeThreadId = Context.Reference<string | undefined>(
57 "@t3tools/shared/AgentScope/ThreadId",
58 { defaultValue: () => undefined },
59);
60​
61export const AGENT_OOM_SCORE_ADJ = 800;
62​
63export const RAISE_OOM_SCORE_LINE = `{ echo ${AGENT_OOM_SCORE_ADJ} >/proc/self/oom_score_adj; } 2>/dev/null`;
64​
65const LAUNCH_SCRIPT = [RAISE_OOM_SCORE_LINE, 'exec "$@"'].join("\n");
66​
67// systemd-run needs XDG_RUNTIME_DIR to reach this user's manager.
68const SCOPED_LAUNCH_SCRIPT = [
69 RAISE_OOM_SCORE_LINE,
70 ': "${XDG_RUNTIME_DIR:=$1}"',
71 "export XDG_RUNTIME_DIR",
72 "shift",
73 'exec "$@"',
74].join("\n");
75​
76export interface AgentSystemdScope {
77 readonly systemdRun: string;
78 readonly slice: string;
79 readonly unit: string;
80 readonly runtimeDir: string;
81 readonly options: ReadonlyArray<string>;
83​
84const quoteShell = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`;
85​
86/** An executable file for launchers that take a path, not a command line, such as Playwright. */
87export function agentScopeLauncherScript(launch: AgentScopeCommand): string {
88 const command = [launch.command, ...launch.args].map(quoteShell).join(" ");
89 return `#!/bin/sh\nexec ${command} "$@"\n`;
91​
92/** Exec at every step preserves the agent PID, stdio, and signals; systemd-run places it and descendants in the scope. */
93export function agentScopeCommand(input: {
94 readonly command: string;
95 readonly args: ReadonlyArray<string>;
96 readonly scope?: AgentSystemdScope | undefined;
97}): AgentScopeCommand {
98 const { command, args, scope } = input;
99 if (scope === undefined) {
100 return { command: "/bin/sh", args: ["-c", LAUNCH_SCRIPT, "t3-agent", command, ...args] };
101 }
102 return {
103 command: "/bin/sh",
104 args: [
105 "-c",
106 SCOPED_LAUNCH_SCRIPT,
107 "t3-agent",
108 scope.runtimeDir,
109 scope.systemdRun,
110 "--user",
111 "--scope",
112 "--quiet",
113 `--slice=${scope.slice}`,
114 `--unit=${scope.unit}`,
115 ...scope.options,
116 "--",
117 command,
118 ...args,
119 ],
120 };