@llm4ts/flow 2.40.1 → 2.42.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/Activity.d.ts +6 -1
- package/dist/Activity.d.ts.map +1 -1
- package/dist/Activity.js +53 -5
- package/dist/Activity.js.map +1 -1
- package/dist/BoardSync.d.ts +3 -0
- package/dist/BoardSync.d.ts.map +1 -1
- package/dist/BoardSync.js +14 -0
- package/dist/BoardSync.js.map +1 -1
- package/dist/CostReport.d.ts +14 -1
- package/dist/CostReport.d.ts.map +1 -1
- package/dist/CostReport.js +43 -6
- package/dist/CostReport.js.map +1 -1
- package/dist/Flow.d.ts +26 -1
- package/dist/Flow.d.ts.map +1 -1
- package/dist/Flow.js +155 -133
- package/dist/Flow.js.map +1 -1
- package/dist/FlowContext.d.ts +5 -0
- package/dist/FlowContext.d.ts.map +1 -1
- package/dist/FlowContext.js.map +1 -1
- package/dist/FlowEvents.d.ts +69 -5
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +92 -6
- package/dist/FlowEvents.js.map +1 -1
- package/dist/Perimeter.d.ts +7 -0
- package/dist/Perimeter.d.ts.map +1 -1
- package/dist/Perimeter.js +31 -0
- package/dist/Perimeter.js.map +1 -1
- package/dist/Plan.d.ts +24 -0
- package/dist/Plan.d.ts.map +1 -1
- package/dist/Plan.js +52 -0
- package/dist/Plan.js.map +1 -1
- package/dist/PlanExecution.d.ts +39 -2
- package/dist/PlanExecution.d.ts.map +1 -1
- package/dist/PlanExecution.js +137 -3
- package/dist/PlanExecution.js.map +1 -1
- package/dist/Roster.d.ts +4 -2
- package/dist/Roster.d.ts.map +1 -1
- package/dist/Roster.js +50 -28
- package/dist/Roster.js.map +1 -1
- package/dist/RosterSeats.d.ts +11 -2
- package/dist/RosterSeats.d.ts.map +1 -1
- package/dist/RosterSeats.js +12 -5
- package/dist/RosterSeats.js.map +1 -1
- package/dist/Stories.d.ts +7 -0
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +157 -13
- package/dist/Stories.js.map +1 -1
- package/dist/Transcript.d.ts +4 -0
- package/dist/Transcript.d.ts.map +1 -1
- package/dist/Transcript.js +4 -0
- package/dist/Transcript.js.map +1 -1
- package/package.json +2 -2
- package/src/Activity.ts +73 -7
- package/src/BoardSync.ts +20 -0
- package/src/CostReport.ts +51 -6
- package/src/Flow.ts +108 -34
- package/src/FlowContext.ts +5 -0
- package/src/FlowEvents.ts +103 -6
- package/src/Perimeter.ts +36 -0
- package/src/Plan.ts +71 -0
- package/src/PlanExecution.ts +233 -4
- package/src/Roster.ts +60 -34
- package/src/RosterSeats.ts +31 -7
- package/src/Stories.ts +230 -17
- package/src/Transcript.ts +6 -0
package/src/CostReport.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as Option from "effect/Option"
|
|
|
3
3
|
import * as Schema from "effect/Schema"
|
|
4
4
|
import type { TokenUsage } from "@llm4ts/core/Models"
|
|
5
5
|
import { isEstimatedModel } from "./EstimatedUsage.ts"
|
|
6
|
-
import { TokensUsed } from "./FlowEvents.ts"
|
|
6
|
+
import { TokensUsed, cloneName } from "./FlowEvents.ts"
|
|
7
7
|
import type { TraceLine } from "./FlowRecorder.ts"
|
|
8
8
|
import { estimateCostUsd, PricesAsOf } from "./PriceList.ts"
|
|
9
9
|
|
|
@@ -31,7 +31,10 @@ export class UsageSample extends Schema.Class<UsageSample>("UsageSample")({
|
|
|
31
31
|
total: Schema.Int,
|
|
32
32
|
cached: Schema.optionalKey(Schema.Int),
|
|
33
33
|
costUsd: Schema.optionalKey(Schema.Number),
|
|
34
|
-
estimated: Schema.Boolean
|
|
34
|
+
estimated: Schema.Boolean,
|
|
35
|
+
/** The lane's executor and clone, when the usage came from a roster lane (ADR 0033). */
|
|
36
|
+
executor: Schema.optionalKey(Schema.String),
|
|
37
|
+
clone: Schema.optionalKey(Schema.Int)
|
|
35
38
|
}) {}
|
|
36
39
|
|
|
37
40
|
export class UsageTotals extends Schema.Class<UsageTotals>("UsageTotals")({
|
|
@@ -87,6 +90,13 @@ export class UsageProjection extends Schema.Class<UsageProjection>("UsageProject
|
|
|
87
90
|
costUsdPerDay: Schema.optionalKey(Schema.Number)
|
|
88
91
|
}) {}
|
|
89
92
|
|
|
93
|
+
/** One executor clone's usage (`codex#2`): the coder's calls on a lane that holds that clone. */
|
|
94
|
+
export class ExecutorUsage extends Schema.Class<ExecutorUsage>("ExecutorUsage")({
|
|
95
|
+
executor: Schema.String,
|
|
96
|
+
totals: UsageTotals,
|
|
97
|
+
share: Schema.Number
|
|
98
|
+
}) {}
|
|
99
|
+
|
|
90
100
|
export class CostReport extends Schema.Class<CostReport>("CostReport")({
|
|
91
101
|
schemaVersion: Schema.Int,
|
|
92
102
|
timeZone: Schema.String,
|
|
@@ -102,10 +112,12 @@ export class CostReport extends Schema.Class<CostReport>("CostReport")({
|
|
|
102
112
|
byDay: Schema.Array(UsageBucket),
|
|
103
113
|
byHour: Schema.Array(UsageBucket),
|
|
104
114
|
byModel: Schema.Array(ModelUsage),
|
|
105
|
-
byAgent: Schema.Array(AgentUsage)
|
|
115
|
+
byAgent: Schema.Array(AgentUsage),
|
|
116
|
+
/** Per executor clone, for lanes served by a roster (schema 3, ADR 0033). */
|
|
117
|
+
byExecutor: Schema.Array(ExecutorUsage)
|
|
106
118
|
}) {}
|
|
107
119
|
|
|
108
|
-
export const CurrentCostReportSchema =
|
|
120
|
+
export const CurrentCostReportSchema = 3
|
|
109
121
|
|
|
110
122
|
const tokensUsedCodec = Schema.fromJsonString(TokensUsed)
|
|
111
123
|
const decodeTokensUsed = Schema.decodeUnknownOption(tokensUsedCodec)
|
|
@@ -145,7 +157,9 @@ export const usageSamplesFromTrace = (
|
|
|
145
157
|
total: event.usage.total,
|
|
146
158
|
...(event.usage.cached === undefined ? {} : { cached: event.usage.cached }),
|
|
147
159
|
...(costUsd === undefined ? {} : { costUsd }),
|
|
148
|
-
estimated: isEstimatedModel(model)
|
|
160
|
+
estimated: isEstimatedModel(model),
|
|
161
|
+
...(event.executor === undefined ? {} : { executor: event.executor }),
|
|
162
|
+
...(event.clone === undefined ? {} : { clone: event.clone })
|
|
149
163
|
})
|
|
150
164
|
]
|
|
151
165
|
})
|
|
@@ -320,6 +334,26 @@ export const buildCostReport = (
|
|
|
320
334
|
right.totals.total - left.totals.total || left.agent.localeCompare(right.agent)
|
|
321
335
|
)
|
|
322
336
|
|
|
337
|
+
// Coder clones only: a sample that names the lane's executor without its
|
|
338
|
+
// clone is a reviewer's or judge's, which ran elsewhere (ADR 0033).
|
|
339
|
+
const byExecutorGroups = new Map<string, Array<UsageSample>>()
|
|
340
|
+
for (const sample of ordered) {
|
|
341
|
+
if (sample.executor !== undefined && sample.clone !== undefined) {
|
|
342
|
+
const key = cloneName(sample.executor, sample.clone)
|
|
343
|
+
byExecutorGroups.set(key, [...(byExecutorGroups.get(key) ?? []), sample])
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const byExecutor = [...byExecutorGroups.entries()]
|
|
347
|
+
.map(([executor, members]) => {
|
|
348
|
+
const executorTotals = totalsOf(members)
|
|
349
|
+
return ExecutorUsage.make({
|
|
350
|
+
executor,
|
|
351
|
+
totals: executorTotals,
|
|
352
|
+
share: tokens === 0 ? 0 : executorTotals.total / tokens
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
.sort((left, right) => left.executor.localeCompare(right.executor))
|
|
356
|
+
|
|
323
357
|
return CostReport.make({
|
|
324
358
|
schemaVersion: CurrentCostReportSchema,
|
|
325
359
|
timeZone: zoneLabel(timeZone),
|
|
@@ -349,7 +383,8 @@ export const buildCostReport = (
|
|
|
349
383
|
byDay,
|
|
350
384
|
byHour,
|
|
351
385
|
byModel,
|
|
352
|
-
byAgent
|
|
386
|
+
byAgent,
|
|
387
|
+
byExecutor
|
|
353
388
|
})
|
|
354
389
|
}
|
|
355
390
|
|
|
@@ -427,6 +462,16 @@ export const renderCostReport = (report: CostReport): string => {
|
|
|
427
462
|
)
|
|
428
463
|
)
|
|
429
464
|
}
|
|
465
|
+
if (report.byExecutor.length > 0) {
|
|
466
|
+
sections.push(
|
|
467
|
+
"",
|
|
468
|
+
"by executor clone:",
|
|
469
|
+
...report.byExecutor.map(
|
|
470
|
+
(entry) =>
|
|
471
|
+
` ${entry.executor} ${thousands(entry.totals.total)} tokens (${Math.round(entry.share * 100)}%)${money(entry.totals.costUsd)}`
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
}
|
|
430
475
|
const notes = [
|
|
431
476
|
`costs are backend-reported where available, otherwise estimated from the pricing table (rates as of ${report.pricesAsOf} — may be stale)`,
|
|
432
477
|
...(report.estimated.requests === 0
|
package/src/Flow.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from "node:path"
|
|
|
2
2
|
import * as Clock from "effect/Clock"
|
|
3
3
|
import * as Effect from "effect/Effect"
|
|
4
4
|
import * as Ref from "effect/Ref"
|
|
5
|
+
import * as Semaphore from "effect/Semaphore"
|
|
5
6
|
import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
6
7
|
import { collect } from "@llm4ts/core/Streaming"
|
|
7
8
|
import { withToolActivity } from "./Activity.ts"
|
|
@@ -25,10 +26,16 @@ import {
|
|
|
25
26
|
TokensUsed,
|
|
26
27
|
type FlowEventsShape
|
|
27
28
|
} from "./FlowEvents.ts"
|
|
28
|
-
import type
|
|
29
|
+
import { hasParallelTasks, type Plan, type Task } from "./Plan.ts"
|
|
29
30
|
import type { PlainFileStoreShape, PlanStoreShape } from "./Persistence.ts"
|
|
30
31
|
import { ensureBaseline } from "./GateTriage.ts"
|
|
31
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
implementTaskGraph,
|
|
34
|
+
implementTaskLoop,
|
|
35
|
+
stage,
|
|
36
|
+
type AsideOutcome,
|
|
37
|
+
type AsideRequest
|
|
38
|
+
} from "./PlanExecution.ts"
|
|
32
39
|
import { truth } from "@llm4ts/core/judgment/Schemas"
|
|
33
40
|
import { certaintyOf, decide, judgmentOf, type JudgmentMode } from "./Judgment.ts"
|
|
34
41
|
import {
|
|
@@ -74,6 +81,30 @@ export const completeAndPublish = Effect.fn("@llm4ts/flow/Flow.completeAndPublis
|
|
|
74
81
|
return response.content
|
|
75
82
|
})
|
|
76
83
|
|
|
84
|
+
/** What a task run aside replaces in the plan's options (ADR 0034). */
|
|
85
|
+
export interface TaskOverrides {
|
|
86
|
+
/** The gates in the aside worktree; `undefined` runs none. */
|
|
87
|
+
readonly lint?: Effect.Effect<ReviewResult, FlowError> | undefined
|
|
88
|
+
readonly triage?: GateTriageOptions
|
|
89
|
+
/** Present to replace the evidence check: the aside coder's tool calls are elsewhere. */
|
|
90
|
+
readonly onTaskReply?: ImplementPlanOptions["onTaskReply"] | undefined
|
|
91
|
+
/** The coder's system prompt there: its own working directory, its own paths. */
|
|
92
|
+
readonly system?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Tasks beside the held coder (ADR 0034): how many at once, and who runs one. */
|
|
96
|
+
export interface ParallelPlanTasks {
|
|
97
|
+
readonly concurrency: number
|
|
98
|
+
/**
|
|
99
|
+
* Runs one ready task on another coder: `run` is the task's whole body
|
|
100
|
+
* (coder, review, gates, commit) in the context it is given.
|
|
101
|
+
*/
|
|
102
|
+
readonly aside: (
|
|
103
|
+
request: AsideRequest,
|
|
104
|
+
run: (context: FlowContextShape, overrides: TaskOverrides) => Effect.Effect<void, FlowError>
|
|
105
|
+
) => Effect.Effect<AsideOutcome, FlowError>
|
|
106
|
+
}
|
|
107
|
+
|
|
77
108
|
export interface ImplementPlanOptions {
|
|
78
109
|
readonly store: PlanStoreShape
|
|
79
110
|
readonly planPath: string
|
|
@@ -86,6 +117,11 @@ export interface ImplementPlanOptions {
|
|
|
86
117
|
* one Chat is shared across every task in the plan.
|
|
87
118
|
*/
|
|
88
119
|
readonly chatPerTask?: boolean
|
|
120
|
+
/**
|
|
121
|
+
* Ready tasks run beside the held coder (ADR 0034), with `chatPerTask`
|
|
122
|
+
* only, and only in a plan whose tasks declare that they may.
|
|
123
|
+
*/
|
|
124
|
+
readonly parallel?: ParallelPlanTasks
|
|
89
125
|
readonly reviewers?: ReadonlyArray<Reviewer>
|
|
90
126
|
readonly commitMessage?: (plan: Plan, task: Task) => string
|
|
91
127
|
readonly checkoutBranch?: boolean
|
|
@@ -288,24 +324,26 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
288
324
|
})
|
|
289
325
|
}
|
|
290
326
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
327
|
+
const carryLock = yield* Semaphore.make(1)
|
|
328
|
+
/** The task's body in `ctx`: the plan's own context at home, another one aside (ADR 0034). */
|
|
329
|
+
const taskBody =
|
|
330
|
+
(ctx: FlowContextShape, over: TaskOverrides) =>
|
|
331
|
+
(task: Task, planSoFar: Plan): Effect.Effect<void, FlowError> => {
|
|
332
|
+
const lint = "lint" in over ? over.lint : options.lint
|
|
333
|
+
const onTaskReply = "onTaskReply" in over ? over.onTaskReply : options.onTaskReply
|
|
334
|
+
const system = over.system ?? options.system
|
|
335
|
+
return Effect.gen(function* () {
|
|
298
336
|
// `sharedCoder`'s definedness mirrors `options.chatPerTask !== true`
|
|
299
337
|
// above: when it's set, every task reuses it; when it's undefined,
|
|
300
338
|
// chatPerTask is active and each task builds its own fresh Chat.
|
|
301
339
|
let coder: Chat
|
|
302
|
-
if (sharedCoder !== undefined) {
|
|
340
|
+
if (sharedCoder !== undefined && ctx === context) {
|
|
303
341
|
coder = sharedCoder
|
|
304
342
|
} else {
|
|
305
|
-
coder = yield* makeChat(
|
|
306
|
-
events:
|
|
343
|
+
coder = yield* makeChat(ctx.coder, {
|
|
344
|
+
events: ctx.events,
|
|
307
345
|
agent: "coder",
|
|
308
|
-
system: composeSystem(
|
|
346
|
+
system: composeSystem(system, planSoFar.render),
|
|
309
347
|
...(options.stall === undefined ? {} : { stall: options.stall })
|
|
310
348
|
})
|
|
311
349
|
}
|
|
@@ -319,11 +357,17 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
319
357
|
const keepFindings = (reply: string): Effect.Effect<void, FlowError> =>
|
|
320
358
|
Effect.gen(function* () {
|
|
321
359
|
const found = findingsIn(reply)
|
|
322
|
-
|
|
323
|
-
|
|
360
|
+
const carry = options.carry
|
|
361
|
+
if (carry !== undefined && found !== undefined) {
|
|
362
|
+
// Tasks may run at once (ADR 0034): append to the notes as they are now.
|
|
363
|
+
yield* carryLock.withPermit(
|
|
364
|
+
Effect.flatMap(carry.read, (latest) =>
|
|
365
|
+
carry.write(appendNote(latest, task.title, found))
|
|
366
|
+
)
|
|
367
|
+
)
|
|
324
368
|
}
|
|
325
|
-
if (
|
|
326
|
-
yield*
|
|
369
|
+
if (onTaskReply !== undefined) {
|
|
370
|
+
yield* onTaskReply(task, reply, trailerIn(found), taskStartedAt)
|
|
327
371
|
}
|
|
328
372
|
})
|
|
329
373
|
yield* keepFindings(
|
|
@@ -333,7 +377,7 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
333
377
|
: `${withNotes(plan.taskPrompt(task), notes)}\n${findingsRequest}`
|
|
334
378
|
)
|
|
335
379
|
)
|
|
336
|
-
const produced = yield*
|
|
380
|
+
const produced = yield* ctx.git.diffAll
|
|
337
381
|
if (produced.trim().length === 0) {
|
|
338
382
|
// An empty diff is ambiguous: the task may be genuinely satisfied
|
|
339
383
|
// already, or the coder may simply have produced nothing. Ask
|
|
@@ -348,19 +392,19 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
348
392
|
].join("\n")
|
|
349
393
|
)
|
|
350
394
|
yield* keepFindings(confirmation)
|
|
351
|
-
const afterConfirmation = yield*
|
|
395
|
+
const afterConfirmation = yield* ctx.git.diffAll
|
|
352
396
|
if (afterConfirmation.trim().length === 0) {
|
|
353
397
|
const judged =
|
|
354
398
|
options.satisfiedProbe !== undefined && options.satisfiedProbe !== "literal"
|
|
355
399
|
? yield* satisfiedByJudgment(
|
|
356
|
-
|
|
400
|
+
ctx,
|
|
357
401
|
task.title,
|
|
358
402
|
confirmation,
|
|
359
403
|
options.satisfiedProbe === "judgment" ? "act" : options.satisfiedProbe.mode
|
|
360
404
|
)
|
|
361
405
|
: undefined
|
|
362
406
|
if (judged ?? confirmation.includes("TASK_ALREADY_SATISFIED")) {
|
|
363
|
-
yield*
|
|
407
|
+
yield* ctx.events.publish(
|
|
364
408
|
Info.make({
|
|
365
409
|
message: `task "${task.title}" confirmed already satisfied; skipping review and commit`
|
|
366
410
|
})
|
|
@@ -368,7 +412,7 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
368
412
|
return
|
|
369
413
|
}
|
|
370
414
|
if (options.noopTaskPolicy === "complete") {
|
|
371
|
-
yield*
|
|
415
|
+
yield* ctx.events.publish(
|
|
372
416
|
Info.make({
|
|
373
417
|
message: `task "${task.title}" produced no changes without confirming TASK_ALREADY_SATISFIED; marking complete per noopTaskPolicy`
|
|
374
418
|
})
|
|
@@ -380,7 +424,7 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
380
424
|
})
|
|
381
425
|
}
|
|
382
426
|
}
|
|
383
|
-
const triage = yield* triageFor(
|
|
427
|
+
const triage = over.triage ?? (yield* triageFor(ctx, options))
|
|
384
428
|
const repoRules = options.repoRules === undefined ? undefined : yield* options.repoRules
|
|
385
429
|
const reviewers = [
|
|
386
430
|
...(options.reviewers ?? minimalReviewers),
|
|
@@ -390,8 +434,8 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
390
434
|
options.fixer === "separate"
|
|
391
435
|
? (prompt: string): Effect.Effect<string, FlowError> =>
|
|
392
436
|
Effect.flatMap(
|
|
393
|
-
makeChat(
|
|
394
|
-
events:
|
|
437
|
+
makeChat(ctx.coder, {
|
|
438
|
+
events: ctx.events,
|
|
395
439
|
agent: "coder",
|
|
396
440
|
system: withContract(fixerBrief),
|
|
397
441
|
...(options.stall === undefined ? {} : { stall: options.stall })
|
|
@@ -403,7 +447,7 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
403
447
|
options.oracle === undefined
|
|
404
448
|
? undefined
|
|
405
449
|
: {
|
|
406
|
-
diff:
|
|
450
|
+
diff: ctx.git.diffAll,
|
|
407
451
|
...(options.oracle.rules === undefined ? {} : { rules: options.oracle.rules }),
|
|
408
452
|
...(options.oracle.testsChange === undefined
|
|
409
453
|
? {}
|
|
@@ -414,14 +458,14 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
414
458
|
}
|
|
415
459
|
yield* reviewAndFixLoop({
|
|
416
460
|
reviewers,
|
|
417
|
-
reviewerService: flowReviewer(
|
|
461
|
+
reviewerService: flowReviewer(ctx),
|
|
418
462
|
coder,
|
|
419
463
|
taskTitle: task.title,
|
|
420
|
-
currentDiff:
|
|
421
|
-
events:
|
|
464
|
+
currentDiff: ctx.git.diffAll,
|
|
465
|
+
events: ctx.events,
|
|
422
466
|
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds }),
|
|
423
467
|
...(options.settle === undefined ? {} : { settle: options.settle }),
|
|
424
|
-
...(
|
|
468
|
+
...(lint === undefined ? {} : { lint }),
|
|
425
469
|
...(options.format === undefined ? {} : { format: options.format }),
|
|
426
470
|
...(triage === undefined ? {} : { triage }),
|
|
427
471
|
...(oracle === undefined ? {} : { oracle }),
|
|
@@ -439,12 +483,12 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
439
483
|
: options.onReview(task, round, result, settled)
|
|
440
484
|
})
|
|
441
485
|
})
|
|
442
|
-
if (
|
|
486
|
+
if (lint !== undefined) {
|
|
443
487
|
const reported = yield* Ref.make<ReadonlySet<string>>(new Set())
|
|
444
488
|
const gate = yield* withOracle(
|
|
445
|
-
yield* applyTriage(yield*
|
|
489
|
+
yield* applyTriage(yield* lint, triage, ctx.events, reported),
|
|
446
490
|
oracle,
|
|
447
|
-
|
|
491
|
+
ctx.events,
|
|
448
492
|
yield* Ref.make(true)
|
|
449
493
|
)
|
|
450
494
|
if (!gate.isClean) {
|
|
@@ -460,7 +504,37 @@ export const implementPlanFlow = Effect.fn("@llm4ts/flow/Flow.implementPlan")(fu
|
|
|
460
504
|
})
|
|
461
505
|
}
|
|
462
506
|
}
|
|
463
|
-
yield*
|
|
507
|
+
yield* ctx.git.commitAll((options.commitMessage ?? defaultCommitMessage)(plan, task))
|
|
464
508
|
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const parallel = options.parallel
|
|
512
|
+
if (
|
|
513
|
+
parallel !== undefined &&
|
|
514
|
+
parallel.concurrency > 1 &&
|
|
515
|
+
options.chatPerTask === true &&
|
|
516
|
+
hasParallelTasks(plan)
|
|
517
|
+
) {
|
|
518
|
+
return yield* implementTaskGraph(
|
|
519
|
+
options.store,
|
|
520
|
+
context.events,
|
|
521
|
+
options.planPath,
|
|
522
|
+
plan,
|
|
523
|
+
taskBody(context, {}),
|
|
524
|
+
{
|
|
525
|
+
concurrency: parallel.concurrency,
|
|
526
|
+
aside: (request) =>
|
|
527
|
+
parallel.aside(request, (ctx, over) =>
|
|
528
|
+
taskBody(ctx, over)(request.node.task, request.planSoFar)
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
return yield* implementTaskLoop(
|
|
534
|
+
options.store,
|
|
535
|
+
context.events,
|
|
536
|
+
options.planPath,
|
|
537
|
+
plan,
|
|
538
|
+
taskBody(context, {})
|
|
465
539
|
)
|
|
466
540
|
})
|
package/src/FlowContext.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface ContextOptions {
|
|
|
16
16
|
readonly prefer?: string
|
|
17
17
|
/** Who the context is for, in the roster's events ("story conto-overview"). */
|
|
18
18
|
readonly label?: string
|
|
19
|
+
/**
|
|
20
|
+
* Take a coder only if one is free now, else fail `RosterExhausted` at
|
|
21
|
+
* once: a task offered aside never waits for a slot (ADR 0034).
|
|
22
|
+
*/
|
|
23
|
+
readonly ifFree?: boolean
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface FlowContextShape {
|
package/src/FlowEvents.ts
CHANGED
|
@@ -88,6 +88,8 @@ export class Info extends Schema.TaggedClass<Info>()("Info", {
|
|
|
88
88
|
export class ToolUse extends Schema.TaggedClass<ToolUse>()("ToolUse", {
|
|
89
89
|
tool: Schema.String,
|
|
90
90
|
args: Schema.String,
|
|
91
|
+
/** The tool call this one runs inside, when a harness delegated it to a sub-agent (ADR 0033). */
|
|
92
|
+
parent: Schema.optionalKey(Schema.String),
|
|
91
93
|
/** The concurrent unit (a story) this event belongs to; absent for run-wide events. */
|
|
92
94
|
lane: Schema.optionalKey(Schema.String),
|
|
93
95
|
/** The roster executor working that lane, when there is one (ADR 0019). */
|
|
@@ -109,7 +111,9 @@ export class TokensUsed extends Schema.TaggedClass<TokensUsed>()("TokensUsed", {
|
|
|
109
111
|
/** The concurrent unit (a story) this event belongs to; absent for run-wide events. */
|
|
110
112
|
lane: Schema.optionalKey(Schema.String),
|
|
111
113
|
/** The roster executor working that lane, when there is one (ADR 0019). */
|
|
112
|
-
executor: Schema.optionalKey(Schema.String)
|
|
114
|
+
executor: Schema.optionalKey(Schema.String),
|
|
115
|
+
/** The executor's clone that made the call, when the lane's coder holds a slot (ADR 0033). */
|
|
116
|
+
clone: Schema.optionalKey(Schema.Int)
|
|
113
117
|
}) {}
|
|
114
118
|
|
|
115
119
|
/** One issue a review round found, as the operator sees it. */
|
|
@@ -270,9 +274,58 @@ export class StoryJudged extends Schema.TaggedClass<StoryJudged>()("StoryJudged"
|
|
|
270
274
|
* The executor roster's events (ADR 0019, 0022). `label` names who holds the
|
|
271
275
|
* lease — a story's id, or "the run" — so a view can put an executor on its lane.
|
|
272
276
|
*/
|
|
277
|
+
/** One task of a story's plan as the task loop announces it (ADR 0033). */
|
|
278
|
+
export const PlannedTask = Schema.Struct({
|
|
279
|
+
title: Schema.String,
|
|
280
|
+
completed: Schema.Boolean,
|
|
281
|
+
/** The acceptance criteria the task's description names (`Satisfies: 1, 3`). */
|
|
282
|
+
satisfies: Schema.optionalKey(Schema.Array(Schema.Int))
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
/** The task loop's plan, before its first task: every task in order (ADR 0033). */
|
|
286
|
+
export class TasksPlanned extends Schema.TaggedClass<TasksPlanned>()("TasksPlanned", {
|
|
287
|
+
tasks: Schema.Array(PlannedTask),
|
|
288
|
+
lane: Schema.optionalKey(Schema.String),
|
|
289
|
+
executor: Schema.optionalKey(Schema.String)
|
|
290
|
+
}) {}
|
|
291
|
+
|
|
292
|
+
/** A task begins: its position in the plan, from 1, and the plan's size (ADR 0033). */
|
|
293
|
+
export class TaskStarted extends Schema.TaggedClass<TaskStarted>()("TaskStarted", {
|
|
294
|
+
index: Schema.Int,
|
|
295
|
+
count: Schema.Int,
|
|
296
|
+
title: Schema.String,
|
|
297
|
+
satisfies: Schema.optionalKey(Schema.Array(Schema.Int)),
|
|
298
|
+
/** The task runs beside the story's held coder, in its own worktree (ADR 0034). */
|
|
299
|
+
parallel: Schema.optionalKey(Schema.Boolean),
|
|
300
|
+
/** The clone of the extra coder a parallel task holds (ADR 0034). */
|
|
301
|
+
clone: Schema.optionalKey(Schema.Int),
|
|
302
|
+
lane: Schema.optionalKey(Schema.String),
|
|
303
|
+
executor: Schema.optionalKey(Schema.String)
|
|
304
|
+
}) {}
|
|
305
|
+
|
|
306
|
+
/** A task is ticked in the plan (ADR 0033). */
|
|
307
|
+
export class TaskCompleted extends Schema.TaggedClass<TaskCompleted>()("TaskCompleted", {
|
|
308
|
+
index: Schema.Int,
|
|
309
|
+
count: Schema.Int,
|
|
310
|
+
title: Schema.String,
|
|
311
|
+
lane: Schema.optionalKey(Schema.String),
|
|
312
|
+
executor: Schema.optionalKey(Schema.String)
|
|
313
|
+
}) {}
|
|
314
|
+
|
|
315
|
+
/** A story moved on the epic's board (ADR 0033); the statuses are `BoardSync`'s. */
|
|
316
|
+
export class StoryStatusChanged extends Schema.TaggedClass<StoryStatusChanged>()(
|
|
317
|
+
"StoryStatusChanged",
|
|
318
|
+
{
|
|
319
|
+
id: Schema.String,
|
|
320
|
+
status: Schema.Literals(["planned", "active", "waiting", "done", "failed", "skipped"])
|
|
321
|
+
}
|
|
322
|
+
) {}
|
|
323
|
+
|
|
273
324
|
export class ExecutorLeased extends Schema.TaggedClass<ExecutorLeased>()("ExecutorLeased", {
|
|
274
325
|
executor: Schema.String,
|
|
275
326
|
role: Schema.String,
|
|
327
|
+
/** Which of the executor's clones took the slot, from 1 (ADR 0033); absent when borrowed or before 2.41. */
|
|
328
|
+
clone: Schema.optionalKey(Schema.Int),
|
|
276
329
|
label: Schema.optionalKey(Schema.String),
|
|
277
330
|
/** What the call is for beyond its role: a review lens, a vote. */
|
|
278
331
|
purpose: Schema.optionalKey(Schema.String),
|
|
@@ -290,6 +343,8 @@ export class ExecutorLeased extends Schema.TaggedClass<ExecutorLeased>()("Execut
|
|
|
290
343
|
export class ExecutorReleased extends Schema.TaggedClass<ExecutorReleased>()("ExecutorReleased", {
|
|
291
344
|
executor: Schema.String,
|
|
292
345
|
role: Schema.String,
|
|
346
|
+
/** Which of the executor's clones freed the slot (ADR 0033); absent when borrowed or before 2.41. */
|
|
347
|
+
clone: Schema.optionalKey(Schema.Int),
|
|
293
348
|
label: Schema.optionalKey(Schema.String)
|
|
294
349
|
}) {}
|
|
295
350
|
|
|
@@ -335,12 +390,16 @@ const resumedWords: Readonly<Record<ExecutorResumed["why"], string>> = {
|
|
|
335
390
|
* the roster used to publish as `Info` — or `undefined` for one it does not
|
|
336
391
|
* show (a release) and for any other event.
|
|
337
392
|
*/
|
|
393
|
+
/** `codex#2` for a numbered clone, the bare executor otherwise (ADR 0033). */
|
|
394
|
+
export const cloneName = (executor: string, clone: number | undefined): string =>
|
|
395
|
+
clone === undefined ? executor : `${executor}#${clone}`
|
|
396
|
+
|
|
338
397
|
export const rosterEventMessage = (event: FlowEvent): string | undefined => {
|
|
339
398
|
const forLabel = (label: string | undefined): string =>
|
|
340
399
|
label === undefined ? "" : ` for ${label}`
|
|
341
400
|
switch (event._tag) {
|
|
342
401
|
case "ExecutorLeased": {
|
|
343
|
-
const taken = `roster: ${event.executor} takes ${event.role}${forLabel(event.label)}${
|
|
402
|
+
const taken = `roster: ${cloneName(event.executor, event.clone)} takes ${event.role}${forLabel(event.label)}${
|
|
344
403
|
event.purpose === undefined ? "" : ` · ${event.purpose}`
|
|
345
404
|
}`
|
|
346
405
|
if (event.because === undefined && event.borrowed !== true) {
|
|
@@ -395,7 +454,11 @@ export const FlowEvent = Schema.Union([
|
|
|
395
454
|
ExecutorHandedOver,
|
|
396
455
|
StoryJudged,
|
|
397
456
|
Timed,
|
|
398
|
-
Began
|
|
457
|
+
Began,
|
|
458
|
+
TasksPlanned,
|
|
459
|
+
TaskStarted,
|
|
460
|
+
TaskCompleted,
|
|
461
|
+
StoryStatusChanged
|
|
399
462
|
])
|
|
400
463
|
export type FlowEvent = typeof FlowEvent.Type
|
|
401
464
|
|
|
@@ -419,6 +482,8 @@ export interface Lane {
|
|
|
419
482
|
readonly lane: string
|
|
420
483
|
/** The executor working it now; read at every publish, so a handover shows. */
|
|
421
484
|
readonly executor?: Effect.Effect<string | undefined>
|
|
485
|
+
/** The coder's clone number, read at every publish as `executor` is (ADR 0033). */
|
|
486
|
+
readonly clone?: Effect.Effect<number | undefined>
|
|
422
487
|
/** The lane's working directory, dropped from tool arguments (the lane already names it). */
|
|
423
488
|
readonly workDir?: string
|
|
424
489
|
}
|
|
@@ -436,10 +501,37 @@ const stamped = (
|
|
|
436
501
|
event: FlowEvent,
|
|
437
502
|
lane: string,
|
|
438
503
|
executor: string | undefined,
|
|
439
|
-
workDir: string | undefined
|
|
504
|
+
workDir: string | undefined,
|
|
505
|
+
clone: number | undefined
|
|
440
506
|
): FlowEvent => {
|
|
441
507
|
const tags = { lane, ...(executor === undefined ? {} : { executor }) }
|
|
508
|
+
const usageTags = { ...tags, ...(clone === undefined ? {} : { clone }) }
|
|
442
509
|
switch (event._tag) {
|
|
510
|
+
case "TasksPlanned":
|
|
511
|
+
return event.lane !== undefined ? event : TasksPlanned.make({ tasks: event.tasks, ...tags })
|
|
512
|
+
case "TaskStarted":
|
|
513
|
+
return event.lane !== undefined
|
|
514
|
+
? event
|
|
515
|
+
: TaskStarted.make({
|
|
516
|
+
index: event.index,
|
|
517
|
+
count: event.count,
|
|
518
|
+
title: event.title,
|
|
519
|
+
...(event.satisfies === undefined ? {} : { satisfies: event.satisfies }),
|
|
520
|
+
...tags,
|
|
521
|
+
// A parallel task names its own coder, not the lane's (ADR 0034).
|
|
522
|
+
...(event.parallel === undefined ? {} : { parallel: event.parallel }),
|
|
523
|
+
...(event.executor === undefined ? {} : { executor: event.executor }),
|
|
524
|
+
...(event.clone === undefined ? {} : { clone: event.clone })
|
|
525
|
+
})
|
|
526
|
+
case "TaskCompleted":
|
|
527
|
+
return event.lane !== undefined
|
|
528
|
+
? event
|
|
529
|
+
: TaskCompleted.make({
|
|
530
|
+
index: event.index,
|
|
531
|
+
count: event.count,
|
|
532
|
+
title: event.title,
|
|
533
|
+
...tags
|
|
534
|
+
})
|
|
443
535
|
case "StageStarted":
|
|
444
536
|
return event.lane !== undefined ? event : StageStarted.make({ stage: event.stage, ...tags })
|
|
445
537
|
case "StageCompleted":
|
|
@@ -456,6 +548,7 @@ const stamped = (
|
|
|
456
548
|
: ToolUse.make({
|
|
457
549
|
tool: event.tool,
|
|
458
550
|
args: shortenLaneArgs(event.args, workDir),
|
|
551
|
+
...(event.parent === undefined ? {} : { parent: event.parent }),
|
|
459
552
|
...tags
|
|
460
553
|
})
|
|
461
554
|
case "AssistantMessage":
|
|
@@ -485,7 +578,9 @@ const stamped = (
|
|
|
485
578
|
agent: event.agent,
|
|
486
579
|
usage: event.usage,
|
|
487
580
|
...(event.model === undefined ? {} : { model: event.model }),
|
|
488
|
-
|
|
581
|
+
// The lane's clone is its coder's: a reviewer or judge on the
|
|
582
|
+
// lane's events ran on another executor (ADR 0033).
|
|
583
|
+
...(event.agent === "coder" ? usageTags : tags)
|
|
489
584
|
})
|
|
490
585
|
// The lane's executor is its coder's: it names a coder call or tool, not
|
|
491
586
|
// a reviewer's or judge's call made on another executor.
|
|
@@ -523,7 +618,9 @@ const stamped = (
|
|
|
523
618
|
export const withLane = (events: FlowEventsShape, lane: Lane): FlowEventsShape => ({
|
|
524
619
|
publish: (event) =>
|
|
525
620
|
Effect.flatMap(lane.executor ?? Effect.succeed(undefined), (executor) =>
|
|
526
|
-
|
|
621
|
+
Effect.flatMap(lane.clone ?? Effect.succeed(undefined), (clone) =>
|
|
622
|
+
events.publish(stamped(event, lane.lane, executor, lane.workDir, clone))
|
|
623
|
+
)
|
|
527
624
|
)
|
|
528
625
|
})
|
|
529
626
|
|
package/src/Perimeter.ts
CHANGED
|
@@ -131,3 +131,39 @@ export const strayTasks = (
|
|
|
131
131
|
})
|
|
132
132
|
return [{ task, paths, foreign }]
|
|
133
133
|
})
|
|
134
|
+
|
|
135
|
+
/** A task's `Owns:` entry as a path prefix: `src/session/*` and `src/routes/**` own their directory. */
|
|
136
|
+
const ownedPrefix = (entry: string): string => entry.replace(/\/?\*+$/u, "")
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A task run aside (ADR 0034) as a gate over what it changed: every path
|
|
140
|
+
* outside its `Owns:` entries is one Critical issue handed back to its
|
|
141
|
+
* coder, so two tasks running at once keep to their own files. A task that
|
|
142
|
+
* names no paths is not checked; the story's perimeter still is.
|
|
143
|
+
*/
|
|
144
|
+
export const taskOwnsGate = (
|
|
145
|
+
changedPaths: ReadonlyArray<string>,
|
|
146
|
+
owns: ReadonlyArray<string>
|
|
147
|
+
): ReviewResult => {
|
|
148
|
+
if (owns.length === 0) {
|
|
149
|
+
return ReviewResult.make({ issues: [], summary: "" })
|
|
150
|
+
}
|
|
151
|
+
const prefixes = owns.map(ownedPrefix)
|
|
152
|
+
const stray = changedPaths.filter((path) => !prefixes.some((prefix) => pathWithin(path, prefix)))
|
|
153
|
+
return stray.length === 0
|
|
154
|
+
? ReviewResult.make({ issues: [], summary: "task owns: clean" })
|
|
155
|
+
: ReviewResult.make({
|
|
156
|
+
issues: [
|
|
157
|
+
ReviewIssue.make({
|
|
158
|
+
severity: "Critical",
|
|
159
|
+
title: "task owns",
|
|
160
|
+
description: [
|
|
161
|
+
`This task owns ${owns.join(", ")} and changed paths outside them: ${stray.join(", ")}.`,
|
|
162
|
+
"Another task may be changing those right now. Revert them and keep this task's",
|
|
163
|
+
"work inside the paths it owns."
|
|
164
|
+
].join("\n")
|
|
165
|
+
})
|
|
166
|
+
],
|
|
167
|
+
summary: "task owns: violated"
|
|
168
|
+
})
|
|
169
|
+
}
|