@usine/agents 0.3.0 → 0.5.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/LICENSE +661 -21
- package/README.md +100 -30
- package/dist/child-host/child-host.d.ts +90 -0
- package/dist/child-host/child-host.d.ts.map +1 -0
- package/dist/child-host/child-host.js +119 -0
- package/dist/child-host/child-host.js.map +1 -0
- package/dist/child-host/command.d.ts +22 -0
- package/dist/child-host/command.d.ts.map +1 -0
- package/dist/child-host/command.js +43 -0
- package/dist/child-host/command.js.map +1 -0
- package/dist/openai-prices.d.ts +8 -0
- package/dist/openai-prices.d.ts.map +1 -0
- package/dist/openai-prices.js +28 -0
- package/dist/openai-prices.js.map +1 -0
- package/dist/pi/authentication.d.ts +11 -2
- package/dist/pi/authentication.d.ts.map +1 -1
- package/dist/pi/authentication.js +15 -2
- package/dist/pi/authentication.js.map +1 -1
- package/dist/pi/children.d.ts +1 -4
- package/dist/pi/children.d.ts.map +1 -1
- package/dist/pi/children.js +1 -4
- package/dist/pi/children.js.map +1 -1
- package/dist/pi/claude/claude.d.ts +10 -0
- package/dist/pi/claude/claude.d.ts.map +1 -0
- package/dist/pi/claude/claude.js +610 -0
- package/dist/pi/claude/claude.js.map +1 -0
- package/dist/pi/claude/codec.d.ts +67 -0
- package/dist/pi/claude/codec.d.ts.map +1 -0
- package/dist/pi/claude/codec.js +140 -0
- package/dist/pi/claude/codec.js.map +1 -0
- package/dist/pi/{claude.d.ts → claude/sdk.d.ts} +11 -10
- package/dist/pi/claude/sdk.d.ts.map +1 -0
- package/dist/pi/claude/sdk.js +63 -0
- package/dist/pi/claude/sdk.js.map +1 -0
- package/dist/pi/codex/account.d.ts +11 -5
- package/dist/pi/codex/account.d.ts.map +1 -1
- package/dist/pi/codex/account.js +23 -6
- package/dist/pi/codex/account.js.map +1 -1
- package/dist/pi/codex/app-server.d.ts +19 -7
- package/dist/pi/codex/app-server.d.ts.map +1 -1
- package/dist/pi/codex/app-server.js +41 -36
- package/dist/pi/codex/app-server.js.map +1 -1
- package/dist/pi/codex/codec.d.ts +10 -84
- package/dist/pi/codex/codec.d.ts.map +1 -1
- package/dist/pi/codex/codec.js +18 -80
- package/dist/pi/codex/codec.js.map +1 -1
- package/dist/pi/codex/codex.d.ts +0 -1
- package/dist/pi/codex/codex.d.ts.map +1 -1
- package/dist/pi/codex/codex.js +89 -18
- package/dist/pi/codex/codex.js.map +1 -1
- package/dist/pi/conversation.d.ts +33 -12
- package/dist/pi/conversation.d.ts.map +1 -1
- package/dist/pi/conversation.js +58 -18
- package/dist/pi/conversation.js.map +1 -1
- package/dist/pi/pi.d.ts +1 -4
- package/dist/pi/pi.d.ts.map +1 -1
- package/dist/pi/pi.js +29 -175
- package/dist/pi/pi.js.map +1 -1
- package/dist/pi/sdk.d.ts +18 -15
- package/dist/pi/sdk.d.ts.map +1 -1
- package/dist/pi/sdk.js +45 -55
- package/dist/pi/sdk.js.map +1 -1
- package/dist/pi/session.d.ts +13 -14
- package/dist/pi/session.d.ts.map +1 -1
- package/dist/pi/session.js +63 -24
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/turns.d.ts +7 -7
- package/dist/pi/turns.d.ts.map +1 -1
- package/dist/pi/turns.js +22 -9
- package/dist/pi/turns.js.map +1 -1
- package/dist/pi/usage.d.ts +36 -22
- package/dist/pi/usage.d.ts.map +1 -1
- package/dist/pi/usage.js +79 -33
- package/dist/pi/usage.js.map +1 -1
- package/dist/preflight/command.d.ts +16 -4
- package/dist/preflight/command.d.ts.map +1 -1
- package/dist/preflight/command.js +29 -27
- package/dist/preflight/command.js.map +1 -1
- package/dist/preflight/preflight.d.ts +19 -7
- package/dist/preflight/preflight.d.ts.map +1 -1
- package/dist/preflight/preflight.js +73 -32
- package/dist/preflight/preflight.js.map +1 -1
- package/dist/prompt.d.ts +12 -9
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +33 -4
- package/dist/prompt.js.map +1 -1
- package/dist/protocol/codex.d.ts +122 -7
- package/dist/protocol/codex.d.ts.map +1 -1
- package/dist/protocol/codex.js +109 -1
- package/dist/protocol/codex.js.map +1 -1
- package/dist/protocol/count.d.ts +4 -0
- package/dist/protocol/count.d.ts.map +1 -0
- package/dist/protocol/count.js +4 -0
- package/dist/protocol/count.js.map +1 -0
- package/dist/runtime.d.ts +110 -55
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +114 -27
- package/dist/runtime.js.map +1 -1
- package/package.json +35 -19
- package/dist/pi/claude.d.ts.map +0 -1
- package/dist/pi/claude.js +0 -302
- package/dist/pi/claude.js.map +0 -1
- package/dist/pi/live.d.ts +0 -4
- package/dist/pi/live.d.ts.map +0 -1
- package/dist/pi/live.js +0 -11
- package/dist/pi/live.js.map +0 -1
- package/dist/pi/subagents.d.ts +0 -29
- package/dist/pi/subagents.d.ts.map +0 -1
- package/dist/pi/subagents.js +0 -120
- package/dist/pi/subagents.js.map +0 -1
- package/dist/pi/thinking.d.ts +0 -8
- package/dist/pi/thinking.d.ts.map +0 -1
- package/dist/pi/thinking.js +0 -15
- package/dist/pi/thinking.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"children.d.ts","sourceRoot":"","sources":["../../src/pi/children.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAyBpG
|
|
1
|
+
{"version":3,"file":"children.d.ts","sourceRoot":"","sources":["../../src/pi/children.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAyBpG,mJAAmJ;AACnJ,eAAO,MAAM,oBAAoB,2FAAkD,CAAC"}
|
package/dist/pi/children.js
CHANGED
|
@@ -21,9 +21,6 @@ const makeChildDispatcher = Effect.gen(function* () {
|
|
|
21
21
|
}),
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
|
-
/**
|
|
25
|
-
* Selects Claude Code, Codex, or Pi for each delegated task from the Agent's harness.
|
|
26
|
-
* Supply the three backend layers here, or use a concrete backend directly.
|
|
27
|
-
*/
|
|
24
|
+
/** Dispatches each delegated task to the Claude Code, Codex, or Pi backend according to the Agent's harness; requires all three backend layers. */
|
|
28
25
|
export const childDispatcherLayer = Layer.effect(ChildBackend, makeChildDispatcher);
|
|
29
26
|
//# sourceMappingURL=children.js.map
|
package/dist/pi/children.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"children.js","sourceRoot":"","sources":["../../src/pi/children.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpG,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,kBAAkB,CAAC;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,iBAAiB,CAAC;IACvC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;IAEjC,OAAO,YAAY,CAAC,EAAE,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,qBAAqB,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAO;YACzD,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,2BAA2B,EAAE;gBAClD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO;gBACtC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;aAC9B,CAAC,CAAC;YACH,QAAQ,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACtC,KAAK,QAAQ;oBACX,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACvC,KAAK,OAAO;oBACV,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACtC,KAAK,IAAI;oBACP,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACrC,CAAC;QACH,CAAC,CAAC;KACH,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH
|
|
1
|
+
{"version":3,"file":"children.js","sourceRoot":"","sources":["../../src/pi/children.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpG,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,kBAAkB,CAAC;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,iBAAiB,CAAC;IACvC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;IAEjC,OAAO,YAAY,CAAC,EAAE,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,qBAAqB,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAO;YACzD,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,2BAA2B,EAAE;gBAClD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO;gBACtC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;aAC9B,CAAC,CAAC;YACH,QAAQ,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACtC,KAAK,QAAQ;oBACX,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACvC,KAAK,OAAO;oBACV,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACtC,KAAK,IAAI;oBACP,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACrC,CAAC;QACH,CAAC,CAAC;KACH,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,mJAAmJ;AACnJ,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC,MAAM,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
3
|
+
import { ClaudeChildBackend, ClaudeLoginLock, ChildBackend } from "../../runtime.ts";
|
|
4
|
+
import { ClaudeSdk } from "./sdk.ts";
|
|
5
|
+
export { ClaudeSdk, claudeSdkLive, type ClaudeModelInfo, type ClaudeQuery, type ClaudeQueryOptions, type ClaudeUserMessage, } from "./sdk.ts";
|
|
6
|
+
/** Provides `ChildBackend` with Claude Code for every delegated task. */
|
|
7
|
+
export declare const claudeChildLayer: Layer.Layer<ChildBackend, never, ChildProcessSpawner.ChildProcessSpawner | ClaudeLoginLock | ClaudeSdk>;
|
|
8
|
+
/** Provides `ClaudeChildBackend` for direct use or harness-based dispatch. */
|
|
9
|
+
export declare const claudeChildBackendLayer: Layer.Layer<ClaudeChildBackend, never, ChildProcessSpawner.ChildProcessSpawner | ClaudeLoginLock | ClaudeSdk>;
|
|
10
|
+
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../src/pi/claude/claude.ts"],"names":[],"mappings":"AAiBA,OAAO,EAOL,KAAK,EAQN,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAEL,kBAAkB,EAClB,eAAe,EACf,YAAY,EAKb,MAAM,kBAAkB,CAAC;AAyB1B,OAAO,EAEL,SAAS,EAKV,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,SAAS,EACT,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAC;AAyyBlB,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,yGAA8C,CAAC;AAC5E,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,+GAAoD,CAAC"}
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import {} from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { ModelId } from "@usine/core/agent/brain";
|
|
4
|
+
import { HarnessSession, HarnessSessionId, ProviderRecovered, ProviderRetries, ProviderRetrying, SessionStarted, SessionTokensUpdated, ToolFinished, } from "@usine/core/agent/session";
|
|
5
|
+
import { Deferred, Duration, Effect, Exit, Fiber, Layer, Option, Queue, Ref, Result, Schema, Scope, Stream, } from "effect";
|
|
6
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
7
|
+
import { AgentResultError, ClaudeChildBackend, ClaudeLoginLock, ChildBackend, SessionOperationError, UnknownModelError, } from "../../runtime.js";
|
|
8
|
+
import { CHILD_STOP_SCRIPT, childPidFile, SSH_FAILURE, sshArguments, } from "../../child-host/child-host.js";
|
|
9
|
+
import { hostOutput } from "../../child-host/command.js";
|
|
10
|
+
import { sessionChannel } from "../session.js";
|
|
11
|
+
import { sessionConversation } from "../conversation.js";
|
|
12
|
+
import { claudeSessionMeter, claudeTokenMeter } from "../usage.js";
|
|
13
|
+
import { apiRetryMessage, ClaudeApiRetry, ClaudeCompletionHeader, ClaudeResultMessage, decodeApiRetry, decodeClaudeInit, decodeCompletionUpdate, isAssistantMessage, loginCheck, sessionEventsFrom, } from "./codec.js";
|
|
14
|
+
import { AsyncInput, ClaudeSdk, userMessage, } from "./sdk.js";
|
|
15
|
+
export { ClaudeSdk, claudeSdkLive, } from "./sdk.js";
|
|
16
|
+
const operationError = (operation, request, message, cause) => new SessionOperationError({
|
|
17
|
+
operation,
|
|
18
|
+
sessionId: Option.some(request.session.id),
|
|
19
|
+
message,
|
|
20
|
+
cause,
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Returns a spawner that runs Claude Code on the remote host as one SSH command, with the SDK's
|
|
24
|
+
* command, arguments, working directory, and environment in the remote command line. The abort
|
|
25
|
+
* signal stops the SSH client; `onExit` gets its exit code, or `null` if a signal stopped it.
|
|
26
|
+
*/
|
|
27
|
+
const sshSpawner = (host, pidFile, onStderr, onExit) => (options) => {
|
|
28
|
+
const child = spawn(host.ssh.command, sshArguments(host, {
|
|
29
|
+
cwd: options.cwd,
|
|
30
|
+
environment: options.env,
|
|
31
|
+
pidFile,
|
|
32
|
+
command: options.command,
|
|
33
|
+
args: options.args,
|
|
34
|
+
}), {
|
|
35
|
+
env: { ...host.sshEnvironment },
|
|
36
|
+
signal: options.signal,
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
});
|
|
40
|
+
child.stderr.setEncoding("utf8").on("data", onStderr);
|
|
41
|
+
child.once("exit", (code) => onExit(code));
|
|
42
|
+
return child;
|
|
43
|
+
};
|
|
44
|
+
/** Returns the options of every launch for `request`; each launch adds its remote spawner. */
|
|
45
|
+
const optionsFor = (request) => {
|
|
46
|
+
const effort = request.session.brain.effort;
|
|
47
|
+
const shared = {
|
|
48
|
+
allowDangerouslySkipPermissions: true,
|
|
49
|
+
cwd: request.cwd,
|
|
50
|
+
// Replaces the subprocess environment instead of extending this process's.
|
|
51
|
+
env: { ...request.environment },
|
|
52
|
+
model: request.session.brain.model,
|
|
53
|
+
// The Worker-provisioned binary that the readiness checks inspect.
|
|
54
|
+
pathToClaudeCodeExecutable: "claude",
|
|
55
|
+
permissionMode: "bypassPermissions",
|
|
56
|
+
persistSession: true,
|
|
57
|
+
// The worktree is untrusted, so its project and local settings (hooks, env) are not loaded into
|
|
58
|
+
// a child that has the App credential; only the Worker user's settings apply.
|
|
59
|
+
settingSources: ["user"],
|
|
60
|
+
};
|
|
61
|
+
return effort === "off"
|
|
62
|
+
? { ...shared, thinking: { type: "disabled" } }
|
|
63
|
+
: {
|
|
64
|
+
...shared,
|
|
65
|
+
effort: effort === "minimal" ? "low" : effort,
|
|
66
|
+
thinking: { type: "adaptive" },
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const modelExists = (models, model) => models.some((candidate) => candidate.value === model || candidate.resolvedModel === model);
|
|
70
|
+
/**
|
|
71
|
+
* The delays before each relaunch after a Claude process loses the login refresh lock; Claude's
|
|
72
|
+
* error says to retry in a minute. If the last launch fails the same way, the parent gets the error
|
|
73
|
+
* (#453).
|
|
74
|
+
*/
|
|
75
|
+
const LOGIN_RETRY_DELAYS = [
|
|
76
|
+
Duration.seconds(15),
|
|
77
|
+
Duration.seconds(30),
|
|
78
|
+
Duration.seconds(60),
|
|
79
|
+
];
|
|
80
|
+
/**
|
|
81
|
+
* The maximum time a launch keeps its Claude login lock while waiting for a message that decides
|
|
82
|
+
* the login state. If no such message arrives, the launch continues as a started session.
|
|
83
|
+
*/
|
|
84
|
+
const LOGIN_WINDOW = Duration.seconds(60);
|
|
85
|
+
/**
|
|
86
|
+
* The delays before each try to resume a remote child whose SSH connection broke, such as when the
|
|
87
|
+
* orchestrator's machine slept. A message from Claude resets the count. When the delays run out,
|
|
88
|
+
* the child fails with the broken connection's error (#603).
|
|
89
|
+
*/
|
|
90
|
+
const RESUME_DELAYS = [
|
|
91
|
+
Duration.seconds(5),
|
|
92
|
+
Duration.seconds(15),
|
|
93
|
+
Duration.seconds(30),
|
|
94
|
+
Duration.minutes(1),
|
|
95
|
+
Duration.minutes(2),
|
|
96
|
+
Duration.minutes(5),
|
|
97
|
+
];
|
|
98
|
+
/** The time a broken launch waits for its SSH client's exit code before it gives up resuming. */
|
|
99
|
+
const SSH_EXIT_WAIT = Duration.seconds(5);
|
|
100
|
+
/** The time limit for stopping a broken launch's Claude process on the Worker. */
|
|
101
|
+
const STOP_TIMEOUT = Duration.seconds(30);
|
|
102
|
+
/** The message that starts the next turn of a resumed child that was working. */
|
|
103
|
+
const RESUME_PROMPT = "usine lost its connection to this session and resumed it. Your last step may not have finished: check the worktree and any command you started before you go on. Then continue the task and end with your final answer. If the task is done, give your final answer again.";
|
|
104
|
+
const makeClaudeChild = Effect.gen(function* () {
|
|
105
|
+
const sdk = yield* ClaudeSdk;
|
|
106
|
+
const logins = yield* ClaudeLoginLock;
|
|
107
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
108
|
+
/**
|
|
109
|
+
* Stops the Claude process of `request` on the remote `host` through its PID file, so that a
|
|
110
|
+
* resumed launch is the only process in the session. Fails with `SessionOperationError` if the
|
|
111
|
+
* Worker can't be reached or the stop fails.
|
|
112
|
+
*/
|
|
113
|
+
const stopRemote = Effect.fn("ClaudeChildBackend.stopRemote")(function* (request, host) {
|
|
114
|
+
yield* Effect.annotateCurrentSpan({ sessionId: request.session.id, worker: request.worker });
|
|
115
|
+
const failed = (message, cause) => operationError("launch", request, message, cause);
|
|
116
|
+
const { stdout, exitCode } = yield* hostOutput(host, {
|
|
117
|
+
environment: {},
|
|
118
|
+
command: "sh",
|
|
119
|
+
args: ["-c", CHILD_STOP_SCRIPT, "sh", childPidFile(request.cwd, request.session.id)],
|
|
120
|
+
}).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.timeout(STOP_TIMEOUT), Effect.mapError((cause) => failed(`Can't stop the Claude child on ${host.destination}`, cause)));
|
|
121
|
+
if (exitCode !== 0)
|
|
122
|
+
return yield* failed(exitCode === SSH_FAILURE
|
|
123
|
+
? `Can't reach ${host.destination} to stop the Claude child`
|
|
124
|
+
: `Stopping the Claude child on ${host.destination} exited with code ${exitCode}`, new Error(`exit code ${exitCode}`));
|
|
125
|
+
yield* Effect.logInfo("Stopped what was left of the Claude child on its Worker", {
|
|
126
|
+
sessionId: request.session.id,
|
|
127
|
+
worker: request.worker,
|
|
128
|
+
outcome: stdout,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* Acquires the request's Claude login lock in a fiber of the current scope and returns an effect
|
|
133
|
+
* that releases it. The lock is also released after `LOGIN_WINDOW`, so a hung launch does not
|
|
134
|
+
* block other Claude launches and preflights on that login.
|
|
135
|
+
*/
|
|
136
|
+
const holdLogin = Effect.fn("ClaudeChildBackend.holdLogin")(function* (request) {
|
|
137
|
+
const fields = {
|
|
138
|
+
sessionId: request.session.id,
|
|
139
|
+
worker: request.worker,
|
|
140
|
+
...Option.match(request.machine, { onNone: () => ({}), onSome: (machine) => ({ machine }) }),
|
|
141
|
+
};
|
|
142
|
+
yield* Effect.annotateCurrentSpan(fields);
|
|
143
|
+
const held = yield* Deferred.make();
|
|
144
|
+
const released = yield* Deferred.make();
|
|
145
|
+
yield* Effect.forkScoped(logins.withLock(request.worker, request.machine)(Deferred.succeed(held, undefined).pipe(Effect.andThen(Deferred.await(released)), Effect.timeoutOption(LOGIN_WINDOW), Effect.flatMap(Option.match({
|
|
146
|
+
onSome: () => Effect.void,
|
|
147
|
+
onNone: () => Effect.logWarning("Claude child showed no sign of its login in time; releasing its Claude login lock", fields),
|
|
148
|
+
})))));
|
|
149
|
+
yield* Deferred.await(held);
|
|
150
|
+
yield* Effect.logDebug("Holding the Claude login lock to launch the child", fields);
|
|
151
|
+
return Deferred.succeed(released, undefined).pipe(Effect.asVoid);
|
|
152
|
+
});
|
|
153
|
+
/** Reads messages from `output` into `read` until one decides the login state. */
|
|
154
|
+
const checkLogin = Effect.fn("ClaudeChildBackend.checkLogin")(function* (output, read) {
|
|
155
|
+
const tokens = claudeTokenMeter();
|
|
156
|
+
let spentTokens = false;
|
|
157
|
+
for (;;) {
|
|
158
|
+
const next = yield* Effect.result(Queue.take(output));
|
|
159
|
+
// The session reports a closed or failed output itself.
|
|
160
|
+
if (Result.isFailure(next))
|
|
161
|
+
return "undecided";
|
|
162
|
+
read.push(next.success);
|
|
163
|
+
spentTokens ||= Option.exists(tokens(next.success), ({ totalTokens }) => totalTokens > 0);
|
|
164
|
+
const check = loginCheck(next.success, spentTokens);
|
|
165
|
+
if (check !== "waiting")
|
|
166
|
+
return check;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Launches Claude Code once for `request` and sends `messages`, keeping its Claude login lock
|
|
171
|
+
* until the first messages decide the login state. With no messages, the launch waits for input
|
|
172
|
+
* and skips the login check. Resources are added to the current scope.
|
|
173
|
+
*/
|
|
174
|
+
const launchOnce = Effect.fn("ClaudeChildBackend.launchOnce")(function* (request, options, messages) {
|
|
175
|
+
yield* Effect.annotateCurrentSpan({ sessionId: request.session.id, worker: request.worker });
|
|
176
|
+
const context = yield* Effect.context();
|
|
177
|
+
const release = yield* holdLogin(request);
|
|
178
|
+
return yield* Effect.gen(function* () {
|
|
179
|
+
const input = new AsyncInput();
|
|
180
|
+
const sshExit = yield* Deferred.make();
|
|
181
|
+
const query = sdk.open(input, request.host._tag === "RemoteShell"
|
|
182
|
+
? {
|
|
183
|
+
...options,
|
|
184
|
+
spawnClaudeCodeProcess: sshSpawner(request.host, childPidFile(request.cwd, request.session.id), (text) => {
|
|
185
|
+
Effect.runForkWith(context)(Effect.logDebug("Claude child stderr", {
|
|
186
|
+
sessionId: request.session.id,
|
|
187
|
+
text: text.trimEnd(),
|
|
188
|
+
}));
|
|
189
|
+
}, (code) => {
|
|
190
|
+
Deferred.doneUnsafe(sshExit, Exit.succeed(code));
|
|
191
|
+
}),
|
|
192
|
+
}
|
|
193
|
+
: // On a local host, the SDK spawns the process itself.
|
|
194
|
+
options);
|
|
195
|
+
const output = yield* Queue.make();
|
|
196
|
+
yield* Effect.forkScoped(Stream.fromAsyncIterable(query, (cause) => operationError("stream", request, "Claude output stream failed", cause)).pipe(Stream.runIntoQueue(output)));
|
|
197
|
+
// Finalizers run newest first, so this closes the query before the scope stops the output
|
|
198
|
+
// reader. Stopping the reader waits for its pending read, and when Claude keeps a session
|
|
199
|
+
// open after a failed turn, only closing the query ends that read.
|
|
200
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
201
|
+
input.end();
|
|
202
|
+
query.close();
|
|
203
|
+
}));
|
|
204
|
+
const models = yield* Effect.tryPromise({
|
|
205
|
+
try: () => query.supportedModels(),
|
|
206
|
+
catch: (cause) => operationError("launch", request, "Claude model discovery failed", cause),
|
|
207
|
+
});
|
|
208
|
+
if (!modelExists(models, request.session.brain.model)) {
|
|
209
|
+
yield* Effect.logError("Claude child rejected an unknown model");
|
|
210
|
+
return yield* new UnknownModelError({
|
|
211
|
+
harness: "claude",
|
|
212
|
+
model: request.session.brain.model,
|
|
213
|
+
message: `Claude has no model "${request.session.brain.model}"`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
for (const message of messages)
|
|
217
|
+
input.offer(userMessage(message));
|
|
218
|
+
const read = [];
|
|
219
|
+
const login = messages.length === 0
|
|
220
|
+
? "undecided"
|
|
221
|
+
: Option.getOrElse(yield* checkLogin(output, read).pipe(Effect.timeoutOption(LOGIN_WINDOW)), () => "undecided");
|
|
222
|
+
yield* Effect.logDebug("Claude child's first messages settled its login check", {
|
|
223
|
+
sessionId: request.session.id,
|
|
224
|
+
worker: request.worker,
|
|
225
|
+
login,
|
|
226
|
+
messages: read.length,
|
|
227
|
+
});
|
|
228
|
+
const lostConnection = request.host._tag === "RemoteShell"
|
|
229
|
+
? Deferred.await(sshExit).pipe(Effect.timeoutOption(SSH_EXIT_WAIT), Effect.map((code) => Option.contains(code, SSH_FAILURE)))
|
|
230
|
+
: Effect.succeed(false);
|
|
231
|
+
return { input, query, read, output, login, lostConnection };
|
|
232
|
+
}).pipe(Effect.ensuring(release));
|
|
233
|
+
});
|
|
234
|
+
/**
|
|
235
|
+
* Launches Claude Code for `request`, relaunching after each delay in `LOGIN_RETRY_DELAYS` while it
|
|
236
|
+
* fails on the login refresh lock. Each launch has its own scope inside `scope`, closed before the
|
|
237
|
+
* next or when the launch fails, so a failed launch's process and messages do not reach the
|
|
238
|
+
* session. Returns the launch with its scope.
|
|
239
|
+
*/
|
|
240
|
+
const launchWithRetries = Effect.fn("ClaudeChildBackend.launchWithRetries")(function* (request, options, messages, scope) {
|
|
241
|
+
yield* Effect.annotateCurrentSpan({ sessionId: request.session.id, worker: request.worker });
|
|
242
|
+
for (let retry = 0;; retry += 1) {
|
|
243
|
+
const launchScope = yield* Scope.fork(scope);
|
|
244
|
+
const opened = {
|
|
245
|
+
...(yield* launchOnce(request, options, messages).pipe(Scope.provide(launchScope), Effect.onError((cause) => Scope.close(launchScope, Exit.failCause(cause))))),
|
|
246
|
+
scope: launchScope,
|
|
247
|
+
};
|
|
248
|
+
if (opened.login !== "contended") {
|
|
249
|
+
if (opened.login === "undecided")
|
|
250
|
+
yield* Effect.logInfo("Claude child started without showing whether its login worked", {
|
|
251
|
+
sessionId: request.session.id,
|
|
252
|
+
worker: request.worker,
|
|
253
|
+
messages: messages.length,
|
|
254
|
+
});
|
|
255
|
+
return opened;
|
|
256
|
+
}
|
|
257
|
+
const delay = LOGIN_RETRY_DELAYS[retry];
|
|
258
|
+
if (delay === undefined) {
|
|
259
|
+
yield* Effect.logWarning("Claude child still could not refresh its Claude login; returning the error to the parent", {
|
|
260
|
+
sessionId: request.session.id,
|
|
261
|
+
parentSessionId: request.session.parentId,
|
|
262
|
+
worker: request.worker,
|
|
263
|
+
retries: retry,
|
|
264
|
+
});
|
|
265
|
+
return opened;
|
|
266
|
+
}
|
|
267
|
+
yield* Scope.close(launchScope, Exit.void);
|
|
268
|
+
yield* Effect.logInfo("Claude child could not refresh its Claude login; launching it again", {
|
|
269
|
+
sessionId: request.session.id,
|
|
270
|
+
parentSessionId: request.session.parentId,
|
|
271
|
+
worker: request.worker,
|
|
272
|
+
retry: retry + 1,
|
|
273
|
+
maxRetries: LOGIN_RETRY_DELAYS.length,
|
|
274
|
+
delay: Duration.format(delay),
|
|
275
|
+
});
|
|
276
|
+
yield* Effect.sleep(delay);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const launch = Effect.fn("ClaudeChildBackend.launch")(function* (request) {
|
|
280
|
+
yield* Effect.annotateCurrentSpan({
|
|
281
|
+
agent: request.session.agent,
|
|
282
|
+
effort: request.session.brain.effort,
|
|
283
|
+
harness: request.session.brain.harness,
|
|
284
|
+
model: request.session.brain.model,
|
|
285
|
+
parentSessionId: request.session.parentId,
|
|
286
|
+
sessionId: request.session.id,
|
|
287
|
+
worker: request.worker,
|
|
288
|
+
});
|
|
289
|
+
yield* Effect.logInfo("Launching Claude child session");
|
|
290
|
+
const context = yield* Effect.context();
|
|
291
|
+
const scope = yield* Effect.scope;
|
|
292
|
+
let boundary = Effect.void;
|
|
293
|
+
const pauseAtBoundary = async (_input, _id, options) => {
|
|
294
|
+
await Effect.runPromiseWith(context)(Effect.acquireUseRelease(Effect.forkIn(Effect.suspend(() => boundary), scope), Fiber.join, Fiber.interrupt), { signal: options.signal });
|
|
295
|
+
return {};
|
|
296
|
+
};
|
|
297
|
+
const options = {
|
|
298
|
+
...optionsFor(request),
|
|
299
|
+
hooks: {
|
|
300
|
+
PreToolUse: [{ hooks: [pauseAtBoundary], timeout: 86_400 }],
|
|
301
|
+
PostToolUse: [{ hooks: [pauseAtBoundary], timeout: 86_400 }],
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
// The launch that runs the session; a resume replaces it.
|
|
305
|
+
let current = yield* launchWithRetries(request, options, [request.prompt], scope);
|
|
306
|
+
const channel = yield* sessionChannel({
|
|
307
|
+
label: "Claude child session",
|
|
308
|
+
sessionId: request.session.id,
|
|
309
|
+
});
|
|
310
|
+
channel.onTerminal(() => {
|
|
311
|
+
current.input.end();
|
|
312
|
+
});
|
|
313
|
+
const conversation = yield* sessionConversation({
|
|
314
|
+
session: request.session,
|
|
315
|
+
channel,
|
|
316
|
+
// A message sent during a turn is queued until a tool call returns or the turn ends.
|
|
317
|
+
send: (message, activity) => Effect.try({
|
|
318
|
+
try: () => current.input.offer(userMessage(message)),
|
|
319
|
+
catch: (cause) => operationError("steer", request, "Can't send a message to Claude", cause),
|
|
320
|
+
}).pipe(Effect.as(activity === "idle" ? "read" : "queued")),
|
|
321
|
+
pause: Effect.void,
|
|
322
|
+
beforeFinish: Effect.void,
|
|
323
|
+
});
|
|
324
|
+
boundary = conversation.awaitUnpaused;
|
|
325
|
+
const tools = new Map();
|
|
326
|
+
const usage = claudeSessionMeter();
|
|
327
|
+
// The latest session ID from Claude, which a resume loads.
|
|
328
|
+
let harnessSessionId = Option.none();
|
|
329
|
+
// Resumes since Claude last sent a message.
|
|
330
|
+
let resumes = 0;
|
|
331
|
+
let backgroundTasks = [];
|
|
332
|
+
let reportsSessionState = false;
|
|
333
|
+
let completedTurn = Option.none();
|
|
334
|
+
// Claude sends `init` with one session ID before each turn; the first is the launch record.
|
|
335
|
+
const launched = yield* Ref.make(false);
|
|
336
|
+
// API retries reported in the current turn, and whether the last retry is unresolved.
|
|
337
|
+
let retries = 0;
|
|
338
|
+
let retrying = false;
|
|
339
|
+
const reportRetry = Effect.fn("ClaudeChildBackend.reportRetry")(function* (retry) {
|
|
340
|
+
yield* Effect.annotateCurrentSpan({ sessionId: request.session.id });
|
|
341
|
+
retries += 1;
|
|
342
|
+
retrying = true;
|
|
343
|
+
const message = apiRetryMessage(retry);
|
|
344
|
+
yield* Effect.logWarning("Claude child cannot reach its provider and will retry", {
|
|
345
|
+
sessionId: request.session.id,
|
|
346
|
+
retries,
|
|
347
|
+
message,
|
|
348
|
+
status: retry.error_status,
|
|
349
|
+
error: retry.error,
|
|
350
|
+
});
|
|
351
|
+
yield* channel.emit(new ProviderRetrying({
|
|
352
|
+
session: request.session,
|
|
353
|
+
message,
|
|
354
|
+
retries: ProviderRetries.make(retries),
|
|
355
|
+
}));
|
|
356
|
+
});
|
|
357
|
+
const reportRecovery = Effect.fn("ClaudeChildBackend.reportRecovery")(function* () {
|
|
358
|
+
yield* Effect.annotateCurrentSpan({ sessionId: request.session.id });
|
|
359
|
+
retrying = false;
|
|
360
|
+
yield* Effect.logInfo("Claude child reached its provider again", {
|
|
361
|
+
sessionId: request.session.id,
|
|
362
|
+
retries,
|
|
363
|
+
});
|
|
364
|
+
yield* channel.emit(new ProviderRecovered({
|
|
365
|
+
session: request.session,
|
|
366
|
+
retries: ProviderRetries.make(retries),
|
|
367
|
+
}));
|
|
368
|
+
});
|
|
369
|
+
/** Applies one message from Claude to the session. */
|
|
370
|
+
const handle = (message) => Effect.gen(function* () {
|
|
371
|
+
const retry = decodeApiRetry(message);
|
|
372
|
+
if (Option.isSome(retry))
|
|
373
|
+
return yield* reportRetry(retry.value);
|
|
374
|
+
if (retrying && isAssistantMessage(message))
|
|
375
|
+
yield* reportRecovery();
|
|
376
|
+
if (Schema.is(ClaudeCompletionHeader)(message)) {
|
|
377
|
+
const update = yield* decodeCompletionUpdate(message).pipe(Effect.mapError((cause) => operationError("stream", request, "Claude returned an invalid completion update", cause)));
|
|
378
|
+
if (update.subtype === "background_tasks_changed") {
|
|
379
|
+
// Each update replaces the whole task snapshot. Ambient watchers are excluded
|
|
380
|
+
// because they are not work for the parent.
|
|
381
|
+
backgroundTasks = update.tasks
|
|
382
|
+
.filter((task) => task.ambient !== true)
|
|
383
|
+
.map((task) => task.task_id);
|
|
384
|
+
yield* Effect.logInfo("Claude background work changed", {
|
|
385
|
+
sessionId: request.session.id,
|
|
386
|
+
backgroundTaskIds: backgroundTasks,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
reportsSessionState = true;
|
|
391
|
+
const candidate = completedTurn;
|
|
392
|
+
completedTurn = Option.none();
|
|
393
|
+
// The SDK reports idle after the result flush and the background-notification loop.
|
|
394
|
+
// An empty task snapshot alone does not finish the session, and a waiting turn's
|
|
395
|
+
// answer is not reused before the notification has run.
|
|
396
|
+
if (update.state === "idle" && Option.isSome(candidate)) {
|
|
397
|
+
if (backgroundTasks.length === 0)
|
|
398
|
+
yield* conversation.finish(candidate.value);
|
|
399
|
+
else
|
|
400
|
+
yield* Effect.logInfo("Claude child is waiting for background work", {
|
|
401
|
+
sessionId: request.session.id,
|
|
402
|
+
backgroundTaskIds: backgroundTasks,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const init = decodeClaudeInit(message);
|
|
409
|
+
if (Option.isSome(init))
|
|
410
|
+
harnessSessionId = Option.some(init.value.session_id);
|
|
411
|
+
if (Option.isSome(init) && !(yield* Ref.getAndSet(launched, true))) {
|
|
412
|
+
const harnessSession = new HarnessSession({
|
|
413
|
+
id: init.value.session_id,
|
|
414
|
+
cwd: request.cwd,
|
|
415
|
+
});
|
|
416
|
+
yield* channel.emit(new SessionStarted({
|
|
417
|
+
session: request.session,
|
|
418
|
+
harnessSession: Option.some(harnessSession),
|
|
419
|
+
}));
|
|
420
|
+
yield* Effect.logInfo("Claude child session saved", {
|
|
421
|
+
sessionId: request.session.id,
|
|
422
|
+
parentSessionId: request.session.parentId,
|
|
423
|
+
harnessSessionId: harnessSession.id,
|
|
424
|
+
cwd: harnessSession.cwd,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const used = usage.read(message);
|
|
428
|
+
if (Option.isSome(used))
|
|
429
|
+
yield* channel.emit(new SessionTokensUpdated({ session: request.session, ...used.value }));
|
|
430
|
+
for (const event of sessionEventsFrom(message, request.session, tools)) {
|
|
431
|
+
yield* channel.emit(event);
|
|
432
|
+
}
|
|
433
|
+
if (!Schema.is(ClaudeResultMessage)(message))
|
|
434
|
+
return;
|
|
435
|
+
completedTurn = Option.none();
|
|
436
|
+
retries = 0;
|
|
437
|
+
retrying = false;
|
|
438
|
+
if (message.subtype !== "success" || message.result === undefined) {
|
|
439
|
+
const detail = message.errors?.join("; ") ?? "Claude returned an unsuccessful result";
|
|
440
|
+
yield* channel.fail(new AgentResultError({
|
|
441
|
+
sessionId: Option.some(request.session.id),
|
|
442
|
+
message: detail,
|
|
443
|
+
}));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const stillQueued = message.queued_turn_count;
|
|
447
|
+
// Claude has read every queued message except those still queued as separate turns.
|
|
448
|
+
if (stillQueued !== undefined)
|
|
449
|
+
yield* conversation.read((queued) => queued.length - stillQueued);
|
|
450
|
+
if (message.queued_turn_count !== undefined && message.queued_turn_count > 0) {
|
|
451
|
+
yield* Effect.logInfo("Claude child has queued user turns", {
|
|
452
|
+
sessionId: request.session.id,
|
|
453
|
+
queuedTurns: message.queued_turn_count,
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const answer = message.result.trim() || "Claude child session completed";
|
|
458
|
+
if (reportsSessionState) {
|
|
459
|
+
completedTurn = Option.some(answer);
|
|
460
|
+
}
|
|
461
|
+
else if (backgroundTasks.length > 0) {
|
|
462
|
+
yield* Effect.logInfo("Claude child is waiting for background work", {
|
|
463
|
+
sessionId: request.session.id,
|
|
464
|
+
backgroundTaskIds: backgroundTasks,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// Older Claude Code versions on Workers do not send session-state messages.
|
|
469
|
+
yield* conversation.finish(answer);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
/**
|
|
473
|
+
* Applies `launch`'s messages until its output ends. Returns the error that broke the output,
|
|
474
|
+
* or `None` if the output closed.
|
|
475
|
+
*/
|
|
476
|
+
const drain = (launch) => Effect.gen(function* () {
|
|
477
|
+
let broken = Option.none();
|
|
478
|
+
yield* Stream.concat(Stream.fromIterable(launch.read), Stream.fromQueue(launch.output)).pipe(Stream.map((message) => Result.succeed(message)), Stream.catch((error) => Stream.make(Result.fail(error))), Stream.runForEach(Result.match({
|
|
479
|
+
onSuccess: (message) => Effect.suspend(() => {
|
|
480
|
+
resumes = 0;
|
|
481
|
+
return handle(message);
|
|
482
|
+
}),
|
|
483
|
+
onFailure: (error) => Effect.sync(() => {
|
|
484
|
+
broken = Option.some(error);
|
|
485
|
+
}),
|
|
486
|
+
})));
|
|
487
|
+
return broken;
|
|
488
|
+
});
|
|
489
|
+
/**
|
|
490
|
+
* Resumes the session in a new launch after `broken` lost its SSH connection to the Worker:
|
|
491
|
+
* stops what is left of its Claude process there, then launches Claude with the saved session,
|
|
492
|
+
* retrying after each delay in `RESUME_DELAYS`. Returns `None` if the session can't be resumed.
|
|
493
|
+
*/
|
|
494
|
+
const resume = Effect.fn("ClaudeChildBackend.resume")(function* (broken, error) {
|
|
495
|
+
yield* Effect.annotateCurrentSpan({
|
|
496
|
+
sessionId: request.session.id,
|
|
497
|
+
worker: request.worker,
|
|
498
|
+
});
|
|
499
|
+
const fields = {
|
|
500
|
+
sessionId: request.session.id,
|
|
501
|
+
parentSessionId: request.session.parentId,
|
|
502
|
+
worker: request.worker,
|
|
503
|
+
};
|
|
504
|
+
const notResumed = (reason) => Effect.logInfo("Not resuming the Claude child after its output ended", {
|
|
505
|
+
...fields,
|
|
506
|
+
reason,
|
|
507
|
+
message: error.message,
|
|
508
|
+
}).pipe(Effect.as(Option.none()));
|
|
509
|
+
const host = request.host;
|
|
510
|
+
const harnessSession = harnessSessionId;
|
|
511
|
+
if (host._tag !== "RemoteShell")
|
|
512
|
+
return yield* notResumed("it runs on this machine");
|
|
513
|
+
if (Option.isNone(harnessSession))
|
|
514
|
+
return yield* notResumed("Claude saved no session");
|
|
515
|
+
if (!(yield* broken.lostConnection))
|
|
516
|
+
return yield* notResumed("its SSH connection didn't break");
|
|
517
|
+
yield* Effect.logWarning("Claude child lost its SSH connection to the Worker; resuming its session", { ...fields, harnessSessionId: harnessSession.value, message: error.message });
|
|
518
|
+
yield* Scope.close(broken.scope, Exit.void);
|
|
519
|
+
// The broken launch's process took its background work and open tool calls with it.
|
|
520
|
+
backgroundTasks = [];
|
|
521
|
+
completedTurn = Option.none();
|
|
522
|
+
for (const [callId, tool] of tools)
|
|
523
|
+
yield* channel.emit(new ToolFinished({
|
|
524
|
+
session: request.session,
|
|
525
|
+
callId,
|
|
526
|
+
tool,
|
|
527
|
+
output: "usine lost its connection to the Worker before this call returned",
|
|
528
|
+
isError: true,
|
|
529
|
+
}));
|
|
530
|
+
tools.clear();
|
|
531
|
+
usage.restart();
|
|
532
|
+
for (;;) {
|
|
533
|
+
const delay = RESUME_DELAYS[resumes];
|
|
534
|
+
if (delay === undefined) {
|
|
535
|
+
yield* Effect.logWarning("Could not resume the Claude child; it fails", {
|
|
536
|
+
...fields,
|
|
537
|
+
tries: resumes,
|
|
538
|
+
});
|
|
539
|
+
return Option.none();
|
|
540
|
+
}
|
|
541
|
+
resumes += 1;
|
|
542
|
+
yield* Effect.logInfo("Resuming the Claude child after a pause", {
|
|
543
|
+
...fields,
|
|
544
|
+
try: resumes,
|
|
545
|
+
maxTries: RESUME_DELAYS.length,
|
|
546
|
+
delay: Duration.format(delay),
|
|
547
|
+
});
|
|
548
|
+
yield* Effect.sleep(delay);
|
|
549
|
+
if (yield* channel.isTerminal)
|
|
550
|
+
return Option.none();
|
|
551
|
+
// A paused or idle child waits for the operator, so only a working child gets a turn.
|
|
552
|
+
const working = !((yield* conversation.isPaused) || (yield* conversation.isIdle));
|
|
553
|
+
const messages = [...(working ? [RESUME_PROMPT] : []), ...(yield* conversation.unread)];
|
|
554
|
+
const resumed = yield* stopRemote(request, host).pipe(Effect.andThen(launchWithRetries(request, { ...options, resume: harnessSession.value }, messages, scope)), Effect.map(Option.some), Effect.catchTag("SessionOperationError", (failure) => Effect.logWarning("Could not resume the Claude child yet", {
|
|
555
|
+
...fields,
|
|
556
|
+
try: resumes,
|
|
557
|
+
message: failure.message,
|
|
558
|
+
}).pipe(Effect.as(Option.none()))));
|
|
559
|
+
if (Option.isSome(resumed)) {
|
|
560
|
+
yield* Effect.logInfo("Resumed the Claude child's session", {
|
|
561
|
+
...fields,
|
|
562
|
+
harnessSessionId: harnessSession.value,
|
|
563
|
+
working,
|
|
564
|
+
messages: messages.length,
|
|
565
|
+
});
|
|
566
|
+
return resumed;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
const consume = Effect.gen(function* () {
|
|
571
|
+
for (;;) {
|
|
572
|
+
const broken = yield* drain(current);
|
|
573
|
+
if (yield* channel.isTerminal)
|
|
574
|
+
return;
|
|
575
|
+
const error = Option.getOrElse(broken, () => operationError("stream", request, "Claude output closed without a result", new Error("unexpected end of output")));
|
|
576
|
+
const resumed = yield* resume(current, error);
|
|
577
|
+
if (Option.isNone(resumed))
|
|
578
|
+
return yield* channel.fail(error);
|
|
579
|
+
current = resumed.value;
|
|
580
|
+
}
|
|
581
|
+
}).pipe(Effect.catch((error) => channel.fail(error)));
|
|
582
|
+
yield* Effect.forkScoped(consume);
|
|
583
|
+
const steer = Effect.fn("ClaudeChildBackend.steer")((message) => Effect.logDebug("Steering Claude child session", {
|
|
584
|
+
sessionId: request.session.id,
|
|
585
|
+
}).pipe(Effect.andThen(Effect.try({
|
|
586
|
+
try: () => current.input.offer(userMessage(message)),
|
|
587
|
+
catch: (cause) => operationError("steer", request, "Can't steer Claude", cause),
|
|
588
|
+
}))));
|
|
589
|
+
const interrupt = Effect.logInfo("Interrupting Claude child session", {
|
|
590
|
+
sessionId: request.session.id,
|
|
591
|
+
}).pipe(Effect.andThen(Effect.tryPromise({
|
|
592
|
+
try: () => current.query.interrupt(),
|
|
593
|
+
catch: (cause) => operationError("interrupt", request, "Can't interrupt Claude", cause),
|
|
594
|
+
})), Effect.tap(() => Effect.sync(() => current.input.end())), Effect.asVoid);
|
|
595
|
+
return {
|
|
596
|
+
session: request.session,
|
|
597
|
+
events: Stream.fromQueue(channel.events),
|
|
598
|
+
interrupt,
|
|
599
|
+
result: Deferred.await(channel.result),
|
|
600
|
+
steer,
|
|
601
|
+
intervene: conversation.intervene,
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
return ChildBackend.of({ launch });
|
|
605
|
+
});
|
|
606
|
+
/** Provides `ChildBackend` with Claude Code for every delegated task. */
|
|
607
|
+
export const claudeChildLayer = Layer.effect(ChildBackend, makeClaudeChild);
|
|
608
|
+
/** Provides `ClaudeChildBackend` for direct use or harness-based dispatch. */
|
|
609
|
+
export const claudeChildBackendLayer = Layer.effect(ClaudeChildBackend, makeClaudeChild);
|
|
610
|
+
//# sourceMappingURL=claude.js.map
|