@llm4ts/flow 2.40.1 → 2.41.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/FlowEvents.d.ts +65 -5
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +84 -6
- package/dist/FlowEvents.js.map +1 -1
- package/dist/PlanExecution.d.ts +2 -0
- package/dist/PlanExecution.d.ts.map +1 -1
- package/dist/PlanExecution.js +31 -2
- 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 +2 -0
- package/dist/RosterSeats.d.ts.map +1 -1
- package/dist/RosterSeats.js +1 -0
- package/dist/RosterSeats.js.map +1 -1
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +4 -2
- 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/FlowEvents.ts +95 -6
- package/src/PlanExecution.ts +45 -2
- package/src/Roster.ts +60 -34
- package/src/RosterSeats.ts +3 -0
- package/src/Stories.ts +4 -2
- 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/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,54 @@ 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
|
+
lane: Schema.optionalKey(Schema.String),
|
|
299
|
+
executor: Schema.optionalKey(Schema.String)
|
|
300
|
+
}) {}
|
|
301
|
+
|
|
302
|
+
/** A task is ticked in the plan (ADR 0033). */
|
|
303
|
+
export class TaskCompleted extends Schema.TaggedClass<TaskCompleted>()("TaskCompleted", {
|
|
304
|
+
index: Schema.Int,
|
|
305
|
+
count: Schema.Int,
|
|
306
|
+
title: Schema.String,
|
|
307
|
+
lane: Schema.optionalKey(Schema.String),
|
|
308
|
+
executor: Schema.optionalKey(Schema.String)
|
|
309
|
+
}) {}
|
|
310
|
+
|
|
311
|
+
/** A story moved on the epic's board (ADR 0033); the statuses are `BoardSync`'s. */
|
|
312
|
+
export class StoryStatusChanged extends Schema.TaggedClass<StoryStatusChanged>()(
|
|
313
|
+
"StoryStatusChanged",
|
|
314
|
+
{
|
|
315
|
+
id: Schema.String,
|
|
316
|
+
status: Schema.Literals(["planned", "active", "waiting", "done", "failed", "skipped"])
|
|
317
|
+
}
|
|
318
|
+
) {}
|
|
319
|
+
|
|
273
320
|
export class ExecutorLeased extends Schema.TaggedClass<ExecutorLeased>()("ExecutorLeased", {
|
|
274
321
|
executor: Schema.String,
|
|
275
322
|
role: Schema.String,
|
|
323
|
+
/** Which of the executor's clones took the slot, from 1 (ADR 0033); absent when borrowed or before 2.41. */
|
|
324
|
+
clone: Schema.optionalKey(Schema.Int),
|
|
276
325
|
label: Schema.optionalKey(Schema.String),
|
|
277
326
|
/** What the call is for beyond its role: a review lens, a vote. */
|
|
278
327
|
purpose: Schema.optionalKey(Schema.String),
|
|
@@ -290,6 +339,8 @@ export class ExecutorLeased extends Schema.TaggedClass<ExecutorLeased>()("Execut
|
|
|
290
339
|
export class ExecutorReleased extends Schema.TaggedClass<ExecutorReleased>()("ExecutorReleased", {
|
|
291
340
|
executor: Schema.String,
|
|
292
341
|
role: Schema.String,
|
|
342
|
+
/** Which of the executor's clones freed the slot (ADR 0033); absent when borrowed or before 2.41. */
|
|
343
|
+
clone: Schema.optionalKey(Schema.Int),
|
|
293
344
|
label: Schema.optionalKey(Schema.String)
|
|
294
345
|
}) {}
|
|
295
346
|
|
|
@@ -335,12 +386,16 @@ const resumedWords: Readonly<Record<ExecutorResumed["why"], string>> = {
|
|
|
335
386
|
* the roster used to publish as `Info` — or `undefined` for one it does not
|
|
336
387
|
* show (a release) and for any other event.
|
|
337
388
|
*/
|
|
389
|
+
/** `codex#2` for a numbered clone, the bare executor otherwise (ADR 0033). */
|
|
390
|
+
export const cloneName = (executor: string, clone: number | undefined): string =>
|
|
391
|
+
clone === undefined ? executor : `${executor}#${clone}`
|
|
392
|
+
|
|
338
393
|
export const rosterEventMessage = (event: FlowEvent): string | undefined => {
|
|
339
394
|
const forLabel = (label: string | undefined): string =>
|
|
340
395
|
label === undefined ? "" : ` for ${label}`
|
|
341
396
|
switch (event._tag) {
|
|
342
397
|
case "ExecutorLeased": {
|
|
343
|
-
const taken = `roster: ${event.executor} takes ${event.role}${forLabel(event.label)}${
|
|
398
|
+
const taken = `roster: ${cloneName(event.executor, event.clone)} takes ${event.role}${forLabel(event.label)}${
|
|
344
399
|
event.purpose === undefined ? "" : ` · ${event.purpose}`
|
|
345
400
|
}`
|
|
346
401
|
if (event.because === undefined && event.borrowed !== true) {
|
|
@@ -395,7 +450,11 @@ export const FlowEvent = Schema.Union([
|
|
|
395
450
|
ExecutorHandedOver,
|
|
396
451
|
StoryJudged,
|
|
397
452
|
Timed,
|
|
398
|
-
Began
|
|
453
|
+
Began,
|
|
454
|
+
TasksPlanned,
|
|
455
|
+
TaskStarted,
|
|
456
|
+
TaskCompleted,
|
|
457
|
+
StoryStatusChanged
|
|
399
458
|
])
|
|
400
459
|
export type FlowEvent = typeof FlowEvent.Type
|
|
401
460
|
|
|
@@ -419,6 +478,8 @@ export interface Lane {
|
|
|
419
478
|
readonly lane: string
|
|
420
479
|
/** The executor working it now; read at every publish, so a handover shows. */
|
|
421
480
|
readonly executor?: Effect.Effect<string | undefined>
|
|
481
|
+
/** The coder's clone number, read at every publish as `executor` is (ADR 0033). */
|
|
482
|
+
readonly clone?: Effect.Effect<number | undefined>
|
|
422
483
|
/** The lane's working directory, dropped from tool arguments (the lane already names it). */
|
|
423
484
|
readonly workDir?: string
|
|
424
485
|
}
|
|
@@ -436,10 +497,33 @@ const stamped = (
|
|
|
436
497
|
event: FlowEvent,
|
|
437
498
|
lane: string,
|
|
438
499
|
executor: string | undefined,
|
|
439
|
-
workDir: string | undefined
|
|
500
|
+
workDir: string | undefined,
|
|
501
|
+
clone: number | undefined
|
|
440
502
|
): FlowEvent => {
|
|
441
503
|
const tags = { lane, ...(executor === undefined ? {} : { executor }) }
|
|
504
|
+
const usageTags = { ...tags, ...(clone === undefined ? {} : { clone }) }
|
|
442
505
|
switch (event._tag) {
|
|
506
|
+
case "TasksPlanned":
|
|
507
|
+
return event.lane !== undefined ? event : TasksPlanned.make({ tasks: event.tasks, ...tags })
|
|
508
|
+
case "TaskStarted":
|
|
509
|
+
return event.lane !== undefined
|
|
510
|
+
? event
|
|
511
|
+
: TaskStarted.make({
|
|
512
|
+
index: event.index,
|
|
513
|
+
count: event.count,
|
|
514
|
+
title: event.title,
|
|
515
|
+
...(event.satisfies === undefined ? {} : { satisfies: event.satisfies }),
|
|
516
|
+
...tags
|
|
517
|
+
})
|
|
518
|
+
case "TaskCompleted":
|
|
519
|
+
return event.lane !== undefined
|
|
520
|
+
? event
|
|
521
|
+
: TaskCompleted.make({
|
|
522
|
+
index: event.index,
|
|
523
|
+
count: event.count,
|
|
524
|
+
title: event.title,
|
|
525
|
+
...tags
|
|
526
|
+
})
|
|
443
527
|
case "StageStarted":
|
|
444
528
|
return event.lane !== undefined ? event : StageStarted.make({ stage: event.stage, ...tags })
|
|
445
529
|
case "StageCompleted":
|
|
@@ -456,6 +540,7 @@ const stamped = (
|
|
|
456
540
|
: ToolUse.make({
|
|
457
541
|
tool: event.tool,
|
|
458
542
|
args: shortenLaneArgs(event.args, workDir),
|
|
543
|
+
...(event.parent === undefined ? {} : { parent: event.parent }),
|
|
459
544
|
...tags
|
|
460
545
|
})
|
|
461
546
|
case "AssistantMessage":
|
|
@@ -485,7 +570,9 @@ const stamped = (
|
|
|
485
570
|
agent: event.agent,
|
|
486
571
|
usage: event.usage,
|
|
487
572
|
...(event.model === undefined ? {} : { model: event.model }),
|
|
488
|
-
|
|
573
|
+
// The lane's clone is its coder's: a reviewer or judge on the
|
|
574
|
+
// lane's events ran on another executor (ADR 0033).
|
|
575
|
+
...(event.agent === "coder" ? usageTags : tags)
|
|
489
576
|
})
|
|
490
577
|
// The lane's executor is its coder's: it names a coder call or tool, not
|
|
491
578
|
// a reviewer's or judge's call made on another executor.
|
|
@@ -523,7 +610,9 @@ const stamped = (
|
|
|
523
610
|
export const withLane = (events: FlowEventsShape, lane: Lane): FlowEventsShape => ({
|
|
524
611
|
publish: (event) =>
|
|
525
612
|
Effect.flatMap(lane.executor ?? Effect.succeed(undefined), (executor) =>
|
|
526
|
-
|
|
613
|
+
Effect.flatMap(lane.clone ?? Effect.succeed(undefined), (clone) =>
|
|
614
|
+
events.publish(stamped(event, lane.lane, executor, lane.workDir, clone))
|
|
615
|
+
)
|
|
527
616
|
)
|
|
528
617
|
})
|
|
529
618
|
|
package/src/PlanExecution.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
StageCompleted,
|
|
4
|
+
StageFailed,
|
|
5
|
+
StageStarted,
|
|
6
|
+
TaskCompleted,
|
|
7
|
+
TaskStarted,
|
|
8
|
+
TasksPlanned,
|
|
9
|
+
type FlowEventsShape
|
|
10
|
+
} from "./FlowEvents.ts"
|
|
3
11
|
import { withKindSpan, type SpanOptions } from "./Spans.ts"
|
|
4
12
|
import { describeFlowError, type FlowError } from "./FlowError.ts"
|
|
5
13
|
import type { Plan, Task } from "./Plan.ts"
|
|
@@ -35,6 +43,26 @@ export const stage = <A, E, R>(
|
|
|
35
43
|
)
|
|
36
44
|
)
|
|
37
45
|
|
|
46
|
+
/** The acceptance criteria a task's description names: `Satisfies: 1, 3` → `[1, 3]` (ADR 0033). */
|
|
47
|
+
export const satisfiesOf = (description: string): ReadonlyArray<number> | undefined => {
|
|
48
|
+
// The planner writes JSON, so the line is usually the description's last sentence.
|
|
49
|
+
const match = /\bSatisfies:\s*([^\n]*)/iu.exec(description)
|
|
50
|
+
if (match === null) {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
const numbers = (match[1] ?? "")
|
|
54
|
+
.split(/[^\d]+/u)
|
|
55
|
+
.filter((part) => part.length > 0)
|
|
56
|
+
.map((part) => Number.parseInt(part, 10))
|
|
57
|
+
.filter((number) => Number.isInteger(number) && number > 0)
|
|
58
|
+
return numbers.length === 0 ? undefined : numbers
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const withSatisfies = (description: string): { readonly satisfies?: ReadonlyArray<number> } => {
|
|
62
|
+
const satisfies = satisfiesOf(description)
|
|
63
|
+
return satisfies === undefined ? {} : { satisfies }
|
|
64
|
+
}
|
|
65
|
+
|
|
38
66
|
export const implementTaskLoop = Effect.fn("@llm4ts/flow/PlanExecution.implementTaskLoop")(
|
|
39
67
|
function* <E, R>(
|
|
40
68
|
store: PlanStoreShape,
|
|
@@ -44,11 +72,26 @@ export const implementTaskLoop = Effect.fn("@llm4ts/flow/PlanExecution.implement
|
|
|
44
72
|
perTask: (task: Task, planSoFar: Plan) => Effect.Effect<void, E, R>
|
|
45
73
|
): Effect.fn.Return<Plan, E | FlowError, R> {
|
|
46
74
|
let current = plan
|
|
47
|
-
|
|
75
|
+
const count = plan.tasks.length
|
|
76
|
+
yield* events.publish(
|
|
77
|
+
TasksPlanned.make({
|
|
78
|
+
tasks: plan.tasks.map((task) => ({
|
|
79
|
+
title: task.title,
|
|
80
|
+
completed: task.completed,
|
|
81
|
+
...withSatisfies(task.description)
|
|
82
|
+
}))
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
for (const [position, task] of plan.tasks.entries()) {
|
|
48
86
|
if (!task.completed) {
|
|
87
|
+
const index = position + 1
|
|
88
|
+
yield* events.publish(
|
|
89
|
+
TaskStarted.make({ index, count, title: task.title, ...withSatisfies(task.description) })
|
|
90
|
+
)
|
|
49
91
|
yield* stage(events, task.title, perTask(task, current))
|
|
50
92
|
current = current.complete(task.title)
|
|
51
93
|
yield* store.save(planPath, current)
|
|
94
|
+
yield* events.publish(TaskCompleted.make({ index, count, title: task.title }))
|
|
52
95
|
}
|
|
53
96
|
}
|
|
54
97
|
return current
|
package/src/Roster.ts
CHANGED
|
@@ -55,7 +55,7 @@ export class ExecutorSpec extends Schema.Class<ExecutorSpec>("ExecutorSpec")({
|
|
|
55
55
|
baseUrl: Schema.optionalKey(Schema.String),
|
|
56
56
|
roles: Schema.Array(Schema.String),
|
|
57
57
|
slots: Schema.optionalKey(Schema.Number),
|
|
58
|
-
/** Slots coders may use; default
|
|
58
|
+
/** Slots coders may use; default: every slot (ADR 0033). */
|
|
59
59
|
coderSlots: Schema.optionalKey(Schema.Number),
|
|
60
60
|
/** Lower first. A number for every role, or per role with a `default`. */
|
|
61
61
|
priority: Schema.optionalKey(
|
|
@@ -105,8 +105,6 @@ export class RosterState extends Schema.Class<RosterState>("RosterState")({
|
|
|
105
105
|
|
|
106
106
|
// ---- Pure helpers -------------------------------------------------------------
|
|
107
107
|
|
|
108
|
-
const reasoningRoles: ReadonlyArray<Role> = ["planner", "reviewer", "judge", "verifier"]
|
|
109
|
-
|
|
110
108
|
export const isRole = (value: string): value is Role => roles.some((role) => role === value)
|
|
111
109
|
|
|
112
110
|
export const rolesOf = (spec: ExecutorSpec): ReadonlyArray<Role> => spec.roles.filter(isRole)
|
|
@@ -115,14 +113,12 @@ export const hasRole = (spec: ExecutorSpec, role: Role): boolean => rolesOf(spec
|
|
|
115
113
|
|
|
116
114
|
export const slotsOf = (spec: ExecutorSpec): number => Math.max(1, Math.floor(spec.slots ?? 1))
|
|
117
115
|
|
|
118
|
-
/** Slots coders may hold:
|
|
116
|
+
/** Slots coders may hold: every clone, unless the roster sets `coderSlots` lower (ADR 0033). */
|
|
119
117
|
export const coderSlotsOf = (spec: ExecutorSpec): number => {
|
|
120
118
|
const slots = slotsOf(spec)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const reasons = rolesOf(spec).some((role) => reasoningRoles.includes(role))
|
|
125
|
-
return hasRole(spec, "coder") && reasons && slots > 1 ? slots - 1 : slots
|
|
119
|
+
return spec.coderSlots === undefined
|
|
120
|
+
? slots
|
|
121
|
+
: Math.max(0, Math.min(slots, Math.floor(spec.coderSlots)))
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
/** The effort a role runs at on this executor (ADR 0029); absent is the harness's default. */
|
|
@@ -419,6 +415,8 @@ export interface Lease {
|
|
|
419
415
|
readonly role: Role
|
|
420
416
|
/** Frees the slot; idempotent, and also run when the lease's scope closes. */
|
|
421
417
|
readonly release: Effect.Effect<void>
|
|
418
|
+
/** Which of the executor's clones this is, from 1; absent on a borrowed lease (ADR 0033). */
|
|
419
|
+
readonly clone: number | undefined
|
|
422
420
|
/** Took no slot: the context's own coder executor serves the call (see `LeaseOptions.borrow`). */
|
|
423
421
|
readonly borrowed: boolean
|
|
424
422
|
/** Set when the executor is the context's own coder (not independent), and why; see `ExecutorLeased.because`. */
|
|
@@ -522,6 +520,12 @@ interface Usage {
|
|
|
522
520
|
busyCoders: number
|
|
523
521
|
}
|
|
524
522
|
|
|
523
|
+
/** A slot taken under the lock: the executor and which of its clones. */
|
|
524
|
+
interface Taken {
|
|
525
|
+
readonly spec: ExecutorSpec
|
|
526
|
+
readonly clone: number
|
|
527
|
+
}
|
|
528
|
+
|
|
525
529
|
export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
526
530
|
options: RosterOptions
|
|
527
531
|
): Effect.fn.Return<RosterShape, FlowError> {
|
|
@@ -534,6 +538,21 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
534
538
|
const rateLimits = new Map<string, ReadonlyArray<number>>()
|
|
535
539
|
const lastLeased = new Map<string, number>()
|
|
536
540
|
let leases = 0
|
|
541
|
+
/** Clone numbers in use per executor; a lease takes the lowest free one (ADR 0033). */
|
|
542
|
+
const clones = new Map<string, Set<number>>(executors.map((spec) => [spec.id, new Set<number>()]))
|
|
543
|
+
const takeClone = (spec: ExecutorSpec): number => {
|
|
544
|
+
const used = clones.get(spec.id) ?? new Set<number>()
|
|
545
|
+
let clone = 1
|
|
546
|
+
while (used.has(clone)) {
|
|
547
|
+
clone += 1
|
|
548
|
+
}
|
|
549
|
+
used.add(clone)
|
|
550
|
+
clones.set(spec.id, used)
|
|
551
|
+
return clone
|
|
552
|
+
}
|
|
553
|
+
const freeClone = (spec: ExecutorSpec, clone: number): void => {
|
|
554
|
+
clones.get(spec.id)?.delete(clone)
|
|
555
|
+
}
|
|
537
556
|
const changed = yield* Ref.make(yield* Deferred.make<void>())
|
|
538
557
|
const known = new Set(executors.map((spec) => spec.id))
|
|
539
558
|
|
|
@@ -611,7 +630,7 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
611
630
|
: best
|
|
612
631
|
}
|
|
613
632
|
|
|
614
|
-
const release = (spec: ExecutorSpec, role: Role): Effect.Effect<void> =>
|
|
633
|
+
const release = (spec: ExecutorSpec, role: Role, clone: number): Effect.Effect<void> =>
|
|
615
634
|
lock
|
|
616
635
|
.withPermit(
|
|
617
636
|
Effect.sync(() => {
|
|
@@ -622,33 +641,42 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
622
641
|
used.busyCoders = Math.max(0, used.busyCoders - 1)
|
|
623
642
|
}
|
|
624
643
|
}
|
|
644
|
+
freeClone(spec, clone)
|
|
625
645
|
})
|
|
626
646
|
)
|
|
627
647
|
.pipe(Effect.andThen(signal))
|
|
628
648
|
|
|
629
649
|
const leaseOf = (
|
|
630
|
-
|
|
650
|
+
taken: Taken,
|
|
631
651
|
role: Role,
|
|
632
652
|
who: LeaseOptions,
|
|
633
653
|
because?: "nobody" | "held" | "out"
|
|
634
654
|
): Effect.Effect<Lease, never, Scope.Scope> =>
|
|
635
655
|
Effect.gen(function* () {
|
|
656
|
+
const { spec, clone } = taken
|
|
636
657
|
const labelled = who.label === undefined ? {} : { label: who.label }
|
|
637
658
|
const own = because === undefined ? {} : { because }
|
|
638
659
|
yield* events.publish(
|
|
639
|
-
ExecutorLeased.make({
|
|
660
|
+
ExecutorLeased.make({
|
|
661
|
+
executor: spec.id,
|
|
662
|
+
role,
|
|
663
|
+
clone,
|
|
664
|
+
...labelled,
|
|
665
|
+
...purposeOf(who),
|
|
666
|
+
...own
|
|
667
|
+
})
|
|
640
668
|
)
|
|
641
669
|
const done = yield* Ref.make(false)
|
|
642
670
|
const free = Effect.flatMap(Ref.getAndSet(done, true), (was) =>
|
|
643
671
|
was
|
|
644
672
|
? Effect.void
|
|
645
673
|
: Effect.andThen(
|
|
646
|
-
release(spec, role),
|
|
647
|
-
events.publish(ExecutorReleased.make({ executor: spec.id, role, ...labelled }))
|
|
674
|
+
release(spec, role, clone),
|
|
675
|
+
events.publish(ExecutorReleased.make({ executor: spec.id, role, clone, ...labelled }))
|
|
648
676
|
)
|
|
649
677
|
)
|
|
650
678
|
yield* Effect.addFinalizer(() => free)
|
|
651
|
-
return { executor: spec, role, release: free, borrowed: false, ...own }
|
|
679
|
+
return { executor: spec, role, clone, release: free, borrowed: false, ...own }
|
|
652
680
|
})
|
|
653
681
|
|
|
654
682
|
/** A lease over the context's own coder executor: no slot taken, none freed. */
|
|
@@ -677,32 +705,30 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
677
705
|
: events.publish(ExecutorReleased.make({ executor: spec.id, role, ...labelled }))
|
|
678
706
|
)
|
|
679
707
|
yield* Effect.addFinalizer(() => free)
|
|
680
|
-
return { executor: spec, role, release: free, borrowed: true, because }
|
|
708
|
+
return { executor: spec, role, clone: undefined, release: free, borrowed: true, because }
|
|
681
709
|
})
|
|
682
710
|
|
|
683
|
-
/** One attempt: expire, pick, take the slot — all under the lock. */
|
|
684
|
-
const attempt = (
|
|
685
|
-
role: Role,
|
|
686
|
-
leaseOptions: LeaseOptions
|
|
687
|
-
): Effect.Effect<ExecutorSpec | undefined> =>
|
|
711
|
+
/** One attempt: expire, pick, take the slot and its clone number — all under the lock. */
|
|
712
|
+
const attempt = (role: Role, leaseOptions: LeaseOptions): Effect.Effect<Taken | undefined> =>
|
|
688
713
|
Effect.gen(function* () {
|
|
689
714
|
const now = yield* Clock.currentTimeMillis
|
|
690
|
-
const {
|
|
715
|
+
const { taken, back } = yield* lock.withPermit(
|
|
691
716
|
Effect.sync(() => {
|
|
692
717
|
const back = expire(now)
|
|
693
718
|
const spec = pick(role, leaseOptions)
|
|
694
|
-
if (spec
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
719
|
+
if (spec === undefined) {
|
|
720
|
+
return { taken: undefined, back }
|
|
721
|
+
}
|
|
722
|
+
const used = usage.get(spec.id)
|
|
723
|
+
if (used !== undefined) {
|
|
724
|
+
used.busy += 1
|
|
725
|
+
if (role === "coder") {
|
|
726
|
+
used.busyCoders += 1
|
|
701
727
|
}
|
|
702
|
-
leases += 1
|
|
703
|
-
lastLeased.set(spec.id, leases)
|
|
704
728
|
}
|
|
705
|
-
|
|
729
|
+
leases += 1
|
|
730
|
+
lastLeased.set(spec.id, leases)
|
|
731
|
+
return { taken: { spec, clone: takeClone(spec) }, back }
|
|
706
732
|
})
|
|
707
733
|
)
|
|
708
734
|
if (back.length > 0) {
|
|
@@ -711,7 +737,7 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
711
737
|
)
|
|
712
738
|
yield* persist
|
|
713
739
|
}
|
|
714
|
-
return
|
|
740
|
+
return taken
|
|
715
741
|
})
|
|
716
742
|
|
|
717
743
|
const canEverServe = (role: Role, avoid: ReadonlyArray<string> = []): Effect.Effect<boolean> =>
|
|
@@ -880,7 +906,7 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
880
906
|
prefer: borrow.id
|
|
881
907
|
})
|
|
882
908
|
yield* waited
|
|
883
|
-
return own !== undefined && own.id === borrow.id
|
|
909
|
+
return own !== undefined && own.spec.id === borrow.id
|
|
884
910
|
? yield* leaseOf(own, role, leaseOptions, because)
|
|
885
911
|
: own !== undefined
|
|
886
912
|
? // Someone came back between the check and the pick: independent after all.
|
package/src/RosterSeats.ts
CHANGED
|
@@ -258,6 +258,8 @@ export interface HeldCoderOptions {
|
|
|
258
258
|
export interface HeldCoder {
|
|
259
259
|
readonly service: LlmServiceShape
|
|
260
260
|
readonly executor: Effect.Effect<string | undefined>
|
|
261
|
+
/** The held coder's clone number, when it holds a slot (ADR 0033). */
|
|
262
|
+
readonly clone: Effect.Effect<number | undefined>
|
|
261
263
|
readonly lease: Effect.Effect<ExecutorSpec | undefined>
|
|
262
264
|
readonly history: Effect.Effect<ReadonlyArray<string>>
|
|
263
265
|
}
|
|
@@ -375,6 +377,7 @@ export const makeHeldCoder = Effect.fn("@llm4ts/flow/RosterSeats.heldCoder")(fun
|
|
|
375
377
|
return {
|
|
376
378
|
service: serviceOver({ run, stream, isAvailable: roster.canEverServe("coder") }),
|
|
377
379
|
executor: Effect.map(Ref.get(current), (held) => held?.lease.executor.id),
|
|
380
|
+
clone: Effect.map(Ref.get(current), (held) => held?.lease.clone),
|
|
378
381
|
lease: Effect.map(Ref.get(current), (held) => held?.lease.executor),
|
|
379
382
|
history: Ref.get(history)
|
|
380
383
|
}
|
package/src/Stories.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
|
18
18
|
import { cap } from "./Context.ts"
|
|
19
19
|
import type { LlmChunk, TokenUsage } from "@llm4ts/core/Models"
|
|
20
20
|
import type { LlmError } from "@llm4ts/core/Errors"
|
|
21
|
-
import { BoardItem, type BoardSyncShape } from "./BoardSync.ts"
|
|
21
|
+
import { BoardItem, eventedBoard, type BoardSyncShape } from "./BoardSync.ts"
|
|
22
22
|
import { makeChat } from "./Chat.ts"
|
|
23
23
|
import { implementPlanFlow } from "./Flow.ts"
|
|
24
24
|
import {
|
|
@@ -873,8 +873,10 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
873
873
|
options: StoriesOptions
|
|
874
874
|
): Effect.fn.Return<EpicReport, FlowError> {
|
|
875
875
|
const plan = yield* validateStoryPlan(options.plan)
|
|
876
|
-
const { files
|
|
876
|
+
const { files } = options
|
|
877
877
|
const events = context.events
|
|
878
|
+
// Every board move is also an event, so the trace and the dashboard see it (ADR 0033).
|
|
879
|
+
const board = eventedBoard(options.board, events)
|
|
878
880
|
const epicBranch = options.epicBranch ?? `epic/${plan.epicId}`
|
|
879
881
|
const concurrency = Math.max(1, options.concurrency ?? 3)
|
|
880
882
|
const judgeRounds = Math.max(1, options.judgeRounds ?? 2)
|
package/src/Transcript.ts
CHANGED
|
@@ -22,6 +22,8 @@ const Call = Schema.TaggedStruct("Call", {
|
|
|
22
22
|
call: Schema.String,
|
|
23
23
|
role: Schema.String,
|
|
24
24
|
executor: Schema.optionalKey(Schema.String),
|
|
25
|
+
/** The executor's clone, when the seat's coder holds a slot (ADR 0033). */
|
|
26
|
+
clone: Schema.optionalKey(Schema.Int),
|
|
25
27
|
/** The system prompt, on a chat's first call only. */
|
|
26
28
|
system: Schema.optionalKey(Schema.String),
|
|
27
29
|
/** The input: a chat's newest message, or a one-shot prompt. */
|
|
@@ -96,6 +98,8 @@ export interface TranscriptSeatOptions {
|
|
|
96
98
|
readonly role: string
|
|
97
99
|
/** Who serves the seat, read at each call (a roster hands the coder over). */
|
|
98
100
|
readonly executor?: Effect.Effect<string | undefined>
|
|
101
|
+
/** Which clone of that executor, read at each call (ADR 0033). */
|
|
102
|
+
readonly clone?: Effect.Effect<number | undefined>
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
/** A seat whose every call is written to `sink`, as it happens. */
|
|
@@ -115,12 +119,14 @@ export const transcriptSeat = (
|
|
|
115
119
|
const start = yield* Clock.currentTimeMillis
|
|
116
120
|
const role = yield* roleOr(options.role)
|
|
117
121
|
const executor = options.executor === undefined ? undefined : yield* options.executor
|
|
122
|
+
const clone = options.clone === undefined ? undefined : yield* options.clone
|
|
118
123
|
yield* write({
|
|
119
124
|
_tag: "Call",
|
|
120
125
|
at: start,
|
|
121
126
|
call,
|
|
122
127
|
role,
|
|
123
128
|
...(executor === undefined ? {} : { executor }),
|
|
129
|
+
...(clone === undefined ? {} : { clone }),
|
|
124
130
|
...(extra.system === undefined ? {} : { system: clean(extra.system, inputChars) }),
|
|
125
131
|
input: clean(input, inputChars),
|
|
126
132
|
...(extra.earlier === undefined ? {} : { earlier: extra.earlier })
|