@defrex/autobuild 0.8.0 → 0.10.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/README.md +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The orchestrator turn runner (AUT-342): a stateless agent loop over the
|
|
3
|
+
* operator tool registry. Every invocation reconstructs its whole
|
|
4
|
+
* conversation from durable state (`conversation.ts`), streams its output as
|
|
5
|
+
* protocol parts onto the turn's session-scoped stream, checkpoints at step
|
|
6
|
+
* boundaries via the stream sink's awaitable flush, and records its outcome
|
|
7
|
+
* as session facts. A turn that outlives one function invocation suspends on
|
|
8
|
+
* budget and resumes in a fresh invocation without losing a step; a tool in
|
|
9
|
+
* the approval list suspends the turn until the operator answers.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here encodes this repository's specifics.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
ToolLoopAgent,
|
|
15
|
+
convertToModelMessages,
|
|
16
|
+
isStepCount,
|
|
17
|
+
toUIMessageChunk,
|
|
18
|
+
type LanguageModel,
|
|
19
|
+
} from 'ai'
|
|
20
|
+
import { createSessionStreamSink } from '../store/streams/session-writer'
|
|
21
|
+
import type { StreamPart } from '../store/streams/types'
|
|
22
|
+
import type { Config } from '../config/schema'
|
|
23
|
+
import { ORCHESTRATOR_ROUTE_LIMIT_SECONDS } from '../config/schema'
|
|
24
|
+
import type { OperatorToolRegistry } from '../operator/registry'
|
|
25
|
+
import { agentActor, type Via } from '../events/envelope'
|
|
26
|
+
import type { SessionEventEnvelope, SessionEventWrite, SessionEventType } from '../events/sessions'
|
|
27
|
+
import type { SessionTurnTrigger } from '../store/session-reducer'
|
|
28
|
+
import { reduceSession } from '../store/session-reducer'
|
|
29
|
+
import { validateExpectedSeq, type BuildStore, type Clock } from '../store/types'
|
|
30
|
+
import type { IdSource } from '../ids'
|
|
31
|
+
import { reconstructConversation } from './conversation'
|
|
32
|
+
import { buildTurnSystemPrompt } from './prompt'
|
|
33
|
+
import { registryTools } from './tools'
|
|
34
|
+
import { approvalConfiguration, parseApprovalEntries } from './approvals'
|
|
35
|
+
import { classifyModelError, resolveGatewayModel, type TurnFailureKind } from './model'
|
|
36
|
+
export type { TurnFailureKind }
|
|
37
|
+
|
|
38
|
+
/** Hard cap on tool-loop steps per invocation — a runaway loop stops here
|
|
39
|
+
* even when the budget condition somehow never fires. */
|
|
40
|
+
export const ORCHESTRATOR_MAX_STEPS = 32
|
|
41
|
+
|
|
42
|
+
/** Seconds of margin the budget condition reserves for the final durable
|
|
43
|
+
* flush and outcome writes: the stop condition fires slightly before the
|
|
44
|
+
* invocation deadline so the suspension lands durably. */
|
|
45
|
+
export const ORCHESTRATOR_FLUSH_MARGIN_SECONDS = 5
|
|
46
|
+
|
|
47
|
+
/** The wake input a wake trigger delivers: the attention event record and —
|
|
48
|
+
* for a build wake — the build's reduced state, frozen at wake time. A
|
|
49
|
+
* journal wake omits `buildState`. Embedded in the `turn.started` payload so
|
|
50
|
+
* reconstruction is byte-identical across invocations even though live build
|
|
51
|
+
* state keeps moving. */
|
|
52
|
+
export interface WakeTurnInput {
|
|
53
|
+
event: { seq: number; ts: string; type: string; payload: unknown }
|
|
54
|
+
buildState?: unknown
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TurnOutcome {
|
|
58
|
+
kind: 'completed' | 'suspended' | 'failed'
|
|
59
|
+
cause?: 'budget' | 'approval'
|
|
60
|
+
failureKind?: TurnFailureKind
|
|
61
|
+
usage?: { inputTokens: number; outputTokens: number; steps: number }
|
|
62
|
+
error?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface StartTurnOptions {
|
|
66
|
+
/** Remaining invocation budget in seconds (the tick's), clamping the
|
|
67
|
+
* configured `invocationBudgetSeconds` for this run. The tick recomputes
|
|
68
|
+
* it per session so every turn draws down the same deadline. */
|
|
69
|
+
remainingBudgetSeconds?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StartTurnResult {
|
|
73
|
+
/** False when the session was not idle (or the CAS lost the race) — no
|
|
74
|
+
* turn was started and no loop is running. */
|
|
75
|
+
started: boolean
|
|
76
|
+
turn?: string
|
|
77
|
+
stream?: string
|
|
78
|
+
/** The running loop's outcome. The caller decides to await it (the tick)
|
|
79
|
+
* or background it (the operator route via `after()`); the promise starts
|
|
80
|
+
* executing immediately either way. */
|
|
81
|
+
outcome?: Promise<TurnOutcome>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ResumeTurnOptions {
|
|
85
|
+
/** When the turn was suspended for approval and the operator has answered:
|
|
86
|
+
* the decision and the answered tool call id. The runner recovers the
|
|
87
|
+
* request's wire `approvalId` from the open stream and appends the
|
|
88
|
+
* protocol's `tool-approval-response` part so the SDK's resume path
|
|
89
|
+
* executes (approve) or synthesizes the denied output (deny). */
|
|
90
|
+
approval?: { decision: 'approve' | 'deny'; toolCallId: string }
|
|
91
|
+
/** Remaining invocation budget in seconds (the tick's), clamping the
|
|
92
|
+
* configured `invocationBudgetSeconds` for this run. */
|
|
93
|
+
remainingBudgetSeconds?: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ResumeTurnResult {
|
|
97
|
+
resumed: boolean
|
|
98
|
+
outcome?: Promise<TurnOutcome>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface OrchestratorTurnRunner {
|
|
102
|
+
startTurn(
|
|
103
|
+
sessionId: string,
|
|
104
|
+
trigger: SessionTurnTrigger,
|
|
105
|
+
wake?: WakeTurnInput,
|
|
106
|
+
opts?: StartTurnOptions,
|
|
107
|
+
): Promise<StartTurnResult>
|
|
108
|
+
resumeTurn(sessionId: string, opts?: ResumeTurnOptions): Promise<ResumeTurnResult>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface OrchestratorRunnerOptions {
|
|
112
|
+
store: BuildStore
|
|
113
|
+
registry: OperatorToolRegistry
|
|
114
|
+
/** The repository this runner serves; the session's repository must match. */
|
|
115
|
+
repo: string
|
|
116
|
+
config: Config
|
|
117
|
+
clock: Clock
|
|
118
|
+
ids: IdSource
|
|
119
|
+
/** Injected language model; defaults to the configured gateway model. */
|
|
120
|
+
model?: LanguageModel
|
|
121
|
+
/** Remaining invocation budget in seconds (the tick's), clamping the
|
|
122
|
+
* configured `invocationBudgetSeconds` for turns this runner starts. */
|
|
123
|
+
remainingBudgetSeconds?: number
|
|
124
|
+
/** The agent's in-turn retry bound. Default 2 (the plan's bound); tests
|
|
125
|
+
* pass 0 to keep model-failure cases fast. */
|
|
126
|
+
maxRetries?: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The agent actor every turn fact carries. */
|
|
130
|
+
function turnActor(turn: string) {
|
|
131
|
+
return agentActor('orchestrator', turn)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The session's reduced state and its event log's last sequence — the CAS
|
|
135
|
+
* expectation for the runner's own appends. */
|
|
136
|
+
async function reduceSessionState(
|
|
137
|
+
store: BuildStore,
|
|
138
|
+
sessionId: string,
|
|
139
|
+
): Promise<{ state: ReturnType<typeof reduceSession>; lastSeq: number }> {
|
|
140
|
+
const events = await store.getSessionEvents(sessionId)
|
|
141
|
+
const state = reduceSession(events)
|
|
142
|
+
const lastSeq = events.length > 0 ? (events[events.length - 1]?.seq ?? 0) : 0
|
|
143
|
+
return { state, lastSeq }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createOrchestratorTurnRunner(
|
|
147
|
+
options: OrchestratorRunnerOptions,
|
|
148
|
+
): OrchestratorTurnRunner {
|
|
149
|
+
const { store, registry, repo, config, clock, ids } = options
|
|
150
|
+
const approvals = parseApprovalEntries(config.orchestrator.approvals)
|
|
151
|
+
const approvalConfig = approvalConfiguration(approvals)
|
|
152
|
+
const budgetSeconds = Math.min(
|
|
153
|
+
config.orchestrator.invocationBudgetSeconds,
|
|
154
|
+
ORCHESTRATOR_ROUTE_LIMIT_SECONDS,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
function modelFor(): LanguageModel {
|
|
158
|
+
return options.model ?? resolveGatewayModel(config.orchestrator.model ?? '')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The invocation deadline: min(configured budget, caller-supplied
|
|
162
|
+
* remaining budget) in wall-clock milliseconds from now. */
|
|
163
|
+
function deadlineMs(remainingBudgetSeconds?: number): number {
|
|
164
|
+
const effective =
|
|
165
|
+
remainingBudgetSeconds === undefined
|
|
166
|
+
? budgetSeconds
|
|
167
|
+
: Math.max(0, Math.min(budgetSeconds, remainingBudgetSeconds))
|
|
168
|
+
return clock().getTime() + effective * 1000
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Append one agent session fact; the store validates and sequences it. */
|
|
172
|
+
function append<T extends SessionEventType>(
|
|
173
|
+
sessionId: string,
|
|
174
|
+
turn: string,
|
|
175
|
+
type: T,
|
|
176
|
+
payload: SessionEventWrite<T>['payload'],
|
|
177
|
+
): Promise<SessionEventEnvelope<T>> {
|
|
178
|
+
return store.appendSessionEvent(sessionId, {
|
|
179
|
+
actor: turnActor(turn),
|
|
180
|
+
type,
|
|
181
|
+
payload,
|
|
182
|
+
}) as Promise<SessionEventEnvelope<T>>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── The agent loop ────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
async function runLoop(
|
|
188
|
+
sessionId: string,
|
|
189
|
+
operator: string,
|
|
190
|
+
turn: string,
|
|
191
|
+
streamId: string,
|
|
192
|
+
deadline: number,
|
|
193
|
+
): Promise<TurnOutcome> {
|
|
194
|
+
const sink = createSessionStreamSink({
|
|
195
|
+
store,
|
|
196
|
+
scope: { kind: 'session', session: sessionId },
|
|
197
|
+
})
|
|
198
|
+
await sink.resume(streamId)
|
|
199
|
+
|
|
200
|
+
// Durable reconstruction — identical model input across invocations.
|
|
201
|
+
const events = await store.getSessionEvents(sessionId)
|
|
202
|
+
const conversation = await reconstructConversation(store, sessionId, events)
|
|
203
|
+
const state = reduceSession(events)
|
|
204
|
+
const system = await buildTurnSystemPrompt({
|
|
205
|
+
store,
|
|
206
|
+
repo,
|
|
207
|
+
wakeGlobs: state.wakeGlobs,
|
|
208
|
+
config: config.orchestrator,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Tool-call inputs, remembered as their `tool-input-available` parts pass
|
|
212
|
+
// so an approval request can name what the model asked to run.
|
|
213
|
+
const toolInputs = new Map<string, { toolName: string; input: unknown }>()
|
|
214
|
+
let approvalRequest: { toolCallId: string; toolName: string; input: unknown } | undefined
|
|
215
|
+
let budgetFired = false
|
|
216
|
+
|
|
217
|
+
const via: Via = { kind: 'session', id: sessionId }
|
|
218
|
+
const tools = registryTools({ registry, repo, operator, via })
|
|
219
|
+
const agent = new ToolLoopAgent({
|
|
220
|
+
model: modelFor(),
|
|
221
|
+
instructions: system,
|
|
222
|
+
tools,
|
|
223
|
+
toolApproval: approvalConfig,
|
|
224
|
+
maxRetries: options.maxRetries ?? 2,
|
|
225
|
+
stopWhen: [
|
|
226
|
+
(_loopState) => {
|
|
227
|
+
if (clock().getTime() >= deadline - ORCHESTRATOR_FLUSH_MARGIN_SECONDS * 1000) {
|
|
228
|
+
budgetFired = true
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
return false
|
|
232
|
+
},
|
|
233
|
+
isStepCount(ORCHESTRATOR_MAX_STEPS),
|
|
234
|
+
],
|
|
235
|
+
onStepEnd: async () => {
|
|
236
|
+
// The checkpoint: everything streamed so far is durable before the
|
|
237
|
+
// next step starts.
|
|
238
|
+
await sink.flush()
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const abortController = new AbortController()
|
|
243
|
+
const abortTimer = setTimeout(
|
|
244
|
+
() => abortController.abort(new Error('orchestrator invocation deadline exceeded')),
|
|
245
|
+
Math.max(0, deadline - clock().getTime()),
|
|
246
|
+
)
|
|
247
|
+
abortTimer.unref?.()
|
|
248
|
+
|
|
249
|
+
// The SDK surfaces model failures as `error` parts on the full stream
|
|
250
|
+
// (never a thrown error), carrying the ORIGINAL error object — the UI
|
|
251
|
+
// chunk reduces it to a generic errorText, so the loop consumes the raw
|
|
252
|
+
// stream and maps each part through `toUIMessageChunk` itself: typed
|
|
253
|
+
// classification sees the real error class, the sink still stores
|
|
254
|
+
// protocol chunks.
|
|
255
|
+
try {
|
|
256
|
+
const result = await agent.stream({
|
|
257
|
+
messages: await convertToModelMessages(conversation.messages),
|
|
258
|
+
abortSignal: abortController.signal,
|
|
259
|
+
})
|
|
260
|
+
for await (const part of result.stream) {
|
|
261
|
+
if (part.type === 'error') {
|
|
262
|
+
throw part.error
|
|
263
|
+
}
|
|
264
|
+
const chunk = toUIMessageChunk(part as never, { tools: tools as never })
|
|
265
|
+
if (chunk === undefined) continue
|
|
266
|
+
sink.append([chunk as unknown as StreamPart])
|
|
267
|
+
if (chunk.type === 'tool-input-available') {
|
|
268
|
+
toolInputs.set(chunk.toolCallId, {
|
|
269
|
+
toolName: chunk.toolName as string,
|
|
270
|
+
input: chunk.input,
|
|
271
|
+
})
|
|
272
|
+
} else if (chunk.type === 'tool-approval-request') {
|
|
273
|
+
const known = toolInputs.get(chunk.toolCallId)
|
|
274
|
+
approvalRequest = {
|
|
275
|
+
toolCallId: chunk.toolCallId,
|
|
276
|
+
toolName: known?.toolName ?? 'unknown',
|
|
277
|
+
input: known?.input ?? {},
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
clearTimeout(abortTimer)
|
|
282
|
+
|
|
283
|
+
// An approval request suspends the turn for the operator; a budget stop
|
|
284
|
+
// suspends it for the next invocation; otherwise the turn completed.
|
|
285
|
+
if (approvalRequest !== undefined) {
|
|
286
|
+
await append(sessionId, turn, 'approval.requested', {
|
|
287
|
+
turn,
|
|
288
|
+
toolCallId: approvalRequest.toolCallId,
|
|
289
|
+
toolName: approvalRequest.toolName,
|
|
290
|
+
input: approvalRequest.input as Record<string, unknown>,
|
|
291
|
+
})
|
|
292
|
+
await append(sessionId, turn, 'turn.suspended', { turn, cause: 'approval' })
|
|
293
|
+
return { kind: 'suspended', cause: 'approval' }
|
|
294
|
+
}
|
|
295
|
+
if (budgetFired || abortController.signal.aborted) {
|
|
296
|
+
await append(sessionId, turn, 'turn.suspended', { turn, cause: 'budget' })
|
|
297
|
+
return { kind: 'suspended', cause: 'budget' }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const totalUsage = await result.totalUsage
|
|
301
|
+
const steps = await result.steps
|
|
302
|
+
const usage = {
|
|
303
|
+
inputTokens: totalUsage.inputTokens ?? 0,
|
|
304
|
+
outputTokens: totalUsage.outputTokens ?? 0,
|
|
305
|
+
steps: steps.length,
|
|
306
|
+
}
|
|
307
|
+
await append(sessionId, turn, 'turn.completed', { turn, usage })
|
|
308
|
+
await sink.close('completed')
|
|
309
|
+
return { kind: 'completed', usage }
|
|
310
|
+
} catch (error) {
|
|
311
|
+
clearTimeout(abortTimer)
|
|
312
|
+
if (abortController.signal.aborted) {
|
|
313
|
+
// A runaway step interrupted at the deadline: the durable state so
|
|
314
|
+
// far is the checkpoint; the next invocation resumes it.
|
|
315
|
+
await append(sessionId, turn, 'turn.suspended', { turn, cause: 'budget' })
|
|
316
|
+
return { kind: 'suspended', cause: 'budget' }
|
|
317
|
+
}
|
|
318
|
+
const kind = classifyModelError(error)
|
|
319
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
320
|
+
await append(sessionId, turn, 'turn.failed', { turn, kind, error: message })
|
|
321
|
+
await sink.close('aborted')
|
|
322
|
+
return { kind: 'failed', failureKind: kind, error: message }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
async startTurn(sessionId, trigger, wake, opts = {}) {
|
|
328
|
+
const { state, lastSeq } = await reduceSessionState(store, sessionId)
|
|
329
|
+
if (state.status !== 'idle') return { started: false }
|
|
330
|
+
// Sessions with empty wake settings are never woken.
|
|
331
|
+
if (trigger.kind === 'wake' && state.wakeGlobs.length === 0) return { started: false }
|
|
332
|
+
|
|
333
|
+
const turn = ids('ot')
|
|
334
|
+
// The stream is created before the CAS because the fact must name its
|
|
335
|
+
// id; on a miss the just-created stream is closed aborted so a losing
|
|
336
|
+
// double-click cannot leak an open stream no turn ever references (the
|
|
337
|
+
// reaper only watches turns, so an orphan would otherwise live forever).
|
|
338
|
+
const stream = await store.createStream(
|
|
339
|
+
{ kind: 'session', session: sessionId },
|
|
340
|
+
`turn:${turn}`,
|
|
341
|
+
)
|
|
342
|
+
const payload: Record<string, unknown> = { turn, stream: stream.id, trigger }
|
|
343
|
+
if (wake !== undefined) payload.wake = wake
|
|
344
|
+
|
|
345
|
+
validateExpectedSeq(lastSeq)
|
|
346
|
+
const appended = await store
|
|
347
|
+
.appendSessionEventIfCurrent(sessionId, lastSeq, {
|
|
348
|
+
actor: turnActor(turn),
|
|
349
|
+
type: 'turn.started',
|
|
350
|
+
payload,
|
|
351
|
+
} as unknown as SessionEventWrite<'turn.started'>)
|
|
352
|
+
.catch(() => null)
|
|
353
|
+
if (appended === null) {
|
|
354
|
+
await store.closeStream(stream.id, 'aborted').catch(() => undefined)
|
|
355
|
+
return { started: false }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const record = await store.getSession(sessionId)
|
|
359
|
+
const outcome = runLoop(
|
|
360
|
+
sessionId,
|
|
361
|
+
record?.operator ?? '',
|
|
362
|
+
turn,
|
|
363
|
+
stream.id,
|
|
364
|
+
deadlineMs(opts.remainingBudgetSeconds ?? options.remainingBudgetSeconds),
|
|
365
|
+
)
|
|
366
|
+
return { started: true, turn, stream: stream.id, outcome }
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
async resumeTurn(sessionId, opts = {}) {
|
|
370
|
+
const { state, lastSeq } = await reduceSessionState(store, sessionId)
|
|
371
|
+
if (state.status !== 'suspended') return { resumed: false }
|
|
372
|
+
if (state.pendingApproval !== undefined) return { resumed: false }
|
|
373
|
+
const open = state.openTurn
|
|
374
|
+
if (open === undefined) return { resumed: false }
|
|
375
|
+
|
|
376
|
+
if (opts.approval !== undefined) {
|
|
377
|
+
const recovered = await recoverApprovalResponse(store, open.stream, opts.approval)
|
|
378
|
+
if (recovered === null) {
|
|
379
|
+
// A missing request part (e.g. dropped by assembly) fails the
|
|
380
|
+
// resume with a typed failure rather than proceeding without it.
|
|
381
|
+
await append(sessionId, open.turn, 'turn.failed', {
|
|
382
|
+
turn: open.turn,
|
|
383
|
+
kind: 'internal',
|
|
384
|
+
error: `approval answer for tool call ${JSON.stringify(opts.approval.toolCallId)} matches no persisted approval request`,
|
|
385
|
+
})
|
|
386
|
+
// The failed resume is terminal, so the turn's stream closes here
|
|
387
|
+
// like every other terminal path — the reaper only watches running
|
|
388
|
+
// turns and would otherwise never close it. Closing an
|
|
389
|
+
// already-closed stream is a contract-pinned no-op, so the
|
|
390
|
+
// non-open-stream leg of recoverApprovalResponse's null is covered
|
|
391
|
+
// too.
|
|
392
|
+
await store.closeStream(open.stream, 'aborted').catch(() => undefined)
|
|
393
|
+
return { resumed: false }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
validateExpectedSeq(lastSeq)
|
|
398
|
+
const appended = await store
|
|
399
|
+
.appendSessionEventIfCurrent(sessionId, lastSeq, {
|
|
400
|
+
actor: turnActor(open.turn),
|
|
401
|
+
type: 'turn.resumed',
|
|
402
|
+
payload: { turn: open.turn },
|
|
403
|
+
} as unknown as SessionEventWrite<'turn.resumed'>)
|
|
404
|
+
.catch(() => null)
|
|
405
|
+
if (appended === null) return { resumed: false }
|
|
406
|
+
|
|
407
|
+
const record = await store.getSession(sessionId)
|
|
408
|
+
const outcome = runLoop(
|
|
409
|
+
sessionId,
|
|
410
|
+
record?.operator ?? '',
|
|
411
|
+
open.turn,
|
|
412
|
+
open.stream,
|
|
413
|
+
deadlineMs(opts.remainingBudgetSeconds),
|
|
414
|
+
)
|
|
415
|
+
return { resumed: true, outcome }
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Scan the open turn's stream for the `tool-approval-request` part whose
|
|
421
|
+
* top-level `toolCallId` matches the answered call, and append the protocol's
|
|
422
|
+
* `tool-approval-response` part. The persisted parts are UI message stream
|
|
423
|
+
* chunks — the SDK flattens the internal approval request into a chunk with
|
|
424
|
+
* the call id at the top level, and no `toolCall` property exists on the
|
|
425
|
+
* stored chunk — and neither `approval.requested` nor the answer event
|
|
426
|
+
* carries the wire `approvalId`, so the stream is its durable home. A deny
|
|
427
|
+
* response makes the SDK synthesize the denied tool output. Returns null
|
|
428
|
+
* (appending nothing) when no request part matches. */
|
|
429
|
+
async function recoverApprovalResponse(
|
|
430
|
+
store: BuildStore,
|
|
431
|
+
streamId: string,
|
|
432
|
+
answer: { decision: 'approve' | 'deny'; toolCallId: string },
|
|
433
|
+
): Promise<{ approvalId: string } | null> {
|
|
434
|
+
const read = await store.readStream(streamId)
|
|
435
|
+
if (read.status !== 'open') return null
|
|
436
|
+
let approvalId: string | undefined
|
|
437
|
+
for (const chunk of read.chunks) {
|
|
438
|
+
for (const part of chunk.parts) {
|
|
439
|
+
if (
|
|
440
|
+
part.type === 'tool-approval-request' &&
|
|
441
|
+
part.toolCallId === answer.toolCallId &&
|
|
442
|
+
typeof part.approvalId === 'string'
|
|
443
|
+
) {
|
|
444
|
+
approvalId = part.approvalId
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (approvalId === undefined) return null
|
|
449
|
+
await store.appendStreamParts(streamId, [
|
|
450
|
+
{
|
|
451
|
+
type: 'tool-approval-response',
|
|
452
|
+
approvalId,
|
|
453
|
+
approved: answer.decision === 'approve',
|
|
454
|
+
},
|
|
455
|
+
])
|
|
456
|
+
return { approvalId }
|
|
457
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export type * from '../ports/types'
|
|
7
7
|
export type * from '../ports/workspace/build-execution'
|
|
8
8
|
export type * from '../store/types'
|
|
9
|
+
export * from '../store/session-reducer'
|
|
9
10
|
export type {
|
|
10
11
|
RuntimeRegistration,
|
|
11
12
|
RuntimeRegistry,
|
|
@@ -77,9 +78,30 @@ export type {
|
|
|
77
78
|
TicketSourcePluginDescriptor,
|
|
78
79
|
TicketSourcePluginFactory,
|
|
79
80
|
TicketSourcePluginRegistration,
|
|
81
|
+
WorkspaceProviderPluginDescriptor,
|
|
80
82
|
WorkspaceProviderPluginFactory,
|
|
83
|
+
WorkspaceProviderPluginFactoryContext,
|
|
81
84
|
WorkspaceProviderPluginRegistration,
|
|
82
85
|
} from '../plugins/manifest'
|
|
86
|
+
export type {
|
|
87
|
+
GuestProbeReport,
|
|
88
|
+
InitValidationReport,
|
|
89
|
+
ReadinessCheck,
|
|
90
|
+
RuntimeReferencesSource,
|
|
91
|
+
WorkspaceProviderCapabilities,
|
|
92
|
+
WorkspaceProviderEnvRequirement,
|
|
93
|
+
WorkspaceReadinessContext,
|
|
94
|
+
} from '../ports/workspace/provider-capabilities'
|
|
95
|
+
// The runtime-route group shape behind `RuntimeReferencesSource`, so plugin
|
|
96
|
+
// authors can name it without reaching for an internal path.
|
|
97
|
+
export type { RuntimeReferenceGroup } from '../config/roles'
|
|
98
|
+
|
|
99
|
+
/** The single shared capability object for the `vercel-sandbox` provider
|
|
100
|
+
* (AUT-517): the builtin registration and the
|
|
101
|
+
* `@defrex/autobuild-vercel-sandbox` plugin reference the same runtime value,
|
|
102
|
+
* so their declarations cannot drift. The one runtime value a provider plugin
|
|
103
|
+
* needs from the SDK. */
|
|
104
|
+
export { VERCEL_SANDBOX_CAPABILITIES } from '../ports/workspace/vercel-capabilities'
|
|
83
105
|
|
|
84
106
|
export {
|
|
85
107
|
describeTicketSourceContract,
|
|
@@ -131,7 +153,14 @@ export { FakeTicketSource } from '../ports/tickets/fake'
|
|
|
131
153
|
export { validateTicketUpdate } from '../ports/tickets/update'
|
|
132
154
|
export { FakeWorkspaceProvider } from '../ports/workspace/fake'
|
|
133
155
|
export { FakeForge } from '../ports/forge/fake'
|
|
134
|
-
|
|
156
|
+
// ScriptContext + defaultTurnResult are the scripted fake's own API: the
|
|
157
|
+
// script callback's context type and its default turn result (needed by the
|
|
158
|
+
// hosted-dispatcher integration test to script guest agents).
|
|
159
|
+
export {
|
|
160
|
+
defaultTurnResult,
|
|
161
|
+
ScriptedAgentRunner,
|
|
162
|
+
type ScriptContext,
|
|
163
|
+
} from '../ports/runner/fake'
|
|
135
164
|
export { MemoryBlobStore, MemoryBuildStore } from '../store/memory'
|
|
136
165
|
|
|
137
166
|
// Runtime primitives needed by out-of-tree BuildStore adapters.
|
|
@@ -172,3 +201,24 @@ export type {
|
|
|
172
201
|
StreamStatus,
|
|
173
202
|
} from '../store/streams/types'
|
|
174
203
|
export { contentHash, systemClock, toBytes, validateExpectedSeq } from '../store/types'
|
|
204
|
+
export {
|
|
205
|
+
DEFAULT_TICKET_ASSET_LIMITS,
|
|
206
|
+
effectiveTicketAssetLimits,
|
|
207
|
+
summarizeTicketAsset,
|
|
208
|
+
TICKET_ASSET_MAX_BYTES,
|
|
209
|
+
TICKET_ASSET_MAX_ENTRIES,
|
|
210
|
+
TicketAssetValidationError,
|
|
211
|
+
validateTicketAssetInput,
|
|
212
|
+
validateTicketAssetStructure,
|
|
213
|
+
} from '../store/ticket-assets'
|
|
214
|
+
export type {
|
|
215
|
+
TicketAsset,
|
|
216
|
+
TicketAssetContentEntry,
|
|
217
|
+
TicketAssetEntry,
|
|
218
|
+
TicketAssetEntryInput,
|
|
219
|
+
TicketAssetInput,
|
|
220
|
+
TicketAssetLayout,
|
|
221
|
+
TicketAssetLimits,
|
|
222
|
+
TicketAssetMeta,
|
|
223
|
+
TicketAssetSummary,
|
|
224
|
+
} from '../store/ticket-assets'
|