@llm4ts/flow 2.35.3 → 2.36.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/dist/AutonomyContract.d.ts +3 -2
- package/dist/AutonomyContract.d.ts.map +1 -1
- package/dist/AutonomyContract.js +17 -4
- package/dist/AutonomyContract.js.map +1 -1
- package/dist/CostReport.d.ts +11 -1
- package/dist/CostReport.d.ts.map +1 -1
- package/dist/CostReport.js +30 -3
- package/dist/CostReport.js.map +1 -1
- package/dist/Diagnostics.d.ts +5 -3
- package/dist/Diagnostics.d.ts.map +1 -1
- package/dist/Diagnostics.js +31 -3
- package/dist/Diagnostics.js.map +1 -1
- package/dist/Pack.d.ts +4 -3
- package/dist/Pack.d.ts.map +1 -1
- package/dist/Pack.js +2 -5
- package/dist/Pack.js.map +1 -1
- package/dist/Roster.d.ts +10 -0
- package/dist/Roster.d.ts.map +1 -1
- package/dist/Roster.js +23 -1
- package/dist/Roster.js.map +1 -1
- package/dist/WorkQueue.d.ts +19 -3
- package/dist/WorkQueue.d.ts.map +1 -1
- package/dist/WorkQueue.js +11 -3
- package/dist/WorkQueue.js.map +1 -1
- package/package.json +2 -2
- package/src/AutonomyContract.ts +18 -4
- package/src/CostReport.ts +41 -3
- package/src/Diagnostics.ts +34 -3
- package/src/Pack.ts +5 -9
- package/src/Roster.ts +26 -1
- package/src/WorkQueue.ts +40 -6
package/src/Roster.ts
CHANGED
|
@@ -13,6 +13,7 @@ import * as Schema from "effect/Schema"
|
|
|
13
13
|
import type * as Scope from "effect/Scope"
|
|
14
14
|
import * as Semaphore from "effect/Semaphore"
|
|
15
15
|
import type { LlmError } from "@llm4ts/core/Errors"
|
|
16
|
+
import { Effort } from "@llm4ts/core/Models"
|
|
16
17
|
import { RosterExhausted, type FlowError } from "./FlowError.ts"
|
|
17
18
|
import {
|
|
18
19
|
Began,
|
|
@@ -66,7 +67,14 @@ export class ExecutorSpec extends Schema.Class<ExecutorSpec>("ExecutorSpec")({
|
|
|
66
67
|
/** In a repository roster: removes the user roster's entry with this id. */
|
|
67
68
|
disabled: Schema.optionalKey(Schema.Boolean),
|
|
68
69
|
/** The autonomy contract profile this executor's coder prompts carry (ADR 0027); default `full`. */
|
|
69
|
-
contract: Schema.optionalKey(Schema.Literals(["full", "minimal", "off"]))
|
|
70
|
+
contract: Schema.optionalKey(Schema.Literals(["full", "minimal", "off"])),
|
|
71
|
+
/**
|
|
72
|
+
* Reasoning effort (ADR 0029): one for every role, or per role with a
|
|
73
|
+
* `default`, mapped to the harness's own flag; absent keeps its default.
|
|
74
|
+
*/
|
|
75
|
+
effort: Schema.optionalKey(Schema.Union([Effort, Schema.Record(Schema.String, Effort)])),
|
|
76
|
+
/** Run the harness without the repository's own hooks, MCP servers, settings and instructions (ADR 0029). */
|
|
77
|
+
isolated: Schema.optionalKey(Schema.Boolean)
|
|
70
78
|
}) {}
|
|
71
79
|
|
|
72
80
|
export class RosterDocument extends Schema.Class<RosterDocument>("RosterDocument")({
|
|
@@ -117,6 +125,15 @@ export const coderSlotsOf = (spec: ExecutorSpec): number => {
|
|
|
117
125
|
return hasRole(spec, "coder") && reasons && slots > 1 ? slots - 1 : slots
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
/** The effort a role runs at on this executor (ADR 0029); absent is the harness's default. */
|
|
129
|
+
export const effortOf = (spec: ExecutorSpec, role: Role): Effort | undefined => {
|
|
130
|
+
const effort = spec.effort
|
|
131
|
+
if (effort === undefined || typeof effort === "string") {
|
|
132
|
+
return effort
|
|
133
|
+
}
|
|
134
|
+
return effort[role] ?? effort.default
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
export const priorityOf = (spec: ExecutorSpec, role: Role): number => {
|
|
121
138
|
const priority = spec.priority
|
|
122
139
|
if (priority === undefined) {
|
|
@@ -207,6 +224,14 @@ export const rosterViolations = (document: RosterDocument): ReadonlyArray<string
|
|
|
207
224
|
violations.push(`${where} has an unreadable ${name} cooldown '${text}'`)
|
|
208
225
|
}
|
|
209
226
|
}
|
|
227
|
+
if (spec.effort !== undefined && typeof spec.effort !== "string") {
|
|
228
|
+
const unknown = Object.keys(spec.effort).filter((key) => key !== "default" && !isRole(key))
|
|
229
|
+
if (unknown.length > 0) {
|
|
230
|
+
violations.push(
|
|
231
|
+
`${where} sets effort for unknown role(s) ${unknown.join(", ")} (roles: ${roles.join(", ")}, or default)`
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
210
235
|
}
|
|
211
236
|
return violations
|
|
212
237
|
}
|
package/src/WorkQueue.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// feature with a DAG and a judge, a queue item is a file with a predicate.
|
|
6
6
|
import * as Clock from "effect/Clock"
|
|
7
7
|
import * as Effect from "effect/Effect"
|
|
8
|
+
import * as Ref from "effect/Ref"
|
|
8
9
|
import * as Schema from "effect/Schema"
|
|
9
10
|
import { Stalled, describeFlowError, type FlowError } from "./FlowError.ts"
|
|
10
11
|
import {
|
|
@@ -20,6 +21,17 @@ export interface QueueItem {
|
|
|
20
21
|
readonly id: string
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* One checkout the queue may work in (ADR 0028, decided later): a git
|
|
26
|
+
* worktree of its own, so units that build or edit shared files do not
|
|
27
|
+
* trip over each other. The flow creates, merges and removes shards; the
|
|
28
|
+
* queue only hands each in-flight item one and never two items the same.
|
|
29
|
+
*/
|
|
30
|
+
export interface Shard {
|
|
31
|
+
readonly id: string
|
|
32
|
+
readonly dir: string
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
export const Confidence = Schema.Literals(["high", "medium", "low"])
|
|
24
36
|
|
|
25
37
|
/** One line of the ledger: what happened to one item in one round. */
|
|
@@ -47,12 +59,18 @@ export interface WorkResult {
|
|
|
47
59
|
export interface RunQueueOptions<Item extends QueueItem> {
|
|
48
60
|
readonly label: string
|
|
49
61
|
readonly items: ReadonlyArray<Item>
|
|
50
|
-
/**
|
|
51
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The filesystem predicate: an item whose output exists is done, whatever
|
|
64
|
+
* the ledger says. Right after `work` it is asked with the shard the work
|
|
65
|
+
* ran in, before the first round without one.
|
|
66
|
+
*/
|
|
67
|
+
readonly done: (item: Item, shard?: Shard) => Effect.Effect<boolean, FlowError>
|
|
52
68
|
/** One unit of work: implement, review, fix; the queue catches its failure. */
|
|
53
|
-
readonly work: (item: Item, round: number) => Effect.Effect<WorkResult, FlowError>
|
|
69
|
+
readonly work: (item: Item, round: number, shard?: Shard) => Effect.Effect<WorkResult, FlowError>
|
|
54
70
|
readonly events: FlowEventsShape
|
|
55
71
|
readonly concurrency?: number
|
|
72
|
+
/** Checkouts to spread the work over; at most one in-flight item per shard, so they also cap concurrency. */
|
|
73
|
+
readonly shards?: ReadonlyArray<Shard>
|
|
56
74
|
/** Rounds over the items that failed; default 3. */
|
|
57
75
|
readonly maxRounds?: number
|
|
58
76
|
/** Where outcomes are appended, one JSON line each; omit to keep none. */
|
|
@@ -95,8 +113,22 @@ export const readLedger = (
|
|
|
95
113
|
export const runQueue = Effect.fn("@llm4ts/flow/WorkQueue.run")(function* <Item extends QueueItem>(
|
|
96
114
|
options: RunQueueOptions<Item>
|
|
97
115
|
): Effect.fn.Return<QueueReport<Item>, FlowError> {
|
|
98
|
-
const
|
|
116
|
+
const shards = options.shards ?? []
|
|
117
|
+
const concurrency = Math.max(
|
|
118
|
+
1,
|
|
119
|
+
Math.min(
|
|
120
|
+
options.concurrency ?? 4,
|
|
121
|
+
shards.length === 0 ? Number.POSITIVE_INFINITY : shards.length
|
|
122
|
+
)
|
|
123
|
+
)
|
|
99
124
|
const maxRounds = Math.max(1, options.maxRounds ?? 3)
|
|
125
|
+
// The free shards: an item takes one before its work and returns it after.
|
|
126
|
+
const free = yield* Ref.make<ReadonlyArray<Shard>>(shards)
|
|
127
|
+
const takeShard: Effect.Effect<Shard | undefined> = Ref.modify(free, (list) =>
|
|
128
|
+
list.length === 0 ? [undefined, list] : [list[0], list.slice(1)]
|
|
129
|
+
)
|
|
130
|
+
const giveShard = (shard: Shard | undefined): Effect.Effect<void> =>
|
|
131
|
+
shard === undefined ? Effect.void : Ref.update(free, (list) => [...list, shard])
|
|
100
132
|
const outcomes: Array<QueueOutcome> = []
|
|
101
133
|
const record = (outcome: QueueOutcome): Effect.Effect<void, FlowError> =>
|
|
102
134
|
Effect.gen(function* () {
|
|
@@ -137,7 +169,8 @@ export const runQueue = Effect.fn("@llm4ts/flow/WorkQueue.run")(function* <Item
|
|
|
137
169
|
const stage = `${options.label} ${item.id}`
|
|
138
170
|
yield* options.events.publish(StageStarted.make({ stage, lane: item.id }))
|
|
139
171
|
const started = yield* Clock.currentTimeMillis
|
|
140
|
-
const
|
|
172
|
+
const shard = yield* takeShard
|
|
173
|
+
const result = yield* options.work(item, round, shard).pipe(
|
|
141
174
|
Effect.map((value) => ({ ok: true as const, value })),
|
|
142
175
|
Effect.catch((error: FlowError) =>
|
|
143
176
|
Effect.succeed({ ok: false as const, error: describeFlowError(error) })
|
|
@@ -145,7 +178,8 @@ export const runQueue = Effect.fn("@llm4ts/flow/WorkQueue.run")(function* <Item
|
|
|
145
178
|
)
|
|
146
179
|
const ended = yield* Clock.currentTimeMillis
|
|
147
180
|
// The predicate decides, not the work's own account of itself.
|
|
148
|
-
const finished = result.ok && (yield* options.done(item))
|
|
181
|
+
const finished = result.ok && (yield* options.done(item, shard))
|
|
182
|
+
yield* giveShard(shard)
|
|
149
183
|
const outcome = QueueOutcome.make({
|
|
150
184
|
id: item.id,
|
|
151
185
|
round,
|