@markjaquith/agency 3.5.0 → 3.5.2
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 +16 -4
- package/docs/opencode-auto-start.md +67 -0
- package/package.json +3 -1
- package/src/commands/work.ts +16 -2
- package/src/utils/exec.ts +21 -6
- package/src/workbase/opencode-launch.ts +120 -0
- package/src/workbase/opencode-v2-tui-plugin-file.ts +108 -21
package/README.md
CHANGED
|
@@ -326,8 +326,18 @@ which open a fresh TUI session so the generated continuation prompt cannot be
|
|
|
326
326
|
routed to an unrelated prior session. By default Agency opens the agent without
|
|
327
327
|
a prompt. `--auto` uses its autonomous command and sends the generated task,
|
|
328
328
|
phase, or epic prompt. OpenCode V2 receives a launch-only environment marker;
|
|
329
|
-
Agency's managed TUI companion
|
|
330
|
-
|
|
329
|
+
Agency's managed TUI companion retries the native submit command until the exact
|
|
330
|
+
prompt appears as a persisted user message. It records whether OpenCode submitted
|
|
331
|
+
the prompt natively or submission followed a companion dispatch, and shows a
|
|
332
|
+
bounded error with a manual recovery instruction when delivery is not observed.
|
|
333
|
+
|
|
334
|
+
For built-in OpenCode V2 launches, Agency submits through the selected CLI's
|
|
335
|
+
authenticated session API, then opens the TUI with `--session` and an empty
|
|
336
|
+
composer. V2's native `--prompt` only fills the composer. Agency sets session
|
|
337
|
+
environment before submission and passes that same environment into the TUI;
|
|
338
|
+
reconnecting the TUI does not replay the prompt. V1 keeps its native launch path.
|
|
339
|
+
See [OpenCode auto-start](docs/opencode-auto-start.md) for details and real
|
|
340
|
+
startup verification.
|
|
331
341
|
|
|
332
342
|
Custom agents are direct argv commands, never shell snippets:
|
|
333
343
|
|
|
@@ -395,8 +405,10 @@ resolve a materialized execution-unit
|
|
|
395
405
|
checkout through `agency context`. A multi-phase
|
|
396
406
|
task root has no single checkout, so launch from its phase directory when using
|
|
397
407
|
plain OpenCode or Pi. Other checkout-local configuration is not composed.
|
|
398
|
-
`--print-command` prints the
|
|
399
|
-
without launching the agent.
|
|
408
|
+
`--print-command` prints the cwd, command template, and non-secret environment
|
|
409
|
+
keys without launching the agent. Built-in OpenCode auto launches also report a
|
|
410
|
+
`startup` note: V2 resolves the final `--session` argv during actual startup, so
|
|
411
|
+
print-only mode creates neither a session nor a prompt.
|
|
400
412
|
|
|
401
413
|
### Custom Chooser Command
|
|
402
414
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# OpenCode auto-start
|
|
2
|
+
|
|
3
|
+
`agency work . --auto` submits the generated prompt and opens the interactive
|
|
4
|
+
OpenCode TUI. Plain `agency work .` remains promptless.
|
|
5
|
+
|
|
6
|
+
## OpenCode V2
|
|
7
|
+
|
|
8
|
+
V2's native `opencode --prompt TEXT` populates the composer; it does not submit.
|
|
9
|
+
The managed Agency plugin's worker-identity hooks run after submission and are
|
|
10
|
+
not an auto-submit trigger.
|
|
11
|
+
|
|
12
|
+
For the built-in `opencode` and `opencode2` presets, Agency:
|
|
13
|
+
|
|
14
|
+
1. Checks the selected executable's version.
|
|
15
|
+
2. Creates a session at the canonical launch directory, or resolves the newest
|
|
16
|
+
root session in that exact directory for a continued launch.
|
|
17
|
+
3. Sets session environment through `PUT /api/session/{id}/environment` before
|
|
18
|
+
admitting input. This is necessary when the service is already running.
|
|
19
|
+
4. Admits one generated prompt with `resume: true`, letting OpenCode assign its ID.
|
|
20
|
+
5. Replaces the launcher with `opencode --session ID`, without a composer prompt.
|
|
21
|
+
|
|
22
|
+
All API calls use `opencode api`, retaining that executable's service discovery
|
|
23
|
+
and authentication. No TUI keystrokes or plugin reload callbacks submit input.
|
|
24
|
+
Admission errors stop startup; they do not fall back to a populated composer or
|
|
25
|
+
blindly retry a possibly admitted request. Errors identify the API operation
|
|
26
|
+
without printing the environment payload. A stopped launch can leave a session
|
|
27
|
+
and a working Agency task for recovery.
|
|
28
|
+
|
|
29
|
+
The native exec helper explicitly passes Bun's current environment to `execve`.
|
|
30
|
+
Calling libc `execvp` directly loses Bun-side environment mutations. This matters
|
|
31
|
+
because the V2 TUI refreshes session environment from its own inherited snapshot
|
|
32
|
+
when it attaches or reconnects.
|
|
33
|
+
|
|
34
|
+
V1 retains its native `--prompt` path. Custom runner definitions remain responsible
|
|
35
|
+
for their own submission behavior. This implementation was exercised against
|
|
36
|
+
installed OpenCode V2.0.1; V1 compatibility has focused command regression coverage.
|
|
37
|
+
|
|
38
|
+
## Verification
|
|
39
|
+
|
|
40
|
+
Focused regressions:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
bun test src/utils/exec.test.ts src/workbase/opencode-launch.test.ts src/commands/work.test.ts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Real opt-in startup smoke (requires installed, authenticated V2 OpenCode and Git):
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
bun run test:opencode-auto
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This creates an isolated fixture repository/workbase and launches the real
|
|
53
|
+
`agency work . --auto --agent opencode` under OpenCode's PTY API. It sends zero
|
|
54
|
+
keyboard bytes and requires:
|
|
55
|
+
|
|
56
|
+
- Exactly one submitted user message.
|
|
57
|
+
- A completed assistant message with a text part exactly
|
|
58
|
+
`AGENCY_AUTO_SMOKE_EXECUTED`, not that string inside tool input/output.
|
|
59
|
+
- Actual completed shell output containing nonempty Agency identity and an empty
|
|
60
|
+
`HERDR_ENV`, with no inherited supervisor pane identity.
|
|
61
|
+
- A clean fixture checkout after execution.
|
|
62
|
+
|
|
63
|
+
It closes only its own PTY and Git daemon. The fixture, `messages.json`, and
|
|
64
|
+
`evidence.json` remain at the printed temporary path for inspection.
|
|
65
|
+
`AGENCY_SMOKE_TMPDIR` overrides the fixture parent.
|
|
66
|
+
Set `AGENCY_SMOKE_EXECUTABLE=/absolute/path/to/agency` to exercise an installed
|
|
67
|
+
CLI instead of the development checkout; evidence records the resolved executable.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"fixtures/protocol/success.json",
|
|
33
33
|
"fixtures/protocol/error.json",
|
|
34
34
|
"README.md",
|
|
35
|
+
"docs/opencode-auto-start.md",
|
|
35
36
|
"LICENSE"
|
|
36
37
|
],
|
|
37
38
|
"type": "module",
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"benchmark:review": "bun scripts/benchmark-review.ts",
|
|
93
94
|
"test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
|
|
94
95
|
"test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
|
|
96
|
+
"test:opencode-auto": "bun scripts/smoke-opencode-auto.ts",
|
|
95
97
|
"format": "oxfmt",
|
|
96
98
|
"format:check": "oxfmt --check",
|
|
97
99
|
"knip": "knip --production",
|
package/src/commands/work.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
readValidationEvidence,
|
|
38
38
|
} from "../workbase/execution-contract"
|
|
39
39
|
import { ValidationFailedError } from "./validate"
|
|
40
|
+
import { prepareOpenCodeLaunch } from "../workbase/opencode-launch"
|
|
40
41
|
|
|
41
42
|
export interface WorkOptions extends BaseCommandOptions {
|
|
42
43
|
readonly directory?: string
|
|
@@ -386,12 +387,22 @@ export const work = (
|
|
|
386
387
|
...agentEnvironment(agent, variables),
|
|
387
388
|
}
|
|
388
389
|
if (writablePath) environment.AGENCY_WRITABLE_CHECKOUT = writablePath
|
|
390
|
+
const managedOpenCodeAuto =
|
|
391
|
+
options.auto &&
|
|
392
|
+
(agent === "opencode" || agent === "opencode2") &&
|
|
393
|
+
!config.agents?.[agent]
|
|
389
394
|
if (options.printCommand) {
|
|
390
395
|
log(
|
|
391
396
|
JSON.stringify(
|
|
392
397
|
{
|
|
393
398
|
cwd: launchPath,
|
|
394
399
|
argv: resolved.argv,
|
|
400
|
+
...(managedOpenCodeAuto
|
|
401
|
+
? {
|
|
402
|
+
startup:
|
|
403
|
+
"OpenCode V2: submit via API, then attach with --session; V1: use argv",
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
395
406
|
environment: printableEnvironment(environment),
|
|
396
407
|
},
|
|
397
408
|
null,
|
|
@@ -449,14 +460,17 @@ export const work = (
|
|
|
449
460
|
)
|
|
450
461
|
}
|
|
451
462
|
}
|
|
463
|
+
const launchArgv = managedOpenCodeAuto
|
|
464
|
+
? yield* prepareOpenCodeLaunch(resolved.argv, launchPath, environment)
|
|
465
|
+
: resolved.argv
|
|
452
466
|
for (const [key, value] of Object.entries(environment)) {
|
|
453
467
|
process.env[key] = value
|
|
454
468
|
}
|
|
455
469
|
verboseLog(
|
|
456
|
-
`Launching command: ${formatCommand(
|
|
470
|
+
`Launching command: ${formatCommand(launchArgv)} (cwd: ${launchPath})`,
|
|
457
471
|
)
|
|
458
472
|
try {
|
|
459
|
-
launch(cli,
|
|
473
|
+
launch(cli, launchArgv, launchPath, environment)
|
|
460
474
|
} finally {
|
|
461
475
|
for (const key of Object.keys(environment)) {
|
|
462
476
|
const previous = previousEnvironment[key]
|
package/src/utils/exec.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dlopen, FFIType, ptr } from "bun:ffi"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* PATH-resolving native exec using POSIX execve and an explicit environment.
|
|
5
5
|
* This completely replaces the current process with the specified command.
|
|
6
6
|
*
|
|
7
7
|
* IMPORTANT: This function will never return if successful. The process
|
|
@@ -12,12 +12,18 @@ import { dlopen, FFIType, ptr } from "bun:ffi"
|
|
|
12
12
|
* @throws Error if exec fails (e.g., command not found)
|
|
13
13
|
*/
|
|
14
14
|
export function execvp(file: string, args: string[]): never {
|
|
15
|
-
|
|
15
|
+
const executable = Bun.which(file)
|
|
16
|
+
if (!executable) throw new Error(`Unable to find executable '${file}'`)
|
|
17
|
+
// Bun's process.env mutations are not reflected in libc's environ. Passing
|
|
18
|
+
// envp explicitly preserves launch identity and deletions across replacement.
|
|
19
|
+
const variables = Object.entries(process.env)
|
|
20
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
21
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
16
22
|
const libcPath =
|
|
17
23
|
process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6"
|
|
18
24
|
const libc = dlopen(libcPath, {
|
|
19
|
-
|
|
20
|
-
args: [FFIType.cstring, FFIType.ptr],
|
|
25
|
+
execve: {
|
|
26
|
+
args: [FFIType.cstring, FFIType.ptr, FFIType.ptr],
|
|
21
27
|
returns: FFIType.int,
|
|
22
28
|
},
|
|
23
29
|
})
|
|
@@ -36,10 +42,19 @@ export function execvp(file: string, args: string[]): never {
|
|
|
36
42
|
}
|
|
37
43
|
// Null-terminate the pointer array
|
|
38
44
|
ptrs[args.length] = 0n
|
|
45
|
+
const environmentStrings = variables.map((value) => Buffer.from(value + "\0"))
|
|
46
|
+
const environmentPointers = new BigUint64Array(environmentStrings.length + 1)
|
|
47
|
+
for (let i = 0; i < environmentStrings.length; i++) {
|
|
48
|
+
environmentPointers[i] = BigInt(ptr(environmentStrings[i]!))
|
|
49
|
+
}
|
|
39
50
|
|
|
40
51
|
// Call execvp - this will replace the current process if successful
|
|
41
|
-
const fileBuffer = Buffer.from(
|
|
42
|
-
const result = libc.symbols.
|
|
52
|
+
const fileBuffer = Buffer.from(executable + "\0")
|
|
53
|
+
const result = libc.symbols.execve(
|
|
54
|
+
ptr(fileBuffer),
|
|
55
|
+
ptr(ptrs),
|
|
56
|
+
ptr(environmentPointers),
|
|
57
|
+
)
|
|
43
58
|
|
|
44
59
|
// If we reach here, exec failed
|
|
45
60
|
throw new Error(
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Schema } from "@effect/schema"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
4
|
+
|
|
5
|
+
const Session = Schema.Struct({
|
|
6
|
+
id: Schema.String.pipe(Schema.pattern(/^ses/)),
|
|
7
|
+
location: Schema.Struct({ directory: Schema.String }),
|
|
8
|
+
})
|
|
9
|
+
const Created = Schema.Struct({ data: Session })
|
|
10
|
+
const Listed = Schema.Struct({ data: Schema.Array(Session) })
|
|
11
|
+
const Admitted = Schema.Struct({
|
|
12
|
+
data: Schema.Struct({
|
|
13
|
+
sessionID: Schema.String,
|
|
14
|
+
type: Schema.Literal("user"),
|
|
15
|
+
}),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// V2's TUI --prompt only seeds the composer. Submit through the same CLI's
|
|
19
|
+
// authenticated service connection, then attach without a prompt. Reloading or
|
|
20
|
+
// reconnecting the TUI cannot replay the launch input. Only built-in Agency
|
|
21
|
+
// auto-command templates call this helper: [cli, [--continue], --prompt, text].
|
|
22
|
+
export const prepareOpenCodeLaunch = (
|
|
23
|
+
argv: readonly string[],
|
|
24
|
+
cwd: string,
|
|
25
|
+
env: Record<string, string>,
|
|
26
|
+
) =>
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
const fs = yield* FileSystemService
|
|
29
|
+
cwd = yield* fs.realPath(cwd)
|
|
30
|
+
const cli = argv[0]!
|
|
31
|
+
const run = (args: readonly string[]) =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const result = yield* fs
|
|
34
|
+
.runCommand([cli, ...args], {
|
|
35
|
+
cwd,
|
|
36
|
+
env,
|
|
37
|
+
captureOutput: true,
|
|
38
|
+
timeoutMs: 60_000,
|
|
39
|
+
})
|
|
40
|
+
.pipe(
|
|
41
|
+
Effect.mapError(
|
|
42
|
+
() =>
|
|
43
|
+
new Error(
|
|
44
|
+
`OpenCode auto-start command failed: ${args.slice(0, args.indexOf("--data") < 0 ? args.length : args.indexOf("--data")).join(" ")}`,
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
if (result.exitCode !== 0) {
|
|
49
|
+
return yield* Effect.fail(
|
|
50
|
+
new Error(
|
|
51
|
+
`OpenCode auto-start failed (${args.slice(0, 3).join(" ")}): ${result.stderr}`,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return result.stdout
|
|
56
|
+
})
|
|
57
|
+
const version = (yield* run(["--version"])).trim()
|
|
58
|
+
if (/^(?:opencode\s+v?)?1\./.test(version)) return [...argv]
|
|
59
|
+
if (!/^(?:opencode\s+v?)?2\./.test(version)) {
|
|
60
|
+
return yield* Effect.fail(
|
|
61
|
+
new Error(`Unsupported OpenCode version for auto-start: ${version}`),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
const api = (method: string, path: string, body?: unknown) =>
|
|
65
|
+
run([
|
|
66
|
+
"api",
|
|
67
|
+
method,
|
|
68
|
+
path,
|
|
69
|
+
...(body === undefined ? [] : ["--data", JSON.stringify(body)]),
|
|
70
|
+
])
|
|
71
|
+
let session: typeof Session.Type | undefined
|
|
72
|
+
if (argv.includes("--continue")) {
|
|
73
|
+
const query = new URLSearchParams({
|
|
74
|
+
directory: cwd,
|
|
75
|
+
parentID: "null",
|
|
76
|
+
order: "desc",
|
|
77
|
+
limit: "1",
|
|
78
|
+
})
|
|
79
|
+
const listed = yield* Schema.decodeUnknown(Schema.parseJson(Listed))(
|
|
80
|
+
yield* api("get", `/api/session?${query}`),
|
|
81
|
+
)
|
|
82
|
+
session = listed.data[0]
|
|
83
|
+
}
|
|
84
|
+
if (!session) {
|
|
85
|
+
const created = yield* Schema.decodeUnknown(Schema.parseJson(Created))(
|
|
86
|
+
yield* api("post", "/api/session", { location: { directory: cwd } }),
|
|
87
|
+
)
|
|
88
|
+
session = created.data
|
|
89
|
+
}
|
|
90
|
+
if ((yield* fs.realPath(session.location.directory)) !== cwd) {
|
|
91
|
+
return yield* Effect.fail(
|
|
92
|
+
new Error(
|
|
93
|
+
`OpenCode returned a session outside the launch directory: ${session.id}`,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
// API client environment is not session environment on the shared server.
|
|
98
|
+
// Replace it before admission so the first shell/tool sees the caller's
|
|
99
|
+
// Agency identity and (for a real Herdr launch) the correct pane identity.
|
|
100
|
+
const variables = Object.fromEntries(
|
|
101
|
+
Object.entries({ ...process.env, ...env }).filter(
|
|
102
|
+
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
yield* api("put", `/api/session/${session.id}/environment`, { variables })
|
|
106
|
+
const submitted = yield* Schema.decodeUnknown(Schema.parseJson(Admitted))(
|
|
107
|
+
yield* api("post", `/api/session/${session.id}/prompt`, {
|
|
108
|
+
text: argv.at(-1)!,
|
|
109
|
+
resume: true,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
if (submitted.data.sessionID !== session.id) {
|
|
113
|
+
return yield* Effect.fail(
|
|
114
|
+
new Error(
|
|
115
|
+
`OpenCode did not confirm the launch prompt for ${session.id}`,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return [cli, "--session", session.id]
|
|
120
|
+
})
|
|
@@ -35,16 +35,37 @@ export default plugin
|
|
|
35
35
|
`
|
|
36
36
|
|
|
37
37
|
const tuiBody = `const autosubmitTimeoutMs = 10_000
|
|
38
|
-
const autosubmitRetryMs =
|
|
38
|
+
const autosubmitRetryMs = 100
|
|
39
|
+
|
|
40
|
+
type AutosubmitObservation = {
|
|
41
|
+
event: "marker" | "dispatch" | "submitted" | "timeout" | "error"
|
|
42
|
+
detail?: string
|
|
43
|
+
dispatches: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type AutosubmitMessage = {
|
|
47
|
+
type?: string
|
|
48
|
+
text?: string
|
|
49
|
+
info?: { role?: string }
|
|
50
|
+
role?: string
|
|
51
|
+
parts?: readonly { type?: string; text?: string }[]
|
|
52
|
+
}
|
|
39
53
|
|
|
40
54
|
type AutosubmitContext = {
|
|
41
55
|
keymap: {
|
|
42
56
|
commands(): readonly { id?: string }[]
|
|
43
57
|
dispatch(id: string): unknown
|
|
44
58
|
}
|
|
45
|
-
|
|
59
|
+
data?: {
|
|
60
|
+
session?: {
|
|
61
|
+
message?: {
|
|
62
|
+
sync?(sessionID: string): Promise<unknown>
|
|
63
|
+
list?(sessionID: string): readonly AutosubmitMessage[] | undefined
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
46
67
|
ui: {
|
|
47
|
-
router: { current(): { type: string } }
|
|
68
|
+
router: { current(): { type: string; sessionID?: string } }
|
|
48
69
|
toast: {
|
|
49
70
|
show(input: {
|
|
50
71
|
variant: "error"
|
|
@@ -57,7 +78,11 @@ type AutosubmitContext = {
|
|
|
57
78
|
}
|
|
58
79
|
|
|
59
80
|
export const createAgencyAutosubmit = (
|
|
60
|
-
options: {
|
|
81
|
+
options: {
|
|
82
|
+
timeoutMs?: number
|
|
83
|
+
retryMs?: number
|
|
84
|
+
observe?: (observation: AutosubmitObservation) => void
|
|
85
|
+
} = {},
|
|
61
86
|
) => {
|
|
62
87
|
let started = false
|
|
63
88
|
|
|
@@ -66,49 +91,111 @@ export const createAgencyAutosubmit = (
|
|
|
66
91
|
if (process.env.AGENCY_TUI_AUTOSUBMIT !== "1") return () => {}
|
|
67
92
|
if (!process.env.AGENCY_PROMPT) return () => {}
|
|
68
93
|
started = true
|
|
69
|
-
delete process.env.AGENCY_TUI_AUTOSUBMIT
|
|
70
94
|
|
|
71
95
|
const timeoutMs = options.timeoutMs ?? autosubmitTimeoutMs
|
|
72
96
|
const retryMs = options.retryMs ?? autosubmitRetryMs
|
|
73
97
|
const deadline = Date.now() + timeoutMs
|
|
98
|
+
const prompt = process.env.AGENCY_PROMPT
|
|
74
99
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
75
100
|
let stopped = false
|
|
101
|
+
let dispatches = 0
|
|
102
|
+
let lastRoute = "unknown"
|
|
103
|
+
|
|
104
|
+
const observe = (event: AutosubmitObservation["event"], detail?: string) => {
|
|
105
|
+
const observation = { event, detail, dispatches }
|
|
106
|
+
options.observe?.(observation)
|
|
107
|
+
if (
|
|
108
|
+
!options.observe &&
|
|
109
|
+
(event !== "dispatch" || dispatches === 1 || dispatches % 10 === 0)
|
|
110
|
+
) {
|
|
111
|
+
const suffix = detail ? ": " + detail : ""
|
|
112
|
+
console.info("[agency.tui] autosubmit " + event + suffix)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
76
115
|
|
|
77
116
|
const stop = () => {
|
|
78
117
|
stopped = true
|
|
79
118
|
if (timer) clearTimeout(timer)
|
|
80
119
|
}
|
|
81
120
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
const submitted = async (sessionID: string) => {
|
|
122
|
+
const messages = context.data?.session?.message
|
|
123
|
+
if (!messages?.list) return false
|
|
124
|
+
try {
|
|
125
|
+
await messages.sync?.(sessionID)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
observe("error", "message sync failed: " + String(error))
|
|
128
|
+
return false
|
|
87
129
|
}
|
|
130
|
+
return (messages.list(sessionID) ?? []).some((message) => {
|
|
131
|
+
const role = message.type === "user" ? "user" : message.info?.role ?? message.role
|
|
132
|
+
const text =
|
|
133
|
+
message.text ??
|
|
134
|
+
(message.parts ?? [])
|
|
135
|
+
.filter((part) => part.type === "text")
|
|
136
|
+
.map((part) => part.text ?? "")
|
|
137
|
+
.join("")
|
|
138
|
+
return role === "user" && text === prompt
|
|
139
|
+
})
|
|
140
|
+
}
|
|
88
141
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
142
|
+
const finish = (event: "submitted" | "timeout", detail: string) => {
|
|
143
|
+
stop()
|
|
144
|
+
delete process.env.AGENCY_TUI_AUTOSUBMIT
|
|
145
|
+
observe(event, detail)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const attempt = async () => {
|
|
149
|
+
if (stopped) return
|
|
150
|
+
const route = context.ui.router.current()
|
|
151
|
+
lastRoute = route.type
|
|
152
|
+
if (route.type === "session" && route.sessionID) {
|
|
153
|
+
if (await submitted(route.sessionID)) {
|
|
154
|
+
finish(
|
|
155
|
+
"submitted",
|
|
156
|
+
dispatches === 0
|
|
157
|
+
? "native OpenCode submission observed"
|
|
158
|
+
: "submitted message observed after companion dispatch",
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
96
162
|
}
|
|
97
163
|
|
|
164
|
+
if (stopped) return
|
|
98
165
|
if (Date.now() >= deadline) {
|
|
99
|
-
|
|
166
|
+
finish(
|
|
167
|
+
"timeout",
|
|
168
|
+
"route=" + lastRoute + ", dispatches=" + String(dispatches),
|
|
169
|
+
)
|
|
100
170
|
context.ui.toast.show({
|
|
101
171
|
variant: "error",
|
|
102
172
|
title: "Agency launch",
|
|
103
|
-
message:
|
|
173
|
+
message:
|
|
174
|
+
"The task prompt was not observed as submitted after " +
|
|
175
|
+
String(dispatches) +
|
|
176
|
+
" automatic attempt(s). Press Enter to submit it manually.",
|
|
104
177
|
duration: 8_000,
|
|
105
178
|
})
|
|
106
179
|
return
|
|
107
180
|
}
|
|
108
|
-
|
|
181
|
+
|
|
182
|
+
const submitReady = context.keymap
|
|
183
|
+
.commands()
|
|
184
|
+
.some((command) => command.id === "prompt.submit")
|
|
185
|
+
if (route.type === "home" && submitReady) {
|
|
186
|
+
dispatches += 1
|
|
187
|
+
observe("dispatch", "prompt.submit")
|
|
188
|
+
try {
|
|
189
|
+
await context.keymap.dispatch("prompt.submit")
|
|
190
|
+
} catch (error) {
|
|
191
|
+
observe("error", "dispatch failed: " + String(error))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!stopped) timer = setTimeout(() => void attempt(), retryMs)
|
|
109
195
|
}
|
|
110
196
|
|
|
111
|
-
|
|
197
|
+
observe("marker", "autonomous prompt detected")
|
|
198
|
+
void attempt()
|
|
112
199
|
return stop
|
|
113
200
|
}
|
|
114
201
|
}
|