@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/Stories.ts
CHANGED
|
@@ -18,9 +18,9 @@ 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
|
-
import { implementPlanFlow } from "./Flow.ts"
|
|
23
|
+
import { implementPlanFlow, type TaskOverrides } from "./Flow.ts"
|
|
24
24
|
import {
|
|
25
25
|
GateBaseline,
|
|
26
26
|
baselineKey,
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
enforcePerimeter,
|
|
59
59
|
isWithinPerimeter,
|
|
60
60
|
perimeterGate,
|
|
61
|
+
taskOwnsGate,
|
|
61
62
|
strayTasks,
|
|
62
63
|
type PerimeterCheck
|
|
63
64
|
} from "./Perimeter.ts"
|
|
@@ -68,7 +69,7 @@ import {
|
|
|
68
69
|
type PlainFileStoreShape
|
|
69
70
|
} from "./Persistence.ts"
|
|
70
71
|
import { Plan, Task } from "./Plan.ts"
|
|
71
|
-
import { stage } from "./PlanExecution.ts"
|
|
72
|
+
import { stage, type AsideOutcome, type AsideRequest } from "./PlanExecution.ts"
|
|
72
73
|
import { planFrom } from "./Planner.ts"
|
|
73
74
|
import {
|
|
74
75
|
ReviewIssue,
|
|
@@ -459,6 +460,10 @@ export const storyTaskPlanInstructions = (story: Story): string =>
|
|
|
459
460
|
"End every task's description with `Satisfies: <n>` naming the criteria it serves, and",
|
|
460
461
|
"make sure every criterion is served by at least one task."
|
|
461
462
|
]),
|
|
463
|
+
"End every task's description with a `Depends on: <n, …>` line naming the earlier tasks it",
|
|
464
|
+
"needs done first (`Depends on: none` when it needs none; without the line it waits for the",
|
|
465
|
+
"task before it), and an `Owns: <paths>` line naming the files it changes. Tasks that need",
|
|
466
|
+
"nothing from each other and own different files may run at the same time.",
|
|
462
467
|
"Never plan a task that creates or changes anything outside them — registering or wiring",
|
|
463
468
|
"the story in elsewhere (the app's composition point, a shared kit file) is another story's job.",
|
|
464
469
|
`Use exactly this epicId: "${story.id}".`,
|
|
@@ -727,6 +732,13 @@ export interface StoriesOptions {
|
|
|
727
732
|
) => Effect.Effect<Plan, FlowError>
|
|
728
733
|
/** Stories implemented at once. Default 3. */
|
|
729
734
|
readonly concurrency?: number
|
|
735
|
+
/**
|
|
736
|
+
* Coders one story may hold at once, its own included (ADR 0034): a
|
|
737
|
+
* ready task whose plan says it may runs beside the held coder, in its
|
|
738
|
+
* own worktree, on a coder that is free now. Default 2; 1 runs every task
|
|
739
|
+
* in order on the story's coder.
|
|
740
|
+
*/
|
|
741
|
+
readonly taskConcurrency?: number
|
|
730
742
|
/** Stop the epic at the first failed story instead of skipping its dependents. */
|
|
731
743
|
readonly failFast?: boolean
|
|
732
744
|
readonly reviewers?: ReadonlyArray<Reviewer>
|
|
@@ -873,12 +885,15 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
873
885
|
options: StoriesOptions
|
|
874
886
|
): Effect.fn.Return<EpicReport, FlowError> {
|
|
875
887
|
const plan = yield* validateStoryPlan(options.plan)
|
|
876
|
-
const { files
|
|
888
|
+
const { files } = options
|
|
877
889
|
const events = context.events
|
|
890
|
+
// Every board move is also an event, so the trace and the dashboard see it (ADR 0033).
|
|
891
|
+
const board = eventedBoard(options.board, events)
|
|
878
892
|
const epicBranch = options.epicBranch ?? `epic/${plan.epicId}`
|
|
879
893
|
const concurrency = Math.max(1, options.concurrency ?? 3)
|
|
880
894
|
const judgeRounds = Math.max(1, options.judgeRounds ?? 2)
|
|
881
895
|
const mergeRevisions = Math.max(0, options.mergeRevisions ?? 1)
|
|
896
|
+
const taskConcurrency = Math.max(1, options.taskConcurrency ?? 2)
|
|
882
897
|
const deferring = options.deferNonBlocking === true
|
|
883
898
|
const statePath = (story: Story): string => join(options.stateDir, `stories/${story.id}.json`)
|
|
884
899
|
const planPath = (story: Story): string => join(options.stateDir, `stories/${story.id}.plan.md`)
|
|
@@ -1360,12 +1375,15 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1360
1375
|
)
|
|
1361
1376
|
// The oracle guard over the story's whole change against the epic:
|
|
1362
1377
|
// committed on the branch and uncommitted in the worktree (ADR 0027).
|
|
1363
|
-
const oracleGate = (
|
|
1378
|
+
const oracleGate = (
|
|
1379
|
+
target: ReviewResult,
|
|
1380
|
+
where: GitToolShape = git
|
|
1381
|
+
): Effect.Effect<ReviewResult, FlowError> =>
|
|
1364
1382
|
story.testsChange
|
|
1365
1383
|
? Effect.succeed(ReviewResult.make({ issues: [] }))
|
|
1366
1384
|
: Effect.gen(function* () {
|
|
1367
|
-
const committed = yield*
|
|
1368
|
-
const uncommitted = yield*
|
|
1385
|
+
const committed = yield* where.diffVsBase(epicBranch)
|
|
1386
|
+
const uncommitted = yield* where.diffAll
|
|
1369
1387
|
const base =
|
|
1370
1388
|
gateCommands === undefined ? undefined : (yield* storyBaseline)?.passedCount
|
|
1371
1389
|
const issues = checkOracle(
|
|
@@ -1376,17 +1394,41 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1376
1394
|
)
|
|
1377
1395
|
return ReviewResult.make({ issues, summary: issues.length === 0 ? "" : "oracle guard" })
|
|
1378
1396
|
})
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1397
|
+
/**
|
|
1398
|
+
* The gates in a worktree of the story: the target's, the story's
|
|
1399
|
+
* perimeter, the oracle guard and — for a task run aside — the paths the
|
|
1400
|
+
* task said it owns, against the story branch it started from.
|
|
1401
|
+
*/
|
|
1402
|
+
const gatesAt = (
|
|
1403
|
+
workDir: string,
|
|
1404
|
+
where: GitToolShape,
|
|
1405
|
+
logDir: string,
|
|
1406
|
+
task?: { readonly owns: ReadonlyArray<string>; readonly from: string }
|
|
1407
|
+
): Effect.Effect<ReviewResult, FlowError> =>
|
|
1408
|
+
Effect.gen(function* () {
|
|
1409
|
+
const target = yield* options.gates(workDir, laneOf(story), { files, dir: logDir })
|
|
1410
|
+
const changed = yield* perimeterNow(story, where)
|
|
1411
|
+
const owned =
|
|
1412
|
+
task === undefined || task.owns.length === 0
|
|
1413
|
+
? ReviewResult.make({ issues: [] })
|
|
1414
|
+
: taskOwnsGate(
|
|
1415
|
+
[
|
|
1416
|
+
...new Set([
|
|
1417
|
+
...(yield* where.changedFilesVsBase(task.from)),
|
|
1418
|
+
...(yield* where.uncommittedFiles)
|
|
1419
|
+
])
|
|
1420
|
+
],
|
|
1421
|
+
task.owns
|
|
1422
|
+
)
|
|
1423
|
+
return combined(
|
|
1424
|
+
combined(
|
|
1425
|
+
combined(target, perimeterGate([...changed.sharedReadOnly, ...changed.outside], story)),
|
|
1426
|
+
yield* oracleGate(target, where)
|
|
1427
|
+
),
|
|
1428
|
+
owned
|
|
1429
|
+
)
|
|
1383
1430
|
})
|
|
1384
|
-
|
|
1385
|
-
return combined(
|
|
1386
|
-
combined(target, perimeterGate([...changed.sharedReadOnly, ...changed.outside], story)),
|
|
1387
|
-
yield* oracleGate(target)
|
|
1388
|
-
)
|
|
1389
|
-
})
|
|
1431
|
+
const gates = gatesAt(state.worktree, git, storyGateLogDir(options.stateDir, story.id))
|
|
1390
1432
|
const triage: GateTriageOptions = {
|
|
1391
1433
|
baseline: storyBaseline,
|
|
1392
1434
|
roots: rootsOf(state.worktree),
|
|
@@ -1569,6 +1611,147 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1569
1611
|
const taskSystem = [system, startingCode]
|
|
1570
1612
|
.filter((part): part is string => part !== undefined)
|
|
1571
1613
|
.join("\n\n")
|
|
1614
|
+
/** Tasks merged in from beside the held coder this run: the story's gates rerun after them. */
|
|
1615
|
+
let mergedAside = 0
|
|
1616
|
+
/**
|
|
1617
|
+
* A ready task beside the held coder (ADR 0034): a coder free now, a
|
|
1618
|
+
* worktree on its own branch from the story branch, the task's whole
|
|
1619
|
+
* body there — coder, review, gates with its `Owns:` — then a merge
|
|
1620
|
+
* into the story branch while no task runs at home. Anything short of
|
|
1621
|
+
* a clean merge hands the task back to the held coder; a conflict
|
|
1622
|
+
* hands it back to run after the others.
|
|
1623
|
+
*/
|
|
1624
|
+
const runAside = (
|
|
1625
|
+
request: AsideRequest,
|
|
1626
|
+
run: (at: FlowContextShape, overrides: TaskOverrides) => Effect.Effect<void, FlowError>
|
|
1627
|
+
): Effect.Effect<AsideOutcome, FlowError> => {
|
|
1628
|
+
const { node } = request
|
|
1629
|
+
const where = join(options.worktreeRoot, `${story.id}.task-${node.index}`)
|
|
1630
|
+
const branch = `${state.branch}--task-${node.index}`
|
|
1631
|
+
const lane = laneOf(story)
|
|
1632
|
+
const said = (message: string) =>
|
|
1633
|
+
lane.publish(Info.make({ message: `story ${story.id}: task ${node.index} ${message}` }))
|
|
1634
|
+
const cleanUp = Effect.gen(function* () {
|
|
1635
|
+
yield* context.git.removeWorktree(where, true).pipe(Effect.ignore)
|
|
1636
|
+
if (yield* context.git.branchExists(branch).pipe(Effect.orElseSucceed(() => false))) {
|
|
1637
|
+
yield* context.git.deleteBranch(branch).pipe(Effect.ignore)
|
|
1638
|
+
}
|
|
1639
|
+
})
|
|
1640
|
+
const attempt = Effect.gen(function* () {
|
|
1641
|
+
// No disk work while no coder is free: the held coder takes the task.
|
|
1642
|
+
if (roster !== undefined && (yield* roster.available("coder")) === 0) {
|
|
1643
|
+
return "declined" as const
|
|
1644
|
+
}
|
|
1645
|
+
// A previous run's leftovers go first.
|
|
1646
|
+
yield* cleanUp
|
|
1647
|
+
yield* context.git.addWorktreeNewBranch(where, branch, state.branch)
|
|
1648
|
+
// The coder is held for the work only: its slot is free again before
|
|
1649
|
+
// the merge waits for the story's coder to finish its own task.
|
|
1650
|
+
const blockedOn = yield* Effect.scoped(
|
|
1651
|
+
Effect.gen(function* () {
|
|
1652
|
+
const asideSeats = yield* options.contextFor(where, {
|
|
1653
|
+
label: `${story.id}#t${node.index}`,
|
|
1654
|
+
ifFree: true
|
|
1655
|
+
})
|
|
1656
|
+
const asideRoster = asideSeats.context.roster
|
|
1657
|
+
yield* request.announce({
|
|
1658
|
+
...(asideRoster === undefined
|
|
1659
|
+
? {}
|
|
1660
|
+
: {
|
|
1661
|
+
...(yield* Effect.map(asideRoster.executor, (executor) =>
|
|
1662
|
+
executor === undefined ? {} : { executor }
|
|
1663
|
+
)),
|
|
1664
|
+
...(asideRoster.clone === undefined
|
|
1665
|
+
? {}
|
|
1666
|
+
: yield* Effect.map(asideRoster.clone, (clone) =>
|
|
1667
|
+
clone === undefined ? {} : { clone }
|
|
1668
|
+
))
|
|
1669
|
+
})
|
|
1670
|
+
})
|
|
1671
|
+
if (options.setup !== undefined) {
|
|
1672
|
+
yield* options.setup(where, lane)
|
|
1673
|
+
}
|
|
1674
|
+
const asideBlocked = yield* Ref.make<string | undefined>(undefined)
|
|
1675
|
+
const asideContext: FlowContextShape = {
|
|
1676
|
+
...asideSeats.context,
|
|
1677
|
+
coder: watchForBlockedOn(asideSeats.context.coder, asideBlocked)
|
|
1678
|
+
}
|
|
1679
|
+
const asideGit = asideContext.git
|
|
1680
|
+
const asideSystem = [
|
|
1681
|
+
withContract(
|
|
1682
|
+
[
|
|
1683
|
+
perimeterRules(story, { plan, worktree: where, epicCheckout: context.workDir }),
|
|
1684
|
+
gateRules,
|
|
1685
|
+
extra,
|
|
1686
|
+
node.owns.length === 0
|
|
1687
|
+
? undefined
|
|
1688
|
+
: `This task owns ${node.owns.join(", ")}: another task of this story may be changing other files right now, so change nothing else.`
|
|
1689
|
+
]
|
|
1690
|
+
.filter((part): part is string => part !== undefined && part.trim().length > 0)
|
|
1691
|
+
.join("\n\n"),
|
|
1692
|
+
contractProfile
|
|
1693
|
+
),
|
|
1694
|
+
startingCode
|
|
1695
|
+
]
|
|
1696
|
+
.filter((part): part is string => part !== undefined)
|
|
1697
|
+
.join("\n\n")
|
|
1698
|
+
yield* run(asideContext, {
|
|
1699
|
+
lint: gatesAt(
|
|
1700
|
+
where,
|
|
1701
|
+
asideGit,
|
|
1702
|
+
join(storyGateLogDir(options.stateDir, story.id), `task-${node.index}`),
|
|
1703
|
+
{ owns: node.owns, from: state.branch }
|
|
1704
|
+
),
|
|
1705
|
+
triage: {
|
|
1706
|
+
baseline: storyBaseline,
|
|
1707
|
+
roots: rootsOf(where),
|
|
1708
|
+
...(options.testGate === undefined
|
|
1709
|
+
? {}
|
|
1710
|
+
: { rerunTest: options.testGate(where, lane) })
|
|
1711
|
+
},
|
|
1712
|
+
// Its tool calls are on its own lane's transcript, not the story's.
|
|
1713
|
+
onTaskReply: undefined,
|
|
1714
|
+
system: asideSystem
|
|
1715
|
+
})
|
|
1716
|
+
return yield* Ref.get(asideBlocked)
|
|
1717
|
+
})
|
|
1718
|
+
)
|
|
1719
|
+
if (blockedOn !== undefined) {
|
|
1720
|
+
yield* said("stopped on BLOCKED_ON aside; the story's coder takes it")
|
|
1721
|
+
return "declined" as const
|
|
1722
|
+
}
|
|
1723
|
+
return yield* request.exclusive(
|
|
1724
|
+
Effect.gen(function* () {
|
|
1725
|
+
yield* git.merge(branch, `${story.id}: merge task ${node.index} (${node.task.title})`)
|
|
1726
|
+
mergedAside += 1
|
|
1727
|
+
yield* said(`merged from beside the story's coder`)
|
|
1728
|
+
return "merged" as const
|
|
1729
|
+
})
|
|
1730
|
+
)
|
|
1731
|
+
})
|
|
1732
|
+
return Effect.scoped(
|
|
1733
|
+
Effect.gen(function* () {
|
|
1734
|
+
yield* Effect.addFinalizer(() => cleanUp)
|
|
1735
|
+
return yield* attempt
|
|
1736
|
+
})
|
|
1737
|
+
).pipe(
|
|
1738
|
+
Effect.catch((error) =>
|
|
1739
|
+
error._tag === "MergeConflict"
|
|
1740
|
+
? Effect.as(
|
|
1741
|
+
said("conflicted merging back; the story's coder runs it last"),
|
|
1742
|
+
"conflict" as const
|
|
1743
|
+
)
|
|
1744
|
+
: error._tag === "RosterExhausted"
|
|
1745
|
+
? Effect.succeed("declined" as const)
|
|
1746
|
+
: Effect.as(
|
|
1747
|
+
said(
|
|
1748
|
+
`could not finish aside (${error.message.split("\n")[0] ?? ""}); the story's coder takes it`
|
|
1749
|
+
),
|
|
1750
|
+
"declined" as const
|
|
1751
|
+
)
|
|
1752
|
+
)
|
|
1753
|
+
)
|
|
1754
|
+
}
|
|
1572
1755
|
const implementTasks = implementPlanFlow(storyContext, {
|
|
1573
1756
|
store: makePlanStore(files),
|
|
1574
1757
|
planPath: planPath(story),
|
|
@@ -1586,6 +1769,9 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1586
1769
|
),
|
|
1587
1770
|
system: taskSystem,
|
|
1588
1771
|
chatPerTask: true,
|
|
1772
|
+
...(taskConcurrency > 1
|
|
1773
|
+
? { parallel: { concurrency: taskConcurrency, aside: runAside } }
|
|
1774
|
+
: {}),
|
|
1589
1775
|
carry: {
|
|
1590
1776
|
read: files.read(notesPath(story)),
|
|
1591
1777
|
write: (notes) => files.writeAtomic(notesPath(story), notes)
|
|
@@ -1644,6 +1830,33 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1644
1830
|
)
|
|
1645
1831
|
}
|
|
1646
1832
|
yield* guarded(implementTasks, implementTasks)
|
|
1833
|
+
// Tasks merged in from beside the held coder were each green alone: the
|
|
1834
|
+
// story's gates now run on them together, and red goes back to the
|
|
1835
|
+
// story's coder as a revision, as a red epic merge does (ADR 0034).
|
|
1836
|
+
for (let revision = 0; mergedAside > 0; revision += 1) {
|
|
1837
|
+
mergedAside = 0
|
|
1838
|
+
const reported = yield* Ref.make<ReadonlySet<string>>(new Set())
|
|
1839
|
+
const together = yield* stage(
|
|
1840
|
+
laneOf(story),
|
|
1841
|
+
`story ${story.id}: gates after parallel tasks`,
|
|
1842
|
+
Effect.flatMap(gates, (result) => applyTriage(result, triage, laneOf(story), reported))
|
|
1843
|
+
)
|
|
1844
|
+
if (together.isClean) {
|
|
1845
|
+
break
|
|
1846
|
+
}
|
|
1847
|
+
if (revision >= mergeRevisions) {
|
|
1848
|
+
return yield* failed(
|
|
1849
|
+
story,
|
|
1850
|
+
`its gates are red with its parallel tasks merged together:\n${issueLines(together)}`
|
|
1851
|
+
)
|
|
1852
|
+
}
|
|
1853
|
+
yield* appendRevision(
|
|
1854
|
+
"make the story gates green after merging its parallel tasks",
|
|
1855
|
+
`Tasks of the story "${story.title}" ran at the same time and were merged together; the gates are red on the result.`,
|
|
1856
|
+
together
|
|
1857
|
+
)
|
|
1858
|
+
yield* guarded(implementTasks, implementTasks)
|
|
1859
|
+
}
|
|
1647
1860
|
|
|
1648
1861
|
// Other stories may have merged while this one ran: judge the branch
|
|
1649
1862
|
// against the epic as it is now.
|
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 })
|