@defrex/autobuild 0.8.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 +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { delimiter, join } from 'node:path'
|
|
5
|
+
import type { StreamPart } from '../../store/streams/types'
|
|
6
|
+
import type { AgentRunner, AgentSessionHandle, AgentTurnResult, Transcript } from '../types'
|
|
7
|
+
import type { OneShotCompletion, OneShotCompletionInput } from './one-shot'
|
|
8
|
+
import { AGENT_BIN_DIR } from './session-env'
|
|
9
|
+
|
|
10
|
+
export type AgentRunnerContractScenario =
|
|
11
|
+
| 'success'
|
|
12
|
+
| 'retryable-failure'
|
|
13
|
+
| 'permanent-failure'
|
|
14
|
+
| 'exhaustion-failure'
|
|
15
|
+
| 'cancel-start'
|
|
16
|
+
| 'cancel-continue'
|
|
17
|
+
| 'stream-turn'
|
|
18
|
+
|
|
19
|
+
export const CONTRACT_INVOCATION = 'agent-runner-contract'
|
|
20
|
+
export const CONTRACT_SKILL = 'ab-runner-contract'
|
|
21
|
+
export const CONTRACT_FOLLOW_UP = 'contract follow-up turn'
|
|
22
|
+
export const CONTRACT_RETRYABLE_FAILURE = 'contract worker exited unexpectedly'
|
|
23
|
+
export const CONTRACT_PERMANENT_FAILURE = 'contract authentication failed'
|
|
24
|
+
export const CONTRACT_EXHAUSTION_FAILURE = 'contract usage quota exhausted'
|
|
25
|
+
export const CONTRACT_ONE_SHOT_PROMPT = 'contract one-shot prompt'
|
|
26
|
+
export const CONTRACT_ONE_SHOT_TEXT = 'contract-one-shot-result'
|
|
27
|
+
export const CONTRACT_STREAM_TOOL = 'contract-tool'
|
|
28
|
+
|
|
29
|
+
/** An observation made below the adapter boundary. Contract fixtures normalize
|
|
30
|
+
* their SDK-specific calls to this shape so the suite can verify delivery and
|
|
31
|
+
* effective process environment without reaching into an adapter. */
|
|
32
|
+
export interface AgentRunnerContractTurnObservation {
|
|
33
|
+
/** Undefined for start; the exact follow-up string for continue. */
|
|
34
|
+
message?: string
|
|
35
|
+
env: Record<string, string>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentRunnerContractOneShotObservation {
|
|
39
|
+
prompt: string
|
|
40
|
+
cwd: string
|
|
41
|
+
env: Record<string, string>
|
|
42
|
+
model?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AgentRunnerContractOneShotHarness {
|
|
46
|
+
completion: OneShotCompletion
|
|
47
|
+
observation: () => AgentRunnerContractOneShotObservation | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AgentRunnerContractHarness {
|
|
51
|
+
runner: AgentRunner
|
|
52
|
+
/** A valid, explicit model identifier for this adapter. */
|
|
53
|
+
model: string
|
|
54
|
+
workspacePath: string
|
|
55
|
+
/** Independent observations from the script/SDK seam, in turn order. */
|
|
56
|
+
turns: () => readonly AgentRunnerContractTurnObservation[]
|
|
57
|
+
/** Adapter-specific disposal observation, when the transport exposes one. */
|
|
58
|
+
disposed?: () => number
|
|
59
|
+
/** Present only when the runtime declares the optional capability. */
|
|
60
|
+
oneShot?: AgentRunnerContractOneShotHarness
|
|
61
|
+
/** Stream-part observation (SPEC §9): every part the adapter appended via
|
|
62
|
+
* the per-turn emitter, across turns, in order. Present only when the
|
|
63
|
+
* offline fake supports streaming. */
|
|
64
|
+
stream?: () => readonly StreamPart[]
|
|
65
|
+
cleanup?: () => Promise<void>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type AgentRunnerContractFactory = (
|
|
69
|
+
scenario: AgentRunnerContractScenario,
|
|
70
|
+
) => Promise<AgentRunnerContractHarness> | AgentRunnerContractHarness
|
|
71
|
+
|
|
72
|
+
async function withHarness(
|
|
73
|
+
factory: AgentRunnerContractFactory,
|
|
74
|
+
scenario: AgentRunnerContractScenario,
|
|
75
|
+
run: (harness: AgentRunnerContractHarness) => Promise<void>,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const harness = await factory(scenario)
|
|
78
|
+
let failure: unknown
|
|
79
|
+
try {
|
|
80
|
+
await run(harness)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
failure = error
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await harness.cleanup?.()
|
|
87
|
+
} catch (cleanupError) {
|
|
88
|
+
if (failure !== undefined) {
|
|
89
|
+
throw new AggregateError(
|
|
90
|
+
[failure, cleanupError],
|
|
91
|
+
'AgentRunner contract assertion and cleanup both failed',
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
throw cleanupError
|
|
95
|
+
}
|
|
96
|
+
if (failure !== undefined) throw failure
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Run assertions while guaranteeing that every successfully launched handle
|
|
100
|
+
* is ended. Assertion and end failures are both retained. */
|
|
101
|
+
async function withEndedSession<T>(
|
|
102
|
+
runner: AgentRunner,
|
|
103
|
+
session: AgentSessionHandle,
|
|
104
|
+
run: () => Promise<T>,
|
|
105
|
+
): Promise<{ value: T; transcript: Transcript }> {
|
|
106
|
+
let value: T | undefined
|
|
107
|
+
let failure: unknown
|
|
108
|
+
try {
|
|
109
|
+
value = await run()
|
|
110
|
+
} catch (error) {
|
|
111
|
+
failure = error
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let transcript: Transcript | undefined
|
|
115
|
+
try {
|
|
116
|
+
transcript = await runner.end(session)
|
|
117
|
+
} catch (endError) {
|
|
118
|
+
if (failure !== undefined) {
|
|
119
|
+
throw new AggregateError(
|
|
120
|
+
[failure, endError],
|
|
121
|
+
'AgentRunner contract assertion and session end both failed',
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
throw endError
|
|
125
|
+
}
|
|
126
|
+
if (failure !== undefined) throw failure
|
|
127
|
+
return { value: value as T, transcript }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function expectUsage(usage: { inputTokens: number; outputTokens: number; turns: number }): void {
|
|
131
|
+
for (const value of [usage.inputTokens, usage.outputTokens, usage.turns]) {
|
|
132
|
+
expect(Number.isFinite(value)).toBe(true)
|
|
133
|
+
expect(Number.isInteger(value)).toBe(true)
|
|
134
|
+
expect(value).toBeGreaterThanOrEqual(0)
|
|
135
|
+
}
|
|
136
|
+
expect(usage.turns).toBeGreaterThan(0)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function expectTypedCompleted(result: AgentTurnResult): void {
|
|
140
|
+
expect(result.kind).toBe('completed')
|
|
141
|
+
expect(typeof result.text).toBe('string')
|
|
142
|
+
expectUsage(result.usage)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function expectTranscript(transcript: Transcript, runner: AgentRunner, model: string): void {
|
|
146
|
+
expect(transcript.content.trim().length).toBeGreaterThan(0)
|
|
147
|
+
expect(transcript.metadata.runner).toBe(runner.name)
|
|
148
|
+
expect(transcript.metadata.model).toBe(model)
|
|
149
|
+
expectUsage(transcript.metadata.usage)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function waitForTurn(harness: AgentRunnerContractHarness, count: number): Promise<void> {
|
|
153
|
+
const deadline = Date.now() + 1000
|
|
154
|
+
while (harness.turns().length < count) {
|
|
155
|
+
if (Date.now() >= deadline)
|
|
156
|
+
throw new Error(`runner did not start turn ${count} within one second`)
|
|
157
|
+
await Bun.sleep(1)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function invokeManagedCli(env: Record<string, string>): Promise<void> {
|
|
162
|
+
const path = env.PATH
|
|
163
|
+
expect(path).toBeDefined()
|
|
164
|
+
expect(path!.split(delimiter)[0]).toBe(AGENT_BIN_DIR)
|
|
165
|
+
|
|
166
|
+
const proc = Bun.spawn(['ab', '--help'], {
|
|
167
|
+
cwd: process.cwd(),
|
|
168
|
+
env,
|
|
169
|
+
stdout: 'pipe',
|
|
170
|
+
stderr: 'pipe',
|
|
171
|
+
})
|
|
172
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
173
|
+
new Response(proc.stdout).text(),
|
|
174
|
+
new Response(proc.stderr).text(),
|
|
175
|
+
proc.exited,
|
|
176
|
+
])
|
|
177
|
+
expect(exitCode).toBe(0)
|
|
178
|
+
expect(stderr).toBe('')
|
|
179
|
+
expect(stdout).toContain('ab — agent-driven software delivery')
|
|
180
|
+
expect(stdout).not.toContain('host-conflicting-ab')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reusable AgentRunner semantics. Runtime transport, stream extraction,
|
|
185
|
+
* resumption mechanics, and disposal tests remain beside each adapter; this is
|
|
186
|
+
* the behavioral floor a future runtime plugin can run unchanged.
|
|
187
|
+
*/
|
|
188
|
+
export function describeAgentRunnerContract(
|
|
189
|
+
name: string,
|
|
190
|
+
factory: AgentRunnerContractFactory,
|
|
191
|
+
): void {
|
|
192
|
+
describe(`AgentRunner contract: ${name}`, () => {
|
|
193
|
+
test('start/continue are typed, refresh ambient auth, resolve managed ab, and end with a complete transcript', async () => {
|
|
194
|
+
await withHarness(factory, 'success', async (harness) => {
|
|
195
|
+
const conflictDir = await mkdtemp(join(tmpdir(), 'ab-runner-contract-path-'))
|
|
196
|
+
try {
|
|
197
|
+
const conflictingAb = join(conflictDir, 'ab')
|
|
198
|
+
await writeFile(conflictingAb, '#!/bin/sh\necho host-conflicting-ab\nexit 91\n')
|
|
199
|
+
await chmod(conflictingAb, 0o755)
|
|
200
|
+
|
|
201
|
+
const startEnv = {
|
|
202
|
+
PATH: [conflictDir, process.env.PATH ?? '']
|
|
203
|
+
.filter((entry) => entry !== '')
|
|
204
|
+
.join(delimiter),
|
|
205
|
+
AB_CONTRACT_START_ONLY: 'retained',
|
|
206
|
+
AB_CONTRACT_COLLIDE: 'start-value',
|
|
207
|
+
AB_PHASE: 'implement@1',
|
|
208
|
+
}
|
|
209
|
+
const started = await harness.runner.start({
|
|
210
|
+
skill: CONTRACT_SKILL,
|
|
211
|
+
invocation: CONTRACT_INVOCATION,
|
|
212
|
+
workspacePath: harness.workspacePath,
|
|
213
|
+
model: harness.model,
|
|
214
|
+
env: startEnv,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const { transcript } = await withEndedSession(
|
|
218
|
+
harness.runner,
|
|
219
|
+
started.session,
|
|
220
|
+
async () => {
|
|
221
|
+
expect(started.session.id.trim().length).toBeGreaterThan(0)
|
|
222
|
+
expect(started.session.runner).toBe(harness.runner.name)
|
|
223
|
+
expect(started.session.model).toBe(harness.model)
|
|
224
|
+
expectTypedCompleted(started.result)
|
|
225
|
+
|
|
226
|
+
const continued = await harness.runner.continue(started.session, CONTRACT_FOLLOW_UP, {
|
|
227
|
+
env: {
|
|
228
|
+
AB_CONTRACT_COLLIDE: 'continue-value',
|
|
229
|
+
AB_CONTRACT_CONTINUE_ONLY: 'fresh',
|
|
230
|
+
AB_PHASE: 'implement@2',
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
expectTypedCompleted(continued)
|
|
234
|
+
|
|
235
|
+
const turns = harness.turns()
|
|
236
|
+
expect(turns).toHaveLength(2)
|
|
237
|
+
expect(turns[0]?.message).toBeUndefined()
|
|
238
|
+
expect(turns[1]?.message).toBe(CONTRACT_FOLLOW_UP)
|
|
239
|
+
expect(turns[0]?.env.AB_CONTRACT_COLLIDE).toBe('start-value')
|
|
240
|
+
expect(turns[0]?.env.AB_CONTRACT_START_ONLY).toBe('retained')
|
|
241
|
+
expect(turns[1]?.env.AB_CONTRACT_COLLIDE).toBe('continue-value')
|
|
242
|
+
expect(turns[1]?.env.AB_CONTRACT_START_ONLY).toBe('retained')
|
|
243
|
+
expect(turns[1]?.env.AB_CONTRACT_CONTINUE_ONLY).toBe('fresh')
|
|
244
|
+
expect(turns[1]?.env.AB_PHASE).toBe('implement@2')
|
|
245
|
+
await invokeManagedCli(turns[0]!.env)
|
|
246
|
+
await invokeManagedCli(turns[1]!.env)
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
expectTranscript(transcript, harness.runner, harness.model)
|
|
250
|
+
} finally {
|
|
251
|
+
await rm(conflictDir, { recursive: true, force: true })
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('an aborted start terminates promptly and retains an endable transcript', async () => {
|
|
257
|
+
await withHarness(factory, 'cancel-start', async (harness) => {
|
|
258
|
+
const controller = new AbortController()
|
|
259
|
+
const pending = harness.runner.start({
|
|
260
|
+
skill: CONTRACT_SKILL,
|
|
261
|
+
invocation: CONTRACT_INVOCATION,
|
|
262
|
+
workspacePath: harness.workspacePath,
|
|
263
|
+
model: harness.model,
|
|
264
|
+
env: { AB_PHASE: 'implement@1' },
|
|
265
|
+
signal: controller.signal,
|
|
266
|
+
})
|
|
267
|
+
await waitForTurn(harness, 1)
|
|
268
|
+
controller.abort(new Error('contract operator cancellation'))
|
|
269
|
+
const started = await Promise.race([
|
|
270
|
+
pending,
|
|
271
|
+
Bun.sleep(1000).then(() => {
|
|
272
|
+
throw new Error('cancelled start did not settle within one second')
|
|
273
|
+
}),
|
|
274
|
+
])
|
|
275
|
+
const { transcript } = await withEndedSession(harness.runner, started.session, async () => {
|
|
276
|
+
expect(started.result).toMatchObject({
|
|
277
|
+
kind: 'failed',
|
|
278
|
+
failure: { permanent: false },
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
expectTranscript(transcript, harness.runner, harness.model)
|
|
282
|
+
if (harness.disposed !== undefined) expect(harness.disposed()).toBe(1)
|
|
283
|
+
await expect(harness.runner.end(started.session)).rejects.toThrow()
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('an aborted continued turn terminates promptly and the whole session remains disposable', async () => {
|
|
288
|
+
await withHarness(factory, 'cancel-continue', async (harness) => {
|
|
289
|
+
const started = await harness.runner.start({
|
|
290
|
+
skill: CONTRACT_SKILL,
|
|
291
|
+
invocation: CONTRACT_INVOCATION,
|
|
292
|
+
workspacePath: harness.workspacePath,
|
|
293
|
+
model: harness.model,
|
|
294
|
+
env: { AB_PHASE: 'implement@1' },
|
|
295
|
+
})
|
|
296
|
+
expectTypedCompleted(started.result)
|
|
297
|
+
const controller = new AbortController()
|
|
298
|
+
const pending = harness.runner.continue(started.session, CONTRACT_FOLLOW_UP, {
|
|
299
|
+
env: { AB_PHASE: 'implement@2' },
|
|
300
|
+
signal: controller.signal,
|
|
301
|
+
})
|
|
302
|
+
await waitForTurn(harness, 2)
|
|
303
|
+
controller.abort(new Error('contract operator cancellation'))
|
|
304
|
+
const { transcript } = await withEndedSession(harness.runner, started.session, async () => {
|
|
305
|
+
const result = await Promise.race([
|
|
306
|
+
pending,
|
|
307
|
+
Bun.sleep(1000).then(() => {
|
|
308
|
+
throw new Error('cancelled continuation did not settle within one second')
|
|
309
|
+
}),
|
|
310
|
+
])
|
|
311
|
+
expect(result).toMatchObject({ kind: 'failed', failure: { permanent: false } })
|
|
312
|
+
})
|
|
313
|
+
expectTranscript(transcript, harness.runner, harness.model)
|
|
314
|
+
if (harness.disposed !== undefined) expect(harness.disposed()).toBe(1)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('an unclassified provider failure stays retryable and keeps an endable handle', async () => {
|
|
319
|
+
await withHarness(factory, 'retryable-failure', async (harness) => {
|
|
320
|
+
const started = await harness.runner.start({
|
|
321
|
+
skill: CONTRACT_SKILL,
|
|
322
|
+
invocation: CONTRACT_INVOCATION,
|
|
323
|
+
workspacePath: harness.workspacePath,
|
|
324
|
+
model: harness.model,
|
|
325
|
+
env: { AB_PHASE: 'implement@1' },
|
|
326
|
+
})
|
|
327
|
+
const { transcript } = await withEndedSession(harness.runner, started.session, async () => {
|
|
328
|
+
expect(started.result).toMatchObject({
|
|
329
|
+
kind: 'failed',
|
|
330
|
+
failure: {
|
|
331
|
+
message: CONTRACT_RETRYABLE_FAILURE,
|
|
332
|
+
permanent: false,
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
expectUsage(started.result.usage)
|
|
336
|
+
})
|
|
337
|
+
expectTranscript(transcript, harness.runner, harness.model)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('positive authentication/permission evidence is a permanent credential failure', async () => {
|
|
342
|
+
await withHarness(factory, 'permanent-failure', async (harness) => {
|
|
343
|
+
const started = await harness.runner.start({
|
|
344
|
+
skill: CONTRACT_SKILL,
|
|
345
|
+
invocation: CONTRACT_INVOCATION,
|
|
346
|
+
workspacePath: harness.workspacePath,
|
|
347
|
+
model: harness.model,
|
|
348
|
+
env: { AB_PHASE: 'implement@1' },
|
|
349
|
+
})
|
|
350
|
+
const { transcript } = await withEndedSession(harness.runner, started.session, async () => {
|
|
351
|
+
expect(started.result).toMatchObject({
|
|
352
|
+
kind: 'failed',
|
|
353
|
+
failure: {
|
|
354
|
+
message: CONTRACT_PERMANENT_FAILURE,
|
|
355
|
+
permanent: true,
|
|
356
|
+
cause: 'credentials',
|
|
357
|
+
},
|
|
358
|
+
})
|
|
359
|
+
expectUsage(started.result.usage)
|
|
360
|
+
})
|
|
361
|
+
expectTranscript(transcript, harness.runner, harness.model)
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('quota evidence is permanent for retry policy but identified as provider exhaustion', async () => {
|
|
366
|
+
await withHarness(factory, 'exhaustion-failure', async (harness) => {
|
|
367
|
+
const started = await harness.runner.start({
|
|
368
|
+
skill: CONTRACT_SKILL,
|
|
369
|
+
invocation: CONTRACT_INVOCATION,
|
|
370
|
+
workspacePath: harness.workspacePath,
|
|
371
|
+
model: harness.model,
|
|
372
|
+
env: { AB_PHASE: 'implement@1' },
|
|
373
|
+
})
|
|
374
|
+
const { transcript } = await withEndedSession(harness.runner, started.session, async () => {
|
|
375
|
+
expect(started.result).toMatchObject({
|
|
376
|
+
kind: 'failed',
|
|
377
|
+
failure: {
|
|
378
|
+
message: CONTRACT_EXHAUSTION_FAILURE,
|
|
379
|
+
permanent: true,
|
|
380
|
+
cause: 'exhaustion',
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
expectUsage(started.result.usage)
|
|
384
|
+
})
|
|
385
|
+
expectTranscript(transcript, harness.runner, harness.model)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('declared one-shot completion forwards its exact input and returns typed text', async () => {
|
|
390
|
+
await withHarness(factory, 'success', async (harness) => {
|
|
391
|
+
if (harness.oneShot === undefined) return
|
|
392
|
+
const input: OneShotCompletionInput = {
|
|
393
|
+
prompt: CONTRACT_ONE_SHOT_PROMPT,
|
|
394
|
+
cwd: harness.workspacePath,
|
|
395
|
+
env: { AB_CONTRACT_ONE_SHOT: 'scoped-value' },
|
|
396
|
+
model: harness.model,
|
|
397
|
+
}
|
|
398
|
+
const result = await harness.oneShot.completion.complete(input)
|
|
399
|
+
expect(result).toEqual({ text: CONTRACT_ONE_SHOT_TEXT })
|
|
400
|
+
const observed = harness.oneShot.observation()
|
|
401
|
+
expect(observed).toBeDefined()
|
|
402
|
+
expect(observed?.prompt).toBe(input.prompt)
|
|
403
|
+
expect(observed?.cwd).toBe(input.cwd)
|
|
404
|
+
expect(observed?.env.AB_CONTRACT_ONE_SHOT).toBe('scoped-value')
|
|
405
|
+
expect(observed?.model).toBe(input.model)
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// ── Session streams (SPEC §9) ── exercised only when the offline fake
|
|
410
|
+
// supports the streaming boundary; a runner without it skips cleanly.
|
|
411
|
+
|
|
412
|
+
test('a representative turn streams prompt, text, one tool call with input and output, and finish-step', async () => {
|
|
413
|
+
await withHarness(factory, 'stream-turn', async (harness) => {
|
|
414
|
+
if (harness.stream === undefined) return
|
|
415
|
+
const parts: StreamPart[] = []
|
|
416
|
+
const started = await harness.runner.start({
|
|
417
|
+
skill: CONTRACT_SKILL,
|
|
418
|
+
invocation: CONTRACT_INVOCATION,
|
|
419
|
+
workspacePath: harness.workspacePath,
|
|
420
|
+
model: harness.model,
|
|
421
|
+
env: { AB_PHASE: 'implement@1' },
|
|
422
|
+
stream: { append: (appended: StreamPart[]) => parts.push(...appended) },
|
|
423
|
+
})
|
|
424
|
+
expectTypedCompleted(started.result)
|
|
425
|
+
const types = parts.map((part) => part.type)
|
|
426
|
+
// The turn's two sides are visible: its prompt precedes the output.
|
|
427
|
+
expect(types[0]).toBe('data-ab-prompt')
|
|
428
|
+
expect(types).toContain('start')
|
|
429
|
+
// At minimum: a completed assistant message, one tool call with its
|
|
430
|
+
// input and output, and the step bracket around them.
|
|
431
|
+
expect(types).toContain('start-step')
|
|
432
|
+
expect(types).toContain('finish-step')
|
|
433
|
+
expect(types).toContain('text-start')
|
|
434
|
+
expect(types).toContain('text-delta')
|
|
435
|
+
expect(types).toContain('text-end')
|
|
436
|
+
const toolInput = parts.find((part) => part.type === 'tool-input-available') as unknown as
|
|
437
|
+
| { toolName: string }
|
|
438
|
+
| undefined
|
|
439
|
+
expect(toolInput?.toolName).toBeDefined()
|
|
440
|
+
expect(parts.some((part) => part.type === 'tool-output-available')).toBe(true)
|
|
441
|
+
expect(parts.at(-1)?.type).toBe('finish')
|
|
442
|
+
// The prompt part carries this turn's skill invocation.
|
|
443
|
+
const prompt = parts.find((part) => part.type === 'data-ab-prompt') as unknown as
|
|
444
|
+
| { data: { text: string } }
|
|
445
|
+
| undefined
|
|
446
|
+
expect(prompt?.data.text).toContain(CONTRACT_SKILL)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
test('a failed turn emits an error part before the stream closes', async () => {
|
|
451
|
+
await withHarness(factory, 'retryable-failure', async (harness) => {
|
|
452
|
+
if (harness.stream === undefined) return
|
|
453
|
+
const parts: StreamPart[] = []
|
|
454
|
+
await harness.runner.start({
|
|
455
|
+
skill: CONTRACT_SKILL,
|
|
456
|
+
invocation: CONTRACT_INVOCATION,
|
|
457
|
+
workspacePath: harness.workspacePath,
|
|
458
|
+
model: harness.model,
|
|
459
|
+
env: { AB_PHASE: 'implement@1' },
|
|
460
|
+
stream: { append: (appended: StreamPart[]) => parts.push(...appended) },
|
|
461
|
+
})
|
|
462
|
+
expect(parts.at(-1)?.type).toBe('error')
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test('a cancelled turn emits an abort part', async () => {
|
|
467
|
+
await withHarness(factory, 'cancel-start', async (harness) => {
|
|
468
|
+
if (harness.stream === undefined) return
|
|
469
|
+
const parts: StreamPart[] = []
|
|
470
|
+
const controller = new AbortController()
|
|
471
|
+
const pending = harness.runner.start({
|
|
472
|
+
skill: CONTRACT_SKILL,
|
|
473
|
+
invocation: CONTRACT_INVOCATION,
|
|
474
|
+
workspacePath: harness.workspacePath,
|
|
475
|
+
model: harness.model,
|
|
476
|
+
env: { AB_PHASE: 'implement@1' },
|
|
477
|
+
signal: controller.signal,
|
|
478
|
+
stream: { append: (appended: StreamPart[]) => parts.push(...appended) },
|
|
479
|
+
})
|
|
480
|
+
await waitForTurn(harness, 1)
|
|
481
|
+
controller.abort(new Error('contract operator cancellation'))
|
|
482
|
+
await Promise.race([
|
|
483
|
+
pending,
|
|
484
|
+
Bun.sleep(1000).then(() => {
|
|
485
|
+
throw new Error('cancelled streaming turn did not settle within one second')
|
|
486
|
+
}),
|
|
487
|
+
])
|
|
488
|
+
expect(parts.at(-1)?.type).toBe('abort')
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScriptedAgentRunner (SPEC §9): the deterministic AgentRunner used by every
|
|
3
|
+
* kernel/process/integration test. A single script function plays the agent;
|
|
4
|
+
* the runner records every turn into public journals so tests can assert the
|
|
5
|
+
* exact session traffic — and `end()` produces a Transcript like any real
|
|
6
|
+
* adapter, because transcripts come back through the interface, guaranteed.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
agentInvocation,
|
|
10
|
+
type AgentContinueOpts,
|
|
11
|
+
type AgentFailureCause,
|
|
12
|
+
type AgentRunner,
|
|
13
|
+
type AgentSessionHandle,
|
|
14
|
+
type AgentStartOpts,
|
|
15
|
+
type AgentTurnResult,
|
|
16
|
+
type Transcript,
|
|
17
|
+
} from '../types'
|
|
18
|
+
import { sessionEnv } from './session-env'
|
|
19
|
+
|
|
20
|
+
/** What the script sees on each invocation — one call per turn. */
|
|
21
|
+
export interface ScriptContext {
|
|
22
|
+
/** This TURN's opts: on `continue`, the refreshed ambient env (§10, D8)
|
|
23
|
+
* merged over the start env — exactly what a real adapter launches with. */
|
|
24
|
+
opts: AgentStartOpts
|
|
25
|
+
session: AgentSessionHandle
|
|
26
|
+
/** 1 on `start`, incremented on each `continue`. */
|
|
27
|
+
turn: number
|
|
28
|
+
/** The `continue` message; undefined on turn 1 (start has no message). */
|
|
29
|
+
message?: string
|
|
30
|
+
/** All prior `continue` messages, oldest first (excludes this turn's). */
|
|
31
|
+
history: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Script = (ctx: ScriptContext) => Promise<AgentTurnResult> | AgentTurnResult
|
|
35
|
+
|
|
36
|
+
/** Default completed turn (text '', usage 1/1/1) — keeps scripts terse. */
|
|
37
|
+
export function defaultTurnResult(text = ''): AgentTurnResult {
|
|
38
|
+
return {
|
|
39
|
+
kind: 'completed',
|
|
40
|
+
text,
|
|
41
|
+
usage: { inputTokens: 1, outputTokens: 1, turns: 1 },
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Script a provider/runner-declared failure while preserving the same endable
|
|
46
|
+
* fake session and transcript behavior as a real adapter. */
|
|
47
|
+
export function failedTurnResult(
|
|
48
|
+
message: string,
|
|
49
|
+
permanent: boolean,
|
|
50
|
+
text = '',
|
|
51
|
+
cause?: AgentFailureCause,
|
|
52
|
+
): AgentTurnResult {
|
|
53
|
+
return {
|
|
54
|
+
kind: 'failed',
|
|
55
|
+
text,
|
|
56
|
+
usage: { inputTokens: 0, outputTokens: 0, turns: 1 },
|
|
57
|
+
failure: { message, permanent, ...(cause !== undefined ? { cause } : {}) },
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RecordedTurn {
|
|
62
|
+
turn: number
|
|
63
|
+
/** Undefined on turn 1 — the start prompt is derived from `opts`. */
|
|
64
|
+
message?: string
|
|
65
|
+
result: AgentTurnResult
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Per-session journal, public for assertions. */
|
|
69
|
+
export interface SessionJournal {
|
|
70
|
+
session: AgentSessionHandle
|
|
71
|
+
opts: AgentStartOpts
|
|
72
|
+
turns: RecordedTurn[]
|
|
73
|
+
/** Messages passed to `continue`, in order. */
|
|
74
|
+
messages: string[]
|
|
75
|
+
ended: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ScriptedAgentRunner implements AgentRunner {
|
|
79
|
+
readonly name = 'scripted'
|
|
80
|
+
/** Journals keyed by session id (`s_1`, `s_2`, …), in creation order. */
|
|
81
|
+
readonly sessions = new Map<string, SessionJournal>()
|
|
82
|
+
|
|
83
|
+
private readonly script: Script
|
|
84
|
+
private nextSession = 0
|
|
85
|
+
|
|
86
|
+
constructor(opts: { script: Script }) {
|
|
87
|
+
this.script = opts.script
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async start(
|
|
91
|
+
opts: AgentStartOpts,
|
|
92
|
+
): Promise<{ session: AgentSessionHandle; result: AgentTurnResult }> {
|
|
93
|
+
this.nextSession += 1
|
|
94
|
+
const session: AgentSessionHandle = {
|
|
95
|
+
id: `s_${this.nextSession}`,
|
|
96
|
+
runner: this.name,
|
|
97
|
+
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
98
|
+
}
|
|
99
|
+
const journal: SessionJournal = {
|
|
100
|
+
session,
|
|
101
|
+
opts,
|
|
102
|
+
turns: [],
|
|
103
|
+
messages: [],
|
|
104
|
+
ended: false,
|
|
105
|
+
}
|
|
106
|
+
this.sessions.set(session.id, journal)
|
|
107
|
+
|
|
108
|
+
const result = await this.runScript(
|
|
109
|
+
{
|
|
110
|
+
// Scripts observe the same effective launch environment as production
|
|
111
|
+
// adapters; the journal below intentionally retains the caller's raw
|
|
112
|
+
// start options as the session identity.
|
|
113
|
+
opts: { ...opts, env: sessionEnv(opts.env) },
|
|
114
|
+
session,
|
|
115
|
+
turn: 1,
|
|
116
|
+
history: [],
|
|
117
|
+
},
|
|
118
|
+
opts.signal,
|
|
119
|
+
)
|
|
120
|
+
journal.turns.push({ turn: 1, result })
|
|
121
|
+
return { session, result }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async continue(
|
|
125
|
+
session: AgentSessionHandle,
|
|
126
|
+
message: string,
|
|
127
|
+
opts?: AgentContinueOpts,
|
|
128
|
+
): Promise<AgentTurnResult> {
|
|
129
|
+
const journal = this.liveJournal(session, 'continue')
|
|
130
|
+
const history = [...journal.messages]
|
|
131
|
+
const turn = journal.turns.length + 1
|
|
132
|
+
// Mirror the real adapters (§10, D8): the continued turn runs under the
|
|
133
|
+
// refreshed ambient env merged over the start env, so the script's fake
|
|
134
|
+
// CLI resolves exactly what a real agent's `ab` would. The journal keeps
|
|
135
|
+
// the START opts — they are the session's identity. §9: the turn streams
|
|
136
|
+
// through THIS turn's emitter only — the start turn's emitter (if any)
|
|
137
|
+
// belongs to its own bracket and never receives this turn's parts.
|
|
138
|
+
const scoped = opts?.env !== undefined ? { ...journal.opts.env, ...opts.env } : journal.opts.env
|
|
139
|
+
const turnOpts = {
|
|
140
|
+
...journal.opts,
|
|
141
|
+
env: sessionEnv(scoped),
|
|
142
|
+
stream: opts?.stream,
|
|
143
|
+
...(opts?.signal !== undefined ? { signal: opts.signal } : {}),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = await this.runScript(
|
|
147
|
+
{
|
|
148
|
+
opts: turnOpts,
|
|
149
|
+
session: journal.session,
|
|
150
|
+
turn,
|
|
151
|
+
message,
|
|
152
|
+
history,
|
|
153
|
+
},
|
|
154
|
+
opts?.signal,
|
|
155
|
+
)
|
|
156
|
+
journal.messages.push(message)
|
|
157
|
+
journal.turns.push({ turn, message, result })
|
|
158
|
+
return result
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async end(session: AgentSessionHandle): Promise<Transcript> {
|
|
162
|
+
const journal = this.liveJournal(session, 'end')
|
|
163
|
+
journal.ended = true
|
|
164
|
+
|
|
165
|
+
const usage = { inputTokens: 0, outputTokens: 0, turns: 0 }
|
|
166
|
+
for (const { result } of journal.turns) {
|
|
167
|
+
usage.inputTokens += result.usage.inputTokens
|
|
168
|
+
usage.outputTokens += result.usage.outputTokens
|
|
169
|
+
usage.turns += result.usage.turns
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
content: JSON.stringify(
|
|
174
|
+
{
|
|
175
|
+
session: journal.session.id,
|
|
176
|
+
skill: journal.opts.skill,
|
|
177
|
+
invocation: agentInvocation(journal.opts),
|
|
178
|
+
...(journal.opts.buildSlug !== undefined ? { buildSlug: journal.opts.buildSlug } : {}),
|
|
179
|
+
turns: journal.turns,
|
|
180
|
+
},
|
|
181
|
+
null,
|
|
182
|
+
2,
|
|
183
|
+
),
|
|
184
|
+
metadata: {
|
|
185
|
+
runner: this.name,
|
|
186
|
+
...(journal.session.model !== undefined ? { model: journal.session.model } : {}),
|
|
187
|
+
usage,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async runScript(ctx: ScriptContext, signal?: AbortSignal): Promise<AgentTurnResult> {
|
|
193
|
+
const running = Promise.resolve(this.script(ctx))
|
|
194
|
+
if (signal === undefined) return running
|
|
195
|
+
if (signal.aborted) return failedTurnResult('scripted runner: turn aborted', false)
|
|
196
|
+
return new Promise<AgentTurnResult>((resolve, reject) => {
|
|
197
|
+
const aborted = (): void => resolve(failedTurnResult('scripted runner: turn aborted', false))
|
|
198
|
+
signal.addEventListener('abort', aborted, { once: true })
|
|
199
|
+
running.then(resolve, reject).finally(() => signal.removeEventListener('abort', aborted))
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private liveJournal(session: AgentSessionHandle, op: 'continue' | 'end'): SessionJournal {
|
|
204
|
+
const journal = this.sessions.get(session.id)
|
|
205
|
+
if (!journal) {
|
|
206
|
+
throw new Error(`${this.name}: ${op} on unknown session "${session.id}"`)
|
|
207
|
+
}
|
|
208
|
+
if (journal.ended) {
|
|
209
|
+
throw new Error(`${this.name}: ${op} on ended session "${session.id}"`)
|
|
210
|
+
}
|
|
211
|
+
return journal
|
|
212
|
+
}
|
|
213
|
+
}
|