@llm4ts/flow 2.21.1 → 2.22.1
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 +4 -3
- package/dist/Activity.d.ts.map +1 -1
- package/dist/Activity.js +48 -5
- package/dist/Activity.js.map +1 -1
- package/dist/FlowEvents.d.ts +27 -1
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +39 -1
- package/dist/FlowEvents.js.map +1 -1
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +11 -1
- package/dist/Review.js.map +1 -1
- package/dist/Roster.d.ts.map +1 -1
- package/dist/Roster.js +13 -1
- package/dist/Roster.js.map +1 -1
- package/dist/Stories.d.ts +20 -6
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +82 -24
- package/dist/Stories.js.map +1 -1
- package/dist/Timing.d.ts +7 -0
- package/dist/Timing.d.ts.map +1 -0
- package/dist/Timing.js +68 -0
- package/dist/Timing.js.map +1 -0
- package/package.json +3 -2
- package/src/Activity.ts +88 -16
- package/src/FlowEvents.ts +41 -1
- package/src/Review.ts +13 -0
- package/src/Roster.ts +17 -0
- package/src/Stories.ts +140 -46
- package/src/Timing.ts +112 -0
package/src/Stories.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// merges back only after its judge and perimeter checks pass, and the
|
|
5
5
|
// target's gates run on the epic branch after every merge. Scheduling,
|
|
6
6
|
// gating, resume and failure policy live here; seats come from `contextFor`.
|
|
7
|
+
import * as Clock from "effect/Clock"
|
|
7
8
|
import * as Effect from "effect/Effect"
|
|
8
9
|
import * as Exit from "effect/Exit"
|
|
9
10
|
import * as Queue from "effect/Queue"
|
|
@@ -28,7 +29,15 @@ import {
|
|
|
28
29
|
StoryFailed,
|
|
29
30
|
type FlowError
|
|
30
31
|
} from "./FlowError.ts"
|
|
31
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
Info,
|
|
34
|
+
JudgedDimension,
|
|
35
|
+
StoryJudged,
|
|
36
|
+
Timed,
|
|
37
|
+
withLane,
|
|
38
|
+
type FlowEventsShape
|
|
39
|
+
} from "./FlowEvents.ts"
|
|
40
|
+
import { timeEffect } from "./Timing.ts"
|
|
32
41
|
import { statusPaths, type GitToolShape } from "./GitTool.ts"
|
|
33
42
|
import {
|
|
34
43
|
checkPerimeter,
|
|
@@ -44,10 +53,10 @@ import {
|
|
|
44
53
|
saveVersioned,
|
|
45
54
|
type PlainFileStoreShape
|
|
46
55
|
} from "./Persistence.ts"
|
|
47
|
-
import { Plan } from "./Plan.ts"
|
|
56
|
+
import { Plan, Task } from "./Plan.ts"
|
|
48
57
|
import { stage } from "./PlanExecution.ts"
|
|
49
58
|
import { planFrom } from "./Planner.ts"
|
|
50
|
-
import { ReviewResult } from "./Review.ts"
|
|
59
|
+
import { ReviewIssue, ReviewResult } from "./Review.ts"
|
|
51
60
|
import type { Reviewer } from "./Reviewer.ts"
|
|
52
61
|
import {
|
|
53
62
|
dependentsOf,
|
|
@@ -464,7 +473,7 @@ export interface StoriesOptions {
|
|
|
464
473
|
* installed dependencies, so the gates cannot run there without this.
|
|
465
474
|
* Runs on every start and resume; a failure fails the story.
|
|
466
475
|
*/
|
|
467
|
-
readonly setup?: (workDir: string) => Effect.Effect<void, FlowError>
|
|
476
|
+
readonly setup?: (workDir: string, events?: FlowEventsShape) => Effect.Effect<void, FlowError>
|
|
468
477
|
/**
|
|
469
478
|
* When setup fails, give the story's coder ONE turn to make the worktree
|
|
470
479
|
* ready (install, warm a cache, generate a client), then run setup again as
|
|
@@ -473,14 +482,30 @@ export interface StoriesOptions {
|
|
|
473
482
|
*/
|
|
474
483
|
readonly setupAgent?: boolean
|
|
475
484
|
/** The target's gates, run in a worktree per task and on the epic checkout after each merge. */
|
|
476
|
-
|
|
485
|
+
/**
|
|
486
|
+
* The target's gates in a directory. `events` is the story's lane, so a
|
|
487
|
+
* gate's timing names its story; a gate may ignore it.
|
|
488
|
+
*/
|
|
489
|
+
readonly gates: (
|
|
490
|
+
workDir: string,
|
|
491
|
+
events?: FlowEventsShape
|
|
492
|
+
) => Effect.Effect<ReviewResult, FlowError>
|
|
477
493
|
/** Story-level judge over the branch's diff against the epic branch; omit to skip. */
|
|
478
494
|
readonly judge?: (
|
|
479
495
|
story: Story,
|
|
480
496
|
diff: string,
|
|
481
|
-
seats: StorySeats
|
|
497
|
+
seats: StorySeats,
|
|
498
|
+
/**
|
|
499
|
+
* `diff`: the branch's changes against the epic. `code`: the branch has
|
|
500
|
+
* none, so this is the current code of the story's owned paths, to judge
|
|
501
|
+
* whether the story is already in place.
|
|
502
|
+
*/
|
|
503
|
+
subject?: "diff" | "code"
|
|
482
504
|
) => Effect.Effect<ReviewResult, FlowError>
|
|
483
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Judge attempts, each but the last followed by a revision task on the
|
|
507
|
+
* story's plan (coder, review, gates, commit). Default 2: one revision.
|
|
508
|
+
*/
|
|
484
509
|
readonly judgeRounds?: number
|
|
485
510
|
/** Extra system context per story (house rules, shared read-only excerpts). */
|
|
486
511
|
readonly system?: (story: Story) => Effect.Effect<string, FlowError>
|
|
@@ -570,6 +595,9 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
570
595
|
const judgeRounds = Math.max(1, options.judgeRounds ?? 2)
|
|
571
596
|
const statePath = (story: Story): string => join(options.stateDir, `stories/${story.id}.json`)
|
|
572
597
|
const planPath = (story: Story): string => join(options.stateDir, `stories/${story.id}.plan.md`)
|
|
598
|
+
/** Git's empty tree: a diff against it is a path's whole current content. */
|
|
599
|
+
const emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
600
|
+
const revisionTitle = /^Revision \d+: /u
|
|
573
601
|
|
|
574
602
|
/**
|
|
575
603
|
* The epic checkout must hold nothing uncommitted: every merge and epic
|
|
@@ -624,24 +652,37 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
624
652
|
|
|
625
653
|
/** Merge the story branch into the epic branch and re-gate the epic head, one story at a time. */
|
|
626
654
|
const integrate = (story: Story, branch: string): Effect.Effect<void, FlowError> =>
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
yield* context.git.rollback(checkpoint)
|
|
636
|
-
return yield* failed(
|
|
637
|
-
story,
|
|
638
|
-
`epic gates failed after merging; merge undone:\n${issueLines(gate)}`
|
|
655
|
+
Effect.flatMap(Clock.currentTimeMillis, (requested) =>
|
|
656
|
+
mergeLock.withPermit(
|
|
657
|
+
Effect.gen(function* () {
|
|
658
|
+
const lane = laneOf(story)
|
|
659
|
+
// Merges are one at a time: how long this story queued for its turn.
|
|
660
|
+
const turn = yield* Clock.currentTimeMillis
|
|
661
|
+
yield* lane.publish(
|
|
662
|
+
Timed.make({ kind: "wait", label: "merge lock", ms: turn - requested })
|
|
639
663
|
)
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
664
|
+
yield* epicCheckoutClean(story)
|
|
665
|
+
const checkpoint = yield* context.git.checkpoint
|
|
666
|
+
yield* timeEffect(
|
|
667
|
+
lane,
|
|
668
|
+
context.git.merge(branch, `${plan.epicId}: merge story ${story.id}`),
|
|
669
|
+
(ms, failed) =>
|
|
670
|
+
Timed.make({ kind: "merge", label: "merge", ms, ...(failed ? { failed } : {}) })
|
|
671
|
+
)
|
|
672
|
+
const gate = yield* options.gates(context.workDir, lane)
|
|
673
|
+
if (!gate.isClean) {
|
|
674
|
+
// Never leave a red epic head for the next story to inherit.
|
|
675
|
+
yield* context.git.rollback(checkpoint)
|
|
676
|
+
return yield* failed(
|
|
677
|
+
story,
|
|
678
|
+
`epic gates failed after merging; merge undone:\n${issueLines(gate)}`
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
yield* laneOf(story).publish(
|
|
682
|
+
Info.make({ message: `story ${story.id}: merged into ${epicBranch}` })
|
|
683
|
+
)
|
|
684
|
+
})
|
|
685
|
+
)
|
|
645
686
|
)
|
|
646
687
|
|
|
647
688
|
/** The worktree and branch for a story, honouring the hash-guarded resume rules. */
|
|
@@ -753,6 +794,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
753
794
|
): Effect.fn.Return<
|
|
754
795
|
{
|
|
755
796
|
readonly judge: string | undefined
|
|
797
|
+
/** Its branch had no changes and the judge found its code already on the epic. */
|
|
798
|
+
readonly inPlace: boolean
|
|
756
799
|
readonly totals: TokenUsage | undefined
|
|
757
800
|
readonly executor: string | undefined
|
|
758
801
|
},
|
|
@@ -794,7 +837,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
794
837
|
// Catch up before setup: the epic may have changed the manifest too.
|
|
795
838
|
yield* catchUp(story, git)
|
|
796
839
|
if (options.setup !== undefined) {
|
|
797
|
-
const setup = options.setup(state.worktree)
|
|
840
|
+
const setup = options.setup(state.worktree, laneOf(story))
|
|
798
841
|
yield* stage(
|
|
799
842
|
laneOf(story),
|
|
800
843
|
`story ${story.id}: setup`,
|
|
@@ -835,7 +878,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
835
878
|
// The target's gates plus the perimeter: a stray path is a gate failure
|
|
836
879
|
// the task review loop hands back to the coder before anything commits.
|
|
837
880
|
const gates: Effect.Effect<ReviewResult, FlowError> = Effect.gen(function* () {
|
|
838
|
-
const target = yield* options.gates(state.worktree)
|
|
881
|
+
const target = yield* options.gates(state.worktree, laneOf(story))
|
|
839
882
|
const changed = yield* perimeterNow(story, git)
|
|
840
883
|
return combined(target, perimeterGate([...changed.sharedReadOnly, ...changed.outside], story))
|
|
841
884
|
})
|
|
@@ -980,17 +1023,62 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
980
1023
|
yield* catchUp(story, git)
|
|
981
1024
|
yield* repairPerimeter
|
|
982
1025
|
|
|
1026
|
+
// A judge finding becomes a revision task on the story's own plan, run by
|
|
1027
|
+
// the task loop like any other: coder, review, gates, commit.
|
|
1028
|
+
const addRevision = (verdict: ReviewResult, empty: boolean): Effect.Effect<void, FlowError> =>
|
|
1029
|
+
Effect.gen(function* () {
|
|
1030
|
+
const store = makePlanStore(files)
|
|
1031
|
+
const current = yield* store.load(planPath(story))
|
|
1032
|
+
if (current === undefined) {
|
|
1033
|
+
return
|
|
1034
|
+
}
|
|
1035
|
+
const number = current.tasks.filter((task) => revisionTitle.test(task.title)).length + 1
|
|
1036
|
+
yield* store.save(
|
|
1037
|
+
planPath(story),
|
|
1038
|
+
Plan.make({
|
|
1039
|
+
...current,
|
|
1040
|
+
tasks: [
|
|
1041
|
+
...current.tasks,
|
|
1042
|
+
Task.make({
|
|
1043
|
+
title: `Revision ${number}: close the judge's findings`,
|
|
1044
|
+
description: [
|
|
1045
|
+
empty
|
|
1046
|
+
? `The story "${story.title}" has no changes yet, and what it must provide is not all in place on the epic branch.`
|
|
1047
|
+
: `The story "${story.title}" was judged short of done.`,
|
|
1048
|
+
"Close these gaps without weakening any test and without leaving your owned paths:",
|
|
1049
|
+
issueLines(verdict)
|
|
1050
|
+
].join("\n")
|
|
1051
|
+
})
|
|
1052
|
+
]
|
|
1053
|
+
})
|
|
1054
|
+
)
|
|
1055
|
+
})
|
|
1056
|
+
|
|
983
1057
|
let judgeNote: string | undefined
|
|
1058
|
+
let inPlace = false
|
|
984
1059
|
if (options.judge !== undefined) {
|
|
985
1060
|
const judge = options.judge
|
|
986
1061
|
for (let round = 1; round <= judgeRounds; round += 1) {
|
|
987
1062
|
const diff = yield* git.diffVsBase(epicBranch)
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const verdict =
|
|
1063
|
+
// No changes is not a failure by itself: the story's code may already
|
|
1064
|
+
// be on the epic (an earlier story or run put it there). Judge that
|
|
1065
|
+
// code instead: the owned paths against the empty tree.
|
|
1066
|
+
const empty = diff.trim().length === 0
|
|
1067
|
+
const subject = empty ? yield* git.diffVsBaseScoped(emptyTree, story.owned, false) : diff
|
|
1068
|
+
const verdict =
|
|
1069
|
+
empty && subject.trim().length === 0
|
|
1070
|
+
? // Nothing to judge is decided here, not asked of a model.
|
|
1071
|
+
ReviewResult.make({
|
|
1072
|
+
issues: [
|
|
1073
|
+
ReviewIssue.make({
|
|
1074
|
+
severity: "Critical",
|
|
1075
|
+
title: `nothing exists yet under ${story.owned.join(", ")}`,
|
|
1076
|
+
description: "The story's owned paths are empty on the epic branch."
|
|
1077
|
+
})
|
|
1078
|
+
],
|
|
1079
|
+
summary: `judge:${story.id}`
|
|
1080
|
+
})
|
|
1081
|
+
: yield* judge(story, subject, watchedSeats, empty ? "code" : "diff")
|
|
994
1082
|
yield* laneOf(story).publish(
|
|
995
1083
|
StoryJudged.make({
|
|
996
1084
|
lane: story.id,
|
|
@@ -1001,27 +1089,25 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1001
1089
|
})
|
|
1002
1090
|
)
|
|
1003
1091
|
if (verdict.isClean) {
|
|
1004
|
-
|
|
1092
|
+
inPlace = empty
|
|
1093
|
+
judgeNote = empty
|
|
1094
|
+
? `verified already in place (round ${round})`
|
|
1095
|
+
: `judge cleared (round ${round})`
|
|
1005
1096
|
break
|
|
1006
1097
|
}
|
|
1007
1098
|
if (round >= judgeRounds) {
|
|
1099
|
+
const revisions = `${round - 1} revision${round === 2 ? "" : "s"}`
|
|
1008
1100
|
return yield* failed(
|
|
1009
1101
|
story,
|
|
1010
|
-
|
|
1102
|
+
empty
|
|
1103
|
+
? `the story's code is not in place after ${revisions}, and its branch has no changes:\n${issueLines(verdict)}`
|
|
1104
|
+
: `judge not cleared after ${revisions}:\n${issueLines(verdict)}`
|
|
1011
1105
|
)
|
|
1012
1106
|
}
|
|
1013
|
-
yield*
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
`The story "${story.title}" scored below the bar. Close these gaps without`,
|
|
1017
|
-
"weakening any test and without leaving your owned paths, then stop:",
|
|
1018
|
-
issueLines(verdict)
|
|
1019
|
-
].join("\n")
|
|
1020
|
-
),
|
|
1021
|
-
Effect.void
|
|
1022
|
-
)
|
|
1107
|
+
yield* addRevision(verdict, empty)
|
|
1108
|
+
yield* guarded(implementTasks, implementTasks)
|
|
1109
|
+
yield* catchUp(story, git)
|
|
1023
1110
|
yield* repairPerimeter
|
|
1024
|
-
yield* commitGreen("address judge feedback", "while addressing judge feedback")
|
|
1025
1111
|
}
|
|
1026
1112
|
}
|
|
1027
1113
|
|
|
@@ -1031,6 +1117,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1031
1117
|
const history = roster === undefined ? [] : yield* roster.history
|
|
1032
1118
|
return {
|
|
1033
1119
|
judge: judgeNote,
|
|
1120
|
+
inPlace,
|
|
1034
1121
|
totals,
|
|
1035
1122
|
executor: history.length === 0 ? undefined : history.join(" → ")
|
|
1036
1123
|
}
|
|
@@ -1054,7 +1141,14 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1054
1141
|
})
|
|
1055
1142
|
}
|
|
1056
1143
|
const result = yield* Effect.scoped(implementStory(story, state))
|
|
1057
|
-
|
|
1144
|
+
if (result.inPlace) {
|
|
1145
|
+
// Verified already on the epic: there is nothing to merge or re-gate.
|
|
1146
|
+
yield* laneOf(story).publish(
|
|
1147
|
+
Info.make({ message: `story ${story.id}: verified already in place; nothing to merge` })
|
|
1148
|
+
)
|
|
1149
|
+
} else {
|
|
1150
|
+
yield* integrate(story, state.branch)
|
|
1151
|
+
}
|
|
1058
1152
|
const last = result.executor?.split(" → ").at(-1)
|
|
1059
1153
|
yield* saveVersioned(
|
|
1060
1154
|
files,
|
package/src/Timing.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as Clock from "effect/Clock"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Exit from "effect/Exit"
|
|
4
|
+
import * as Ref from "effect/Ref"
|
|
5
|
+
import * as Stream from "effect/Stream"
|
|
6
|
+
import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
7
|
+
import type { LlmChunk } from "@llm4ts/core/Models"
|
|
8
|
+
import { Timed, type FlowEventsShape } from "./FlowEvents.ts"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Where a run's time goes. `timedSeat` decorates a seat so every
|
|
12
|
+
* call publishes a `Timed{kind:"model"}` when it ends — its wall time, when
|
|
13
|
+
* its first output came, and the API and tool time the backend reported, if
|
|
14
|
+
* it did. The events carry the seat's role, never the prompt or the reply.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const reportedMs = (chunk: LlmChunk, key: string): number | undefined => {
|
|
18
|
+
const raw = chunk.metadata[key]
|
|
19
|
+
const value = raw === undefined ? Number.NaN : Number(raw)
|
|
20
|
+
return Number.isFinite(value) ? value : undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Times `effect`, publishing what `timed` makes of its duration whether it succeeds or fails. */
|
|
24
|
+
export const timeEffect = <A, E, R>(
|
|
25
|
+
events: FlowEventsShape,
|
|
26
|
+
effect: Effect.Effect<A, E, R>,
|
|
27
|
+
timed: (ms: number, failed: boolean) => Timed
|
|
28
|
+
): Effect.Effect<A, E, R> =>
|
|
29
|
+
Effect.flatMap(Clock.currentTimeMillis, (start) =>
|
|
30
|
+
Effect.onExit(effect, (exit) =>
|
|
31
|
+
Effect.flatMap(Clock.currentTimeMillis, (end) =>
|
|
32
|
+
events.publish(timed(end - start, Exit.isFailure(exit)))
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const timedStream = <E>(
|
|
38
|
+
events: FlowEventsShape,
|
|
39
|
+
label: string,
|
|
40
|
+
stream: Stream.Stream<LlmChunk, E>
|
|
41
|
+
): Stream.Stream<LlmChunk, E> =>
|
|
42
|
+
Stream.unwrap(
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const start = yield* Clock.currentTimeMillis
|
|
45
|
+
const first = yield* Ref.make<number | undefined>(undefined)
|
|
46
|
+
const reported = yield* Ref.make<{ apiMs?: number; toolMs?: number }>({})
|
|
47
|
+
const failed = yield* Ref.make(false)
|
|
48
|
+
return stream.pipe(
|
|
49
|
+
Stream.tap((chunk) =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const now = yield* Clock.currentTimeMillis
|
|
52
|
+
yield* Ref.update(first, (at) => at ?? now - start)
|
|
53
|
+
const apiMs = reportedMs(chunk, "api_ms")
|
|
54
|
+
const toolMs = reportedMs(chunk, "tools_ms")
|
|
55
|
+
if (apiMs !== undefined || toolMs !== undefined) {
|
|
56
|
+
yield* Ref.update(reported, (current) => ({
|
|
57
|
+
...current,
|
|
58
|
+
...(apiMs === undefined ? {} : { apiMs }),
|
|
59
|
+
...(toolMs === undefined ? {} : { toolMs })
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
),
|
|
64
|
+
Stream.tapError(() => Ref.set(failed, true)),
|
|
65
|
+
Stream.ensuring(
|
|
66
|
+
Effect.gen(function* () {
|
|
67
|
+
const end = yield* Clock.currentTimeMillis
|
|
68
|
+
const firstMs = yield* Ref.get(first)
|
|
69
|
+
yield* events.publish(
|
|
70
|
+
Timed.make({
|
|
71
|
+
kind: "model",
|
|
72
|
+
label,
|
|
73
|
+
ms: end - start,
|
|
74
|
+
...(firstMs === undefined ? {} : { firstMs }),
|
|
75
|
+
...(yield* Ref.get(reported)),
|
|
76
|
+
...((yield* Ref.get(failed)) ? { failed: true } : {})
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
export const timedSeat = (
|
|
86
|
+
service: LlmServiceShape,
|
|
87
|
+
events: FlowEventsShape,
|
|
88
|
+
label: string
|
|
89
|
+
): LlmServiceShape => {
|
|
90
|
+
const model = (ms: number, failed: boolean): Timed =>
|
|
91
|
+
Timed.make({ kind: "model", label, ms, ...(failed ? { failed: true } : {}) })
|
|
92
|
+
const batched = service.scoreLabelSequence
|
|
93
|
+
return {
|
|
94
|
+
executeStream: (prompt) => timedStream(events, label, service.executeStream(prompt)),
|
|
95
|
+
executeStreamWithHistory: (messages) =>
|
|
96
|
+
timedStream(events, label, service.executeStreamWithHistory(messages)),
|
|
97
|
+
executeWithTools: (prompt, tools) =>
|
|
98
|
+
timeEffect(events, service.executeWithTools(prompt, tools), model),
|
|
99
|
+
executeStructured: (prompt, schema, jsonSchema) =>
|
|
100
|
+
timeEffect(events, service.executeStructured(prompt, schema, jsonSchema), model),
|
|
101
|
+
executeStructuredWithUsage: (prompt, schema, jsonSchema) =>
|
|
102
|
+
timeEffect(events, service.executeStructuredWithUsage(prompt, schema, jsonSchema), model),
|
|
103
|
+
scoreLabels: (prompt, labels) => timeEffect(events, service.scoreLabels(prompt, labels), model),
|
|
104
|
+
...(batched === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: {
|
|
107
|
+
scoreLabelSequence: (prompt: string, labelSets: ReadonlyArray<ReadonlyArray<string>>) =>
|
|
108
|
+
timeEffect(events, batched(prompt, labelSets), model)
|
|
109
|
+
}),
|
|
110
|
+
isAvailable: service.isAvailable
|
|
111
|
+
}
|
|
112
|
+
}
|