@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
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/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -1,53 +1,15 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { realpathSync } from 'node:fs'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
|
|
5
1
|
import {
|
|
6
2
|
query as sdkQuery,
|
|
7
3
|
type EffortLevel,
|
|
8
|
-
type Options,
|
|
9
|
-
type SDKUserMessage,
|
|
10
4
|
} from '@anthropic-ai/claude-agent-sdk'
|
|
11
|
-
import { z } from 'zod'
|
|
12
5
|
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type AgentTurn,
|
|
18
|
-
type ModelTokens,
|
|
19
|
-
} from './agent-launcher'
|
|
20
|
-
import {
|
|
21
|
-
AGENT_EFFORT,
|
|
22
|
-
agentEnv,
|
|
23
|
-
agentOptions,
|
|
24
|
-
CLAUDE_MODEL,
|
|
25
|
-
checkModel,
|
|
26
|
-
} from './claude-options'
|
|
27
|
-
import { createDeliveryHook, createLucaServer } from './message-tool'
|
|
28
|
-
import type { AgentRole } from './role-results'
|
|
6
|
+
import type { AgentLauncher } from './agent-launcher'
|
|
7
|
+
import { AGENT_EFFORT } from './claude-options'
|
|
8
|
+
import { createLaunch, type AgentQuery } from './launch-session'
|
|
9
|
+
import { createSessionPool } from './session-pool'
|
|
29
10
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import { planSignal } from '../limits/plan-signals'
|
|
33
|
-
import { runCommand } from '../shell/run-command'
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* One running SDK session, as the launcher uses it. The SDK's own `Query`
|
|
37
|
-
* fits this; tests hand in a fake. Messages are read as `unknown` and parsed
|
|
38
|
-
* with the launcher's own schemas.
|
|
39
|
-
*/
|
|
40
|
-
export type AgentQuerySession = AsyncIterable<unknown> & {
|
|
41
|
-
accountInfo: () => Promise<unknown>
|
|
42
|
-
interrupt: () => Promise<unknown>
|
|
43
|
-
close: () => void
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Starts an SDK session: the SDK's `query`, or a fake in tests. */
|
|
47
|
-
export type AgentQuery = (params: {
|
|
48
|
-
prompt: AsyncIterable<SDKUserMessage>
|
|
49
|
-
options: Options
|
|
50
|
-
}) => AgentQuerySession
|
|
11
|
+
export type { AgentQuery } from './launch-session'
|
|
12
|
+
export type { AgentQuerySession } from './open-session'
|
|
51
13
|
|
|
52
14
|
/** A turn gives up after this long unless told otherwise: 40 minutes. */
|
|
53
15
|
export const DEFAULT_TURN_TIMEOUT_MS = 40 * 60_000
|
|
@@ -58,401 +20,12 @@ export const DEFAULT_TURN_TIMEOUT_MS = 40 * 60_000
|
|
|
58
20
|
*/
|
|
59
21
|
export const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60_000
|
|
60
22
|
|
|
61
|
-
/** The Claude plans an agent may run on. */
|
|
62
|
-
const PLANS = ['pro', 'max', 'team', 'enterprise']
|
|
63
|
-
|
|
64
|
-
const AccountSchema = z.looseObject({
|
|
65
|
-
subscriptionType: z.string().optional(),
|
|
66
|
-
apiKeySource: z.string().optional(),
|
|
67
|
-
apiProvider: z.string().optional(),
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const InitSchema = z.object({
|
|
71
|
-
type: z.literal('system'),
|
|
72
|
-
subtype: z.literal('init'),
|
|
73
|
-
apiKeySource: z.string(),
|
|
74
|
-
model: z.string(),
|
|
75
|
-
claude_code_version: z.string().nullable().default(null),
|
|
76
|
-
session_id: z.string().nullable().default(null),
|
|
77
|
-
tools: z.array(z.string()).default([]),
|
|
78
|
-
mcp_servers: z.array(z.object({ name: z.string() })).default([]),
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const RateLimitSchema = z.object({
|
|
82
|
-
type: z.literal('rate_limit_event'),
|
|
83
|
-
rate_limit_info: z.record(z.string(), z.unknown()),
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
const AssistantSchema = z.object({
|
|
87
|
-
type: z.literal('assistant'),
|
|
88
|
-
error: z.string().optional(),
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const ResultSchema = z.object({
|
|
92
|
-
type: z.literal('result'),
|
|
93
|
-
subtype: z.string(),
|
|
94
|
-
is_error: z.boolean().default(false),
|
|
95
|
-
num_turns: z.number().int().min(0).default(0),
|
|
96
|
-
duration_ms: z.number().min(0).default(0),
|
|
97
|
-
total_cost_usd: z.number().default(0),
|
|
98
|
-
usage: z
|
|
99
|
-
.object({
|
|
100
|
-
input_tokens: z.number().default(0),
|
|
101
|
-
output_tokens: z.number().default(0),
|
|
102
|
-
cache_read_input_tokens: z.number().default(0),
|
|
103
|
-
cache_creation_input_tokens: z.number().default(0),
|
|
104
|
-
})
|
|
105
|
-
.default({
|
|
106
|
-
input_tokens: 0,
|
|
107
|
-
output_tokens: 0,
|
|
108
|
-
cache_read_input_tokens: 0,
|
|
109
|
-
cache_creation_input_tokens: 0,
|
|
110
|
-
}),
|
|
111
|
-
/** Tokens per model, subagents included: the session's running totals. */
|
|
112
|
-
modelUsage: z
|
|
113
|
-
.record(
|
|
114
|
-
z.string(),
|
|
115
|
-
z.object({
|
|
116
|
-
inputTokens: z.number().default(0),
|
|
117
|
-
outputTokens: z.number().default(0),
|
|
118
|
-
cacheReadInputTokens: z.number().default(0),
|
|
119
|
-
cacheCreationInputTokens: z.number().default(0),
|
|
120
|
-
})
|
|
121
|
-
)
|
|
122
|
-
.default({}),
|
|
123
|
-
permission_denials: z
|
|
124
|
-
.array(z.object({ tool_name: z.string(), tool_use_id: z.string() }))
|
|
125
|
-
.default([]),
|
|
126
|
-
structured_output: z.unknown().optional(),
|
|
127
|
-
result: z.string().default(''),
|
|
128
|
-
errors: z.array(z.string()).default([]),
|
|
129
|
-
session_id: z.string().optional(),
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
type Failed = {
|
|
133
|
-
ok: false
|
|
134
|
-
failure: 'agent' | 'engine' | 'stop' | 'plan'
|
|
135
|
-
error: string
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
type Outcome = { ok: true; structured_output: unknown } | Failed
|
|
139
|
-
|
|
140
|
-
const stop = (error: string): Failed => ({ ok: false, failure: 'stop', error })
|
|
141
|
-
|
|
142
|
-
const errorText = (error: unknown): string =>
|
|
143
|
-
error instanceof Error ? error.message : String(error)
|
|
144
|
-
|
|
145
|
-
/** Why the account may not run agents, or `null` if it may. */
|
|
146
|
-
const accountProblem = (raw: unknown): string | null => {
|
|
147
|
-
const parsed = AccountSchema.safeParse(raw)
|
|
148
|
-
if (!parsed.success)
|
|
149
|
-
return 'accountInfo() gave an answer of the wrong shape'
|
|
150
|
-
const { subscriptionType, apiKeySource, apiProvider } = parsed.data
|
|
151
|
-
const plan = (subscriptionType ?? '').toLowerCase()
|
|
152
|
-
if (!PLANS.some((name) => plan.includes(name))) {
|
|
153
|
-
return `no Claude subscription (subscriptionType=${subscriptionType ?? 'none'})`
|
|
154
|
-
}
|
|
155
|
-
if (apiKeySource !== undefined && apiKeySource !== 'none') {
|
|
156
|
-
return `an API key is in use (apiKeySource=${apiKeySource})`
|
|
157
|
-
}
|
|
158
|
-
if (apiProvider !== undefined && apiProvider !== 'firstParty') {
|
|
159
|
-
return `not Anthropic's own API (apiProvider=${apiProvider})`
|
|
160
|
-
}
|
|
161
|
-
return null
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Why an init message is unsafe to go on with, or `null` if it is fine. */
|
|
165
|
-
const initProblem = (init: z.infer<typeof InitSchema>): string | null => {
|
|
166
|
-
if (init.apiKeySource !== 'none') {
|
|
167
|
-
return `init apiKeySource=${init.apiKeySource} (expected "none")`
|
|
168
|
-
}
|
|
169
|
-
const model = checkModel({ model: init.model })
|
|
170
|
-
if (model !== null) return `init model: ${model}`
|
|
171
|
-
const servers = init.mcp_servers
|
|
172
|
-
.map(({ name }) => name)
|
|
173
|
-
.filter((name) => name !== 'luca')
|
|
174
|
-
const tools = init.tools.filter(
|
|
175
|
-
(tool) => tool.startsWith('mcp__') && !tool.startsWith('mcp__luca__')
|
|
176
|
-
)
|
|
177
|
-
if (servers.length > 0 || tools.length > 0) {
|
|
178
|
-
return `foreign MCP servers or tools: ${[...servers, ...tools].join(', ')}`
|
|
179
|
-
}
|
|
180
|
-
return null
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** A turn the plan cut off: the reason, in words. */
|
|
184
|
-
const planCut = (error: string): Failed => ({
|
|
185
|
-
ok: false,
|
|
186
|
-
failure: 'plan',
|
|
187
|
-
error,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
/** A queue the SDK reads the agent's prompt from (streaming-input mode). */
|
|
191
|
-
const createInputChannel = () => {
|
|
192
|
-
const queue: SDKUserMessage[] = []
|
|
193
|
-
let wake: (() => void) | null = null
|
|
194
|
-
let closed = false
|
|
195
|
-
const poke = () => {
|
|
196
|
-
wake?.()
|
|
197
|
-
wake = null
|
|
198
|
-
}
|
|
199
|
-
const iterate = async function* (): AsyncGenerator<SDKUserMessage> {
|
|
200
|
-
while (true) {
|
|
201
|
-
const next = queue.shift()
|
|
202
|
-
if (next !== undefined) {
|
|
203
|
-
yield next
|
|
204
|
-
continue
|
|
205
|
-
}
|
|
206
|
-
if (closed) return
|
|
207
|
-
await new Promise<void>((resolve) => {
|
|
208
|
-
wake = resolve
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return {
|
|
213
|
-
push: (message: SDKUserMessage) => {
|
|
214
|
-
queue.push(message)
|
|
215
|
-
poke()
|
|
216
|
-
},
|
|
217
|
-
close: () => {
|
|
218
|
-
closed = true
|
|
219
|
-
poke()
|
|
220
|
-
},
|
|
221
|
-
iterable: iterate(),
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const realClaudePath = (): string => {
|
|
226
|
-
const found = Bun.which('claude')
|
|
227
|
-
if (found === null) throw new Error('No `claude` binary on PATH.')
|
|
228
|
-
return realpathSync(found)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const commonGitDir = async ({ cwd }: { cwd: string }): Promise<string> => {
|
|
232
|
-
const result = await runCommand({
|
|
233
|
-
cmd: ['git', 'rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
234
|
-
cwd,
|
|
235
|
-
timeout_ms: 30_000,
|
|
236
|
-
})
|
|
237
|
-
if (result.exit_code !== 0) {
|
|
238
|
-
throw new Error(`git rev-parse failed in ${cwd}: ${result.stderr}`)
|
|
239
|
-
}
|
|
240
|
-
return realpathSync(result.stdout.trim())
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* One agent session the launcher keeps open between turns, so a follow-up
|
|
245
|
-
* reaches the same agent. Its guard hook, sandbox, and options are the ones
|
|
246
|
-
* it was launched with.
|
|
247
|
-
*/
|
|
248
|
-
type OpenSession = {
|
|
249
|
-
role: AgentRole
|
|
250
|
-
ticket: number
|
|
251
|
-
cwd: string
|
|
252
|
-
may_edit_tests: boolean
|
|
253
|
-
running: AgentQuerySession
|
|
254
|
-
input: ReturnType<typeof createInputChannel>
|
|
255
|
-
/** What the launcher has seen of the session; per-turn fields reset each turn. */
|
|
256
|
-
summary: AgentSession
|
|
257
|
-
/** The SDK's running tokens per model, as of the session's last result. */
|
|
258
|
-
model_totals: Record<string, ModelTokens>
|
|
259
|
-
/** Resolves the turn waiting for the session's next outcome. */
|
|
260
|
-
waiter: ((outcome: Outcome) => void) | null
|
|
261
|
-
/** A stop or an engine failure: the session is over, with this outcome. */
|
|
262
|
-
ended: Outcome | null
|
|
263
|
-
/** Reads the session's messages until it ends. */
|
|
264
|
-
pump: Promise<void>
|
|
265
|
-
idle_timer: ReturnType<typeof setTimeout> | undefined
|
|
266
|
-
stderr: string[]
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** The session's next outcome: a result, a stop, or its end. */
|
|
270
|
-
const nextOutcome = (open: OpenSession): Promise<Outcome> =>
|
|
271
|
-
open.ended !== null
|
|
272
|
-
? Promise.resolve(open.ended)
|
|
273
|
-
: new Promise<Outcome>((resolve) => {
|
|
274
|
-
open.waiter = resolve
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
const deliver = ({
|
|
278
|
-
open,
|
|
279
|
-
outcome,
|
|
280
|
-
}: {
|
|
281
|
-
open: OpenSession
|
|
282
|
-
outcome: Outcome
|
|
283
|
-
}): void => {
|
|
284
|
-
if (!outcome.ok && outcome.failure !== 'agent') open.ended = outcome
|
|
285
|
-
const waiter = open.waiter
|
|
286
|
-
open.waiter = null
|
|
287
|
-
waiter?.(outcome)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const stderrTail = (open: OpenSession): string =>
|
|
291
|
-
open.stderr.length === 0 ? '' : `\n${open.stderr.join('').slice(-2000)}`
|
|
292
|
-
|
|
293
|
-
/** A result's `modelUsage`, in the journal's words. */
|
|
294
|
-
const modelTotalsOf = (
|
|
295
|
-
modelUsage: z.infer<typeof ResultSchema>['modelUsage']
|
|
296
|
-
): Record<string, ModelTokens> =>
|
|
297
|
-
Object.fromEntries(
|
|
298
|
-
Object.entries(modelUsage).map(([name, each]) => [
|
|
299
|
-
name,
|
|
300
|
-
{
|
|
301
|
-
input_tokens: each.inputTokens,
|
|
302
|
-
output_tokens: each.outputTokens,
|
|
303
|
-
cache_read_input_tokens: each.cacheReadInputTokens,
|
|
304
|
-
cache_creation_input_tokens: each.cacheCreationInputTokens,
|
|
305
|
-
},
|
|
306
|
-
])
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* One turn's tokens per model: the session's running totals less those at
|
|
311
|
-
* its last result, as the SDK sums `modelUsage` over a session's turns.
|
|
312
|
-
*/
|
|
313
|
-
const turnTokens = ({
|
|
314
|
-
totals,
|
|
315
|
-
before,
|
|
316
|
-
}: {
|
|
317
|
-
totals: Record<string, ModelTokens>
|
|
318
|
-
before: Record<string, ModelTokens>
|
|
319
|
-
}): Record<string, ModelTokens> =>
|
|
320
|
-
Object.fromEntries(
|
|
321
|
-
Object.entries(totals).map(([name, now]) => {
|
|
322
|
-
const was = before[name]
|
|
323
|
-
const less = (key: keyof ModelTokens) =>
|
|
324
|
-
Math.max(0, now[key] - (was?.[key] ?? 0))
|
|
325
|
-
return [
|
|
326
|
-
name,
|
|
327
|
-
{
|
|
328
|
-
input_tokens: less('input_tokens'),
|
|
329
|
-
output_tokens: less('output_tokens'),
|
|
330
|
-
cache_read_input_tokens: less('cache_read_input_tokens'),
|
|
331
|
-
cache_creation_input_tokens: less(
|
|
332
|
-
'cache_creation_input_tokens'
|
|
333
|
-
),
|
|
334
|
-
},
|
|
335
|
-
]
|
|
336
|
-
})
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
/** Reads one message into the session's summary; an outcome ends the turn. */
|
|
340
|
-
const watch = ({
|
|
341
|
-
open,
|
|
342
|
-
raw,
|
|
343
|
-
}: {
|
|
344
|
-
open: OpenSession
|
|
345
|
-
raw: unknown
|
|
346
|
-
}): Outcome | null => {
|
|
347
|
-
const { summary } = open
|
|
348
|
-
const init = InitSchema.safeParse(raw)
|
|
349
|
-
if (init.success) {
|
|
350
|
-
summary.session_id = init.data.session_id
|
|
351
|
-
summary.model = init.data.model
|
|
352
|
-
summary.claude_code_version = init.data.claude_code_version
|
|
353
|
-
summary.api_key_source = init.data.apiKeySource
|
|
354
|
-
const problem = initProblem(init.data)
|
|
355
|
-
return problem === null ? null : stop(problem)
|
|
356
|
-
}
|
|
357
|
-
const rateLimit = RateLimitSchema.safeParse(raw)
|
|
358
|
-
if (rateLimit.success) {
|
|
359
|
-
const info = rateLimit.data.rate_limit_info
|
|
360
|
-
summary.rate_limit_events.push({
|
|
361
|
-
...info,
|
|
362
|
-
arrived_at: new Date().toISOString(),
|
|
363
|
-
})
|
|
364
|
-
const signal = planSignal({ info })
|
|
365
|
-
switch (signal.kind) {
|
|
366
|
-
case 'ok':
|
|
367
|
-
return null
|
|
368
|
-
case 'billing':
|
|
369
|
-
return planCut(signal.reason)
|
|
370
|
-
case 'limit':
|
|
371
|
-
return planCut(
|
|
372
|
-
`rate_limit_event status=rejected (${signal.rate_limit_type ?? 'unknown'})`
|
|
373
|
-
)
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const assistant = AssistantSchema.safeParse(raw)
|
|
377
|
-
if (assistant.success) {
|
|
378
|
-
if (assistant.data.error !== 'billing_error') return null
|
|
379
|
-
summary.billing_error = true
|
|
380
|
-
return planCut('assistant error billing_error')
|
|
381
|
-
}
|
|
382
|
-
const result = ResultSchema.safeParse(raw)
|
|
383
|
-
if (!result.success) return null
|
|
384
|
-
const done = result.data
|
|
385
|
-
summary.num_turns = done.num_turns
|
|
386
|
-
summary.usage = done.usage
|
|
387
|
-
const totals = modelTotalsOf(done.modelUsage)
|
|
388
|
-
summary.model_usage = turnTokens({
|
|
389
|
-
totals,
|
|
390
|
-
before: open.model_totals,
|
|
391
|
-
})
|
|
392
|
-
open.model_totals = { ...open.model_totals, ...totals }
|
|
393
|
-
summary.total_cost_usd = done.total_cost_usd
|
|
394
|
-
summary.permission_denials = done.permission_denials.map(
|
|
395
|
-
({ tool_name, tool_use_id }) => ({ tool_name, tool_use_id })
|
|
396
|
-
)
|
|
397
|
-
if (done.subtype === 'success' && !done.is_error) {
|
|
398
|
-
return { ok: true, structured_output: done.structured_output }
|
|
399
|
-
}
|
|
400
|
-
const detail = done.errors.length > 0 ? done.errors.join('; ') : done.result
|
|
401
|
-
return {
|
|
402
|
-
ok: false,
|
|
403
|
-
failure: 'agent',
|
|
404
|
-
error: `The agent's turn ended with ${done.subtype}: ${detail}`,
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Reads a session's messages until it ends, handing each outcome to the turn
|
|
410
|
-
* waiting for it, then calls `on_end`.
|
|
411
|
-
*/
|
|
412
|
-
const pumpMessages = async ({
|
|
413
|
-
open,
|
|
414
|
-
on_end,
|
|
415
|
-
}: {
|
|
416
|
-
open: OpenSession
|
|
417
|
-
on_end: () => void
|
|
418
|
-
}): Promise<void> => {
|
|
419
|
-
try {
|
|
420
|
-
for await (const raw of open.running) {
|
|
421
|
-
const outcome = watch({ open, raw })
|
|
422
|
-
if (outcome === null) continue
|
|
423
|
-
deliver({ open, outcome })
|
|
424
|
-
if (open.ended !== null) break
|
|
425
|
-
}
|
|
426
|
-
if (open.ended !== null) {
|
|
427
|
-
on_end()
|
|
428
|
-
return
|
|
429
|
-
}
|
|
430
|
-
deliver({
|
|
431
|
-
open,
|
|
432
|
-
outcome: {
|
|
433
|
-
ok: false,
|
|
434
|
-
failure: 'engine',
|
|
435
|
-
error: `The agent session ended with no result.${stderrTail(open)}`,
|
|
436
|
-
},
|
|
437
|
-
})
|
|
438
|
-
} catch (error) {
|
|
439
|
-
deliver({
|
|
440
|
-
open,
|
|
441
|
-
outcome: {
|
|
442
|
-
ok: false,
|
|
443
|
-
failure: 'engine',
|
|
444
|
-
error: `The agent session failed: ${errorText(error)}${stderrTail(open)}`,
|
|
445
|
-
},
|
|
446
|
-
})
|
|
447
|
-
}
|
|
448
|
-
on_end()
|
|
449
|
-
}
|
|
450
|
-
|
|
451
23
|
/**
|
|
452
24
|
* The real agent launcher: one Claude Agent SDK session per agent, with
|
|
453
25
|
* every guard on. Per launch it:
|
|
454
26
|
*
|
|
455
|
-
* 1. refuses
|
|
27
|
+
* 1. refuses any model but `opus`, `sonnet` or `haiku` before starting
|
|
28
|
+
* anything; follow-ups stay on their session's model;
|
|
456
29
|
* 2. builds the role's locked-down options (`agentOptions`) with the guard
|
|
457
30
|
* hook (honoring `may_edit_tests`), the sandbox, and a clean environment;
|
|
458
31
|
* with `messaging` (test-writers and implementers), the `luca` server's
|
|
@@ -465,7 +38,10 @@ const pumpMessages = async ({
|
|
|
465
38
|
* cuts the turn off as `plan`, with the reason in its session, for the
|
|
466
39
|
* decision step to turn into a limit wait or a billing stop;
|
|
467
40
|
* 5. returns the result's structured output and the session's id (from its
|
|
468
|
-
* init message)
|
|
41
|
+
* init message);
|
|
42
|
+
* 6. tells the turn's `on_progress`, if given, what the agent is doing as it
|
|
43
|
+
* happens: its tokens so far, its latest action, the tool call still
|
|
44
|
+
* open, and its plan-usage readings (`turn-progress.ts`, #628).
|
|
469
45
|
*
|
|
470
46
|
* The session stays open after its turn, in streaming-input mode, so
|
|
471
47
|
* `followUp` can send it another message; the same watch applies to every
|
|
@@ -479,21 +55,18 @@ const pumpMessages = async ({
|
|
|
479
55
|
*
|
|
480
56
|
* @example
|
|
481
57
|
* const launcher = createClaudeLauncher({})
|
|
482
|
-
* const turn = await launcher.launch({ role: 'implementer', ticket: 11, prompt, cwd, may_edit_tests: false, config })
|
|
58
|
+
* const turn = await launcher.launch({ role: 'implementer', ticket: 11, prompt, cwd, may_edit_tests: false, config, messaging, model: 'opus' })
|
|
483
59
|
* // ... later, in a fix loop:
|
|
484
60
|
* if (turn.ok) await launcher.followUp({ session_id: turn.session_id, role: 'implementer', ticket: 11, message, cwd, config })
|
|
485
61
|
* await launcher.closeAll()
|
|
486
62
|
*/
|
|
487
63
|
export const createClaudeLauncher = ({
|
|
488
|
-
model,
|
|
489
64
|
effort,
|
|
490
65
|
query,
|
|
491
66
|
claude_path,
|
|
492
67
|
turn_timeout_ms,
|
|
493
68
|
idle_timeout_ms,
|
|
494
69
|
}: {
|
|
495
|
-
/** Defaults to `CLAUDE_MODEL`, Claude Opus 5.5. */
|
|
496
|
-
model?: string
|
|
497
70
|
/** Defaults to `AGENT_EFFORT`, the same for every role. */
|
|
498
71
|
effort?: EffortLevel
|
|
499
72
|
query?: AgentQuery
|
|
@@ -503,241 +76,29 @@ export const createClaudeLauncher = ({
|
|
|
503
76
|
/** Defaults to `DEFAULT_IDLE_TIMEOUT_MS`. */
|
|
504
77
|
idle_timeout_ms?: number
|
|
505
78
|
}): AgentLauncher & { closeAll: () => Promise<void> } => {
|
|
506
|
-
const useModel = model ?? CLAUDE_MODEL
|
|
507
79
|
const useEffort = effort ?? AGENT_EFFORT
|
|
508
80
|
const startQuery: AgentQuery = query ?? sdkQuery
|
|
509
81
|
const timeoutMs = turn_timeout_ms ?? DEFAULT_TURN_TIMEOUT_MS
|
|
510
82
|
const idleMs = idle_timeout_ms ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
/** Keeps a session open for follow-ups until it sits idle too long. */
|
|
524
|
-
const keepOpen = (open: OpenSession, session_id: string): void => {
|
|
525
|
-
sessions.set(session_id, open)
|
|
526
|
-
clearTimeout(open.idle_timer)
|
|
527
|
-
open.idle_timer = setTimeout(() => void close(open), idleMs)
|
|
528
|
-
open.idle_timer.unref?.()
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/** Resets the summary's per-turn fields; the session's own stay. */
|
|
532
|
-
const startTurn = (open: OpenSession): void => {
|
|
533
|
-
clearTimeout(open.idle_timer)
|
|
534
|
-
const fresh = AgentSessionSchema.parse({})
|
|
535
|
-
open.summary = {
|
|
536
|
-
...fresh,
|
|
537
|
-
session_id: open.summary.session_id,
|
|
538
|
-
model: open.summary.model,
|
|
539
|
-
claude_code_version: open.summary.claude_code_version,
|
|
540
|
-
api_key_source: open.summary.api_key_source,
|
|
541
|
-
subscription_type: open.summary.subscription_type,
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Sends one message and waits for the turn's outcome. A success or an
|
|
547
|
-
* agent failure keeps the session open for follow-ups; a stop, an
|
|
548
|
-
* engine failure, or a timeout closes it.
|
|
549
|
-
*/
|
|
550
|
-
const runTurn = async ({
|
|
551
|
-
open,
|
|
552
|
-
message,
|
|
553
|
-
started,
|
|
554
|
-
fallback_id,
|
|
555
|
-
}: {
|
|
556
|
-
open: OpenSession
|
|
557
|
-
message: string
|
|
558
|
-
started: number
|
|
559
|
-
/** The id to report if the session never named itself. */
|
|
560
|
-
fallback_id: string
|
|
561
|
-
}): Promise<AgentTurn> => {
|
|
562
|
-
const waiting = nextOutcome(open)
|
|
563
|
-
open.input.push({
|
|
564
|
-
type: 'user',
|
|
565
|
-
message: { role: 'user', content: message },
|
|
566
|
-
parent_tool_use_id: null,
|
|
567
|
-
})
|
|
568
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
569
|
-
const timedOut = new Promise<'timeout'>((resolve) => {
|
|
570
|
-
timer = setTimeout(() => resolve('timeout'), timeoutMs)
|
|
571
|
-
})
|
|
572
|
-
const raced = await Promise.race([waiting, timedOut])
|
|
573
|
-
clearTimeout(timer)
|
|
574
|
-
const outcome: Outcome =
|
|
575
|
-
raced === 'timeout'
|
|
576
|
-
? {
|
|
577
|
-
ok: false,
|
|
578
|
-
failure: 'agent',
|
|
579
|
-
error: `No result within ${timeoutMs} ms; the agent was interrupted.`,
|
|
580
|
-
}
|
|
581
|
-
: raced
|
|
582
|
-
if (raced === 'timeout') {
|
|
583
|
-
await open.running.interrupt().catch(() => undefined)
|
|
584
|
-
}
|
|
585
|
-
const session_id = open.summary.session_id ?? fallback_id
|
|
586
|
-
const session = {
|
|
587
|
-
...open.summary,
|
|
588
|
-
duration_ms: Date.now() - started,
|
|
589
|
-
}
|
|
590
|
-
const keep =
|
|
591
|
-
raced !== 'timeout' &&
|
|
592
|
-
open.ended === null &&
|
|
593
|
-
open.summary.session_id !== null
|
|
594
|
-
if (keep) keepOpen(open, session_id)
|
|
595
|
-
else await close(open)
|
|
596
|
-
return { ...outcome, session_id, session }
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const launch: AgentLauncher['launch'] = async ({
|
|
600
|
-
role,
|
|
601
|
-
ticket,
|
|
602
|
-
prompt,
|
|
603
|
-
cwd,
|
|
604
|
-
may_edit_tests,
|
|
605
|
-
config,
|
|
606
|
-
messaging,
|
|
607
|
-
}) => {
|
|
608
|
-
const refused = checkModel({ model: useModel })
|
|
609
|
-
if (refused !== null) return stop(refused)
|
|
610
|
-
const started = Date.now()
|
|
611
|
-
const fallback_id = `claude-launch-${randomUUID()}`
|
|
612
|
-
|
|
613
|
-
let claudePath: string
|
|
614
|
-
try {
|
|
615
|
-
claudePath = claude_path ?? realClaudePath()
|
|
616
|
-
} catch (error) {
|
|
617
|
-
return stop(errorText(error))
|
|
618
|
-
}
|
|
619
|
-
let worktree: string
|
|
620
|
-
let gitDir: string
|
|
621
|
-
try {
|
|
622
|
-
worktree = realpathSync(cwd)
|
|
623
|
-
gitDir = await commonGitDir({ cwd: worktree })
|
|
624
|
-
} catch (error) {
|
|
625
|
-
return { ok: false, failure: 'engine', error: errorText(error) }
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const summary: AgentSession = AgentSessionSchema.parse({})
|
|
629
|
-
const stderr: string[] = []
|
|
630
|
-
const options = agentOptions({
|
|
631
|
-
role,
|
|
632
|
-
may_edit_tests,
|
|
633
|
-
cwd: worktree,
|
|
634
|
-
model: useModel,
|
|
635
|
-
effort: useEffort,
|
|
636
|
-
claude_path: claudePath,
|
|
637
|
-
config,
|
|
638
|
-
common_git_dir: gitDir,
|
|
639
|
-
home: realpathSync(homedir()),
|
|
640
|
-
env: agentEnv({ source: process.env }),
|
|
641
|
-
guard_hook: createGuardHook({
|
|
642
|
-
role: guardRoleOf({ role }),
|
|
643
|
-
may_edit_tests,
|
|
644
|
-
worktree,
|
|
645
|
-
config,
|
|
646
|
-
// The summary is swapped each turn, so this reads it late.
|
|
647
|
-
on_deny: (denial) => open.summary.guard_denials.push(denial),
|
|
648
|
-
}),
|
|
649
|
-
// Kept in the session's options, so its follow-ups have them too.
|
|
650
|
-
luca_server:
|
|
651
|
-
messaging === null ? null : createLucaServer({ messaging }),
|
|
652
|
-
delivery_hook:
|
|
653
|
-
messaging === null ? null : createDeliveryHook({ messaging }),
|
|
654
|
-
stderr: (data) => {
|
|
655
|
-
stderr.push(data)
|
|
656
|
-
if (stderr.length > 50) stderr.shift()
|
|
657
|
-
},
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
const input = createInputChannel()
|
|
661
|
-
let running: AgentQuerySession
|
|
662
|
-
try {
|
|
663
|
-
running = startQuery({ prompt: input.iterable, options })
|
|
664
|
-
} catch (error) {
|
|
665
|
-
return {
|
|
666
|
-
ok: false,
|
|
667
|
-
failure: 'engine',
|
|
668
|
-
error: `The SDK did not start: ${errorText(error)}`,
|
|
669
|
-
session: { ...summary, duration_ms: Date.now() - started },
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
const open: OpenSession = {
|
|
673
|
-
role,
|
|
674
|
-
ticket,
|
|
675
|
-
cwd,
|
|
676
|
-
may_edit_tests,
|
|
677
|
-
running,
|
|
678
|
-
input,
|
|
679
|
-
summary,
|
|
680
|
-
model_totals: {},
|
|
681
|
-
waiter: null,
|
|
682
|
-
ended: null,
|
|
683
|
-
pump: Promise.resolve(),
|
|
684
|
-
idle_timer: undefined,
|
|
685
|
-
stderr,
|
|
686
|
-
}
|
|
687
|
-
open.pump = pumpMessages({ open, on_end: () => void close(open) })
|
|
688
|
-
|
|
689
|
-
const ended = async (outcome: Outcome): Promise<AgentTurn> => {
|
|
690
|
-
await close(open)
|
|
691
|
-
return {
|
|
692
|
-
...outcome,
|
|
693
|
-
session_id: open.summary.session_id ?? fallback_id,
|
|
694
|
-
session: { ...open.summary, duration_ms: Date.now() - started },
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
699
|
-
const timedOut = new Promise<'timeout'>((resolve) => {
|
|
700
|
-
timer = setTimeout(() => resolve('timeout'), timeoutMs)
|
|
701
|
-
})
|
|
702
|
-
const account = await Promise.race([
|
|
703
|
-
running.accountInfo().then(
|
|
704
|
-
(info) => ({ info }),
|
|
705
|
-
(error: unknown) => ({ error })
|
|
706
|
-
),
|
|
707
|
-
nextOutcome(open).then((outcome) => ({ outcome })),
|
|
708
|
-
timedOut,
|
|
709
|
-
])
|
|
710
|
-
clearTimeout(timer)
|
|
711
|
-
open.waiter = null
|
|
712
|
-
if (account === 'timeout') {
|
|
713
|
-
return ended({
|
|
714
|
-
ok: false,
|
|
715
|
-
failure: 'engine',
|
|
716
|
-
error: 'accountInfo() did not answer in time.',
|
|
717
|
-
})
|
|
718
|
-
}
|
|
719
|
-
if ('outcome' in account) return ended(account.outcome)
|
|
720
|
-
if ('error' in account) {
|
|
721
|
-
return ended({
|
|
722
|
-
ok: false,
|
|
723
|
-
failure: 'engine',
|
|
724
|
-
error: `accountInfo() failed: ${errorText(account.error)}`,
|
|
725
|
-
})
|
|
726
|
-
}
|
|
727
|
-
const accountParsed = AccountSchema.safeParse(account.info)
|
|
728
|
-
summary.subscription_type = accountParsed.success
|
|
729
|
-
? (accountParsed.data.subscriptionType ?? null)
|
|
730
|
-
: null
|
|
731
|
-
const problem = accountProblem(account.info)
|
|
732
|
-
if (problem !== null) return ended(stop(`credential check: ${problem}`))
|
|
733
|
-
return runTurn({ open, message: prompt, started, fallback_id })
|
|
734
|
-
}
|
|
83
|
+
const pool = createSessionPool({
|
|
84
|
+
turn_timeout_ms: timeoutMs,
|
|
85
|
+
idle_timeout_ms: idleMs,
|
|
86
|
+
})
|
|
87
|
+
const { sessions, close, startTurn, runTurn, closeAll } = pool
|
|
88
|
+
const launch = createLaunch({
|
|
89
|
+
effort: useEffort,
|
|
90
|
+
start_query: startQuery,
|
|
91
|
+
claude_path,
|
|
92
|
+
turn_timeout_ms: timeoutMs,
|
|
93
|
+
pool,
|
|
94
|
+
})
|
|
735
95
|
|
|
736
96
|
const followUp: AgentLauncher['followUp'] = async ({
|
|
737
97
|
session_id,
|
|
738
98
|
role,
|
|
739
99
|
ticket,
|
|
740
100
|
message,
|
|
101
|
+
on_progress,
|
|
741
102
|
}) => {
|
|
742
103
|
const open = sessions.get(session_id)
|
|
743
104
|
if (open === undefined || open.ended !== null) {
|
|
@@ -746,6 +107,7 @@ export const createClaudeLauncher = ({
|
|
|
746
107
|
ok: false,
|
|
747
108
|
failure: 'engine',
|
|
748
109
|
session_id,
|
|
110
|
+
session_gone: true,
|
|
749
111
|
error: `No open agent session ${session_id}: it closed or never started.`,
|
|
750
112
|
}
|
|
751
113
|
}
|
|
@@ -758,6 +120,7 @@ export const createClaudeLauncher = ({
|
|
|
758
120
|
}
|
|
759
121
|
}
|
|
760
122
|
startTurn(open)
|
|
123
|
+
open.progress.start(on_progress)
|
|
761
124
|
return runTurn({
|
|
762
125
|
open,
|
|
763
126
|
message,
|
|
@@ -773,9 +136,5 @@ export const createClaudeLauncher = ({
|
|
|
773
136
|
if (open !== undefined) await close(open)
|
|
774
137
|
}
|
|
775
138
|
|
|
776
|
-
const closeAll = async (): Promise<void> => {
|
|
777
|
-
await Promise.all([...new Set(sessions.values())].map(close))
|
|
778
|
-
}
|
|
779
|
-
|
|
780
139
|
return { launch, followUp, closeSession, closeAll }
|
|
781
140
|
}
|