saeeol 1.2.0 → 1.2.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/package.json +14 -14
- package/src/session/compaction-helpers.ts +1 -169
- package/src/session/compaction.ts +1 -712
- package/src/session/core/compaction/compaction-helpers.ts +169 -0
- package/src/session/core/compaction/compaction.ts +712 -0
- package/src/session/core/compaction/overflow.ts +28 -0
- package/src/session/core/instruction.ts +234 -0
- package/src/session/core/llm.ts +504 -0
- package/src/session/core/network.ts +392 -0
- package/src/session/core/processor.ts +731 -0
- package/src/session/core/projectors.ts +139 -0
- package/src/session/core/resolve-tools.ts +241 -0
- package/src/session/core/retry.ts +149 -0
- package/src/session/core/revert.ts +173 -0
- package/src/session/core/run-state.ts +110 -0
- package/src/session/core/schema.ts +35 -0
- package/src/session/core/session-types.ts +160 -0
- package/src/session/core/session.sql.ts +124 -0
- package/src/session/core/session.ts +948 -0
- package/src/session/core/shell-exec.ts +205 -0
- package/src/session/core/status.ts +100 -0
- package/src/session/core/subtask.ts +268 -0
- package/src/session/core/summary.ts +173 -0
- package/src/session/core/system.ts +114 -0
- package/src/session/core/todo.ts +86 -0
- package/src/session/core/user-part.ts +293 -0
- package/src/session/instruction.ts +1 -234
- package/src/session/llm.ts +1 -504
- package/src/session/message/message-errors.ts +83 -0
- package/src/session/message/message-parts.ts +89 -0
- package/src/session/message/message-query.ts +107 -0
- package/src/session/message/message-transform.ts +156 -0
- package/src/session/message/message-types.ts +68 -0
- package/src/session/message/message-v2.ts +73 -0
- package/src/session/message/message.ts +192 -0
- package/src/session/message-errors.ts +1 -83
- package/src/session/message-parts.ts +1 -89
- package/src/session/message-query.ts +1 -107
- package/src/session/message-transform.ts +1 -156
- package/src/session/message-types.ts +1 -68
- package/src/session/message-v2.ts +1 -73
- package/src/session/message.ts +1 -192
- package/src/session/network.ts +1 -392
- package/src/session/overflow.ts +1 -28
- package/src/session/processor.ts +1 -731
- package/src/session/projectors.ts +2 -139
- package/src/session/prompt/prompt-command.ts +93 -0
- package/src/session/prompt/prompt-loop.ts +299 -0
- package/src/session/prompt/prompt-model.ts +44 -0
- package/src/session/prompt/prompt-reminders.ts +120 -0
- package/src/session/prompt/prompt-resolve.ts +42 -0
- package/src/session/prompt/prompt-schemas.ts +128 -0
- package/src/session/prompt/prompt-title.ts +55 -0
- package/src/session/prompt/prompt-types.ts +47 -0
- package/src/session/prompt/prompt-user-msg.ts +80 -0
- package/src/session/prompt/prompt.ts +211 -0
- package/src/session/prompt-command.ts +1 -93
- package/src/session/prompt-loop.ts +1 -299
- package/src/session/prompt-model.ts +1 -44
- package/src/session/prompt-reminders.ts +1 -120
- package/src/session/prompt-resolve.ts +1 -42
- package/src/session/prompt-schemas.ts +1 -128
- package/src/session/prompt-title.ts +1 -55
- package/src/session/prompt-types.ts +1 -47
- package/src/session/prompt-user-msg.ts +1 -80
- package/src/session/prompt.ts +1 -211
- package/src/session/resolve-tools.ts +1 -241
- package/src/session/retry.ts +1 -149
- package/src/session/revert.ts +1 -173
- package/src/session/run-state.ts +1 -110
- package/src/session/schema.ts +1 -35
- package/src/session/session-types.ts +1 -160
- package/src/session/session.sql.ts +1 -124
- package/src/session/session.ts +1 -948
- package/src/session/shell-exec.ts +1 -205
- package/src/session/status.ts +1 -100
- package/src/session/subtask.ts +1 -268
- package/src/session/summary.ts +1 -173
- package/src/session/system.ts +1 -114
- package/src/session/todo.ts +1 -86
- package/src/session/user-part.ts +1 -293
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Effect, Layer, Context, Schema } from "effect"
|
|
2
|
+
import { Bus } from "../../bus"
|
|
3
|
+
import { Snapshot } from "../../snapshot"
|
|
4
|
+
import { Storage } from "@/storage/storage"
|
|
5
|
+
import { SyncEvent } from "../../sync"
|
|
6
|
+
import * as Log from "@saeeol/core/util/log"
|
|
7
|
+
import { zod } from "@/util/effect-zod"
|
|
8
|
+
import { withStatics } from "@/util/schema"
|
|
9
|
+
import * as Session from "./session"
|
|
10
|
+
import { MessageV2 } from "../message/message-v2"
|
|
11
|
+
import { SessionID, MessageID, PartID } from "./schema"
|
|
12
|
+
import { SessionRunState } from "./run-state"
|
|
13
|
+
import { SessionSummary } from "./summary"
|
|
14
|
+
|
|
15
|
+
const log = Log.create({ service: "session.revert" })
|
|
16
|
+
|
|
17
|
+
export const RevertInput = Schema.Struct({
|
|
18
|
+
sessionID: SessionID,
|
|
19
|
+
messageID: MessageID,
|
|
20
|
+
partID: Schema.optional(PartID),
|
|
21
|
+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
22
|
+
export type RevertInput = Schema.Schema.Type<typeof RevertInput>
|
|
23
|
+
|
|
24
|
+
export interface Interface {
|
|
25
|
+
readonly revert: (input: RevertInput) => Effect.Effect<Session.Info>
|
|
26
|
+
readonly unrevert: (input: { sessionID: SessionID }) => Effect.Effect<Session.Info>
|
|
27
|
+
readonly cleanup: (session: Session.Info) => Effect.Effect<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionRevert") {}
|
|
31
|
+
|
|
32
|
+
export const layer = Layer.effect(
|
|
33
|
+
Service,
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const sessions = yield* Session.Service
|
|
36
|
+
const snap = yield* Snapshot.Service
|
|
37
|
+
const storage = yield* Storage.Service
|
|
38
|
+
const bus = yield* Bus.Service
|
|
39
|
+
const summary = yield* SessionSummary.Service
|
|
40
|
+
const state = yield* SessionRunState.Service
|
|
41
|
+
const sync = yield* SyncEvent.Service
|
|
42
|
+
|
|
43
|
+
const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) {
|
|
44
|
+
yield* state.assertNotBusy(input.sessionID)
|
|
45
|
+
const all = yield* sessions.messages({ sessionID: input.sessionID })
|
|
46
|
+
let lastUser: MessageV2.User | undefined
|
|
47
|
+
const session = yield* sessions.get(input.sessionID)
|
|
48
|
+
|
|
49
|
+
let rev: Session.Info["revert"]
|
|
50
|
+
const patches: Snapshot.Patch[] = []
|
|
51
|
+
for (const msg of all) {
|
|
52
|
+
if (msg.info.role === "user") lastUser = msg.info
|
|
53
|
+
const remaining = []
|
|
54
|
+
for (const part of msg.parts) {
|
|
55
|
+
if (rev) {
|
|
56
|
+
if (part.type === "patch") patches.push(part)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!rev) {
|
|
61
|
+
if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) {
|
|
62
|
+
const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined
|
|
63
|
+
rev = {
|
|
64
|
+
messageID: !partID && lastUser ? lastUser.id : msg.info.id,
|
|
65
|
+
partID,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
remaining.push(part)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!rev) return session
|
|
74
|
+
|
|
75
|
+
rev.snapshot = session.revert?.snapshot ?? (yield* snap.track())
|
|
76
|
+
if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot)
|
|
77
|
+
// reflects changes being undone (files on disk still have AI modifications)
|
|
78
|
+
const range = all.filter((msg) => msg.info.id >= rev!.messageID)
|
|
79
|
+
const diffs = yield* summary.computeDiff({ messages: range })
|
|
80
|
+
|
|
81
|
+
yield* snap.revert(patches)
|
|
82
|
+
if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot as string)
|
|
83
|
+
yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore)
|
|
84
|
+
yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
|
|
85
|
+
const summaryDiffs: Snapshot.SummaryFileDiff[] = diffs.map((d) => ({
|
|
86
|
+
file: d.file,
|
|
87
|
+
additions: d.additions,
|
|
88
|
+
deletions: d.deletions,
|
|
89
|
+
status: d.status,
|
|
90
|
+
}))
|
|
91
|
+
yield* sessions.setRevert({
|
|
92
|
+
sessionID: input.sessionID,
|
|
93
|
+
revert: rev,
|
|
94
|
+
summary: {
|
|
95
|
+
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
|
|
96
|
+
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
|
|
97
|
+
files: diffs.length,
|
|
98
|
+
diffs: summaryDiffs,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
return yield* sessions.get(input.sessionID)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) {
|
|
105
|
+
log.info("unreverting", input)
|
|
106
|
+
yield* state.assertNotBusy(input.sessionID)
|
|
107
|
+
const session = yield* sessions.get(input.sessionID)
|
|
108
|
+
if (!session.revert) return session
|
|
109
|
+
if (session.revert.snapshot) yield* snap.restore(session.revert!.snapshot!)
|
|
110
|
+
yield* sessions.clearRevert(input.sessionID)
|
|
111
|
+
return yield* sessions.get(input.sessionID)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) {
|
|
115
|
+
if (!session.revert) return
|
|
116
|
+
const sessionID = session.id
|
|
117
|
+
const msgs = yield* sessions.messages({ sessionID })
|
|
118
|
+
const messageID = session.revert.messageID
|
|
119
|
+
const remove = [] as MessageV2.WithParts[]
|
|
120
|
+
let target: MessageV2.WithParts | undefined
|
|
121
|
+
for (const msg of msgs) {
|
|
122
|
+
if (msg.info.id < messageID) continue
|
|
123
|
+
if (msg.info.id > messageID) {
|
|
124
|
+
remove.push(msg)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
if (session.revert.partID) {
|
|
128
|
+
target = msg
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
remove.push(msg)
|
|
132
|
+
}
|
|
133
|
+
for (const msg of remove) {
|
|
134
|
+
yield* sync.run(MessageV2.Event.Removed, {
|
|
135
|
+
sessionID,
|
|
136
|
+
messageID: msg.info.id,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
if (session.revert.partID && target) {
|
|
140
|
+
const partID = session.revert.partID
|
|
141
|
+
const idx = target.parts.findIndex((part) => part.id === partID)
|
|
142
|
+
if (idx >= 0) {
|
|
143
|
+
const removeParts = target.parts.slice(idx)
|
|
144
|
+
target.parts = target.parts.slice(0, idx)
|
|
145
|
+
for (const part of removeParts) {
|
|
146
|
+
yield* sync.run(MessageV2.Event.PartRemoved, {
|
|
147
|
+
sessionID,
|
|
148
|
+
messageID: target.info.id,
|
|
149
|
+
partID: part.id,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
yield* sessions.clearRevert(sessionID)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return Service.of({ revert, unrevert, cleanup })
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
export const defaultLayer = Layer.suspend(() =>
|
|
162
|
+
layer.pipe(
|
|
163
|
+
Layer.provide(SessionRunState.defaultLayer),
|
|
164
|
+
Layer.provide(Session.defaultLayer),
|
|
165
|
+
Layer.provide(Snapshot.defaultLayer),
|
|
166
|
+
Layer.provide(Storage.defaultLayer),
|
|
167
|
+
Layer.provide(Bus.layer),
|
|
168
|
+
Layer.provide(SessionSummary.defaultLayer),
|
|
169
|
+
Layer.provide(SyncEvent.defaultLayer),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
export * as SessionRevert from "./revert"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { InstanceState } from "@/effect/instance-state"
|
|
2
|
+
import { Runner } from "@/effect/runner"
|
|
3
|
+
import { Effect, Latch, Layer, Scope, Context } from "effect"
|
|
4
|
+
import * as Session from "./session"
|
|
5
|
+
import { MessageV2 } from "../message/message-v2"
|
|
6
|
+
import { SessionID } from "./schema"
|
|
7
|
+
import { SessionStatus } from "./status"
|
|
8
|
+
|
|
9
|
+
export interface Interface {
|
|
10
|
+
readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect<void>
|
|
11
|
+
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
|
12
|
+
readonly ensureRunning: (
|
|
13
|
+
sessionID: SessionID,
|
|
14
|
+
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
|
15
|
+
work: Effect.Effect<MessageV2.WithParts>,
|
|
16
|
+
) => Effect.Effect<MessageV2.WithParts>
|
|
17
|
+
readonly startShell: (
|
|
18
|
+
sessionID: SessionID,
|
|
19
|
+
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
|
20
|
+
work: Effect.Effect<MessageV2.WithParts>,
|
|
21
|
+
ready?: Latch.Latch,
|
|
22
|
+
) => Effect.Effect<MessageV2.WithParts>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionRunState") {}
|
|
26
|
+
|
|
27
|
+
export const layer = Layer.effect(
|
|
28
|
+
Service,
|
|
29
|
+
Effect.gen(function* () {
|
|
30
|
+
const status = yield* SessionStatus.Service
|
|
31
|
+
|
|
32
|
+
const state = yield* InstanceState.make(
|
|
33
|
+
Effect.fn("SessionRunState.state")(function* () {
|
|
34
|
+
const scope = yield* Scope.Scope
|
|
35
|
+
const runners = new Map<SessionID, Runner.Runner<MessageV2.WithParts>>()
|
|
36
|
+
yield* Effect.addFinalizer(
|
|
37
|
+
Effect.fnUntraced(function* () {
|
|
38
|
+
yield* Effect.forEach(runners.values(), (runner) => runner.cancel, {
|
|
39
|
+
concurrency: "unbounded",
|
|
40
|
+
discard: true,
|
|
41
|
+
})
|
|
42
|
+
runners.clear()
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
return { runners, scope }
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const runner = Effect.fn("SessionRunState.runner")(function* (
|
|
50
|
+
sessionID: SessionID,
|
|
51
|
+
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
|
52
|
+
) {
|
|
53
|
+
const data = yield* InstanceState.get(state)
|
|
54
|
+
const existing = data.runners.get(sessionID)
|
|
55
|
+
if (existing) return existing
|
|
56
|
+
const next = Runner.make<MessageV2.WithParts>(data.scope, {
|
|
57
|
+
onIdle: Effect.gen(function* () {
|
|
58
|
+
data.runners.delete(sessionID)
|
|
59
|
+
yield* status.set(sessionID, { type: "idle" })
|
|
60
|
+
}),
|
|
61
|
+
onBusy: status.set(sessionID, { type: "busy" }),
|
|
62
|
+
onInterrupt,
|
|
63
|
+
busy: () => {
|
|
64
|
+
throw new Session.BusyError(sessionID)
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
data.runners.set(sessionID, next)
|
|
68
|
+
return next
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const assertNotBusy = Effect.fn("SessionRunState.assertNotBusy")(function* (sessionID: SessionID) {
|
|
72
|
+
const data = yield* InstanceState.get(state)
|
|
73
|
+
const existing = data.runners.get(sessionID)
|
|
74
|
+
if (existing?.busy) throw new Session.BusyError(sessionID)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const cancel = Effect.fn("SessionRunState.cancel")(function* (sessionID: SessionID) {
|
|
78
|
+
const data = yield* InstanceState.get(state)
|
|
79
|
+
const existing = data.runners.get(sessionID)
|
|
80
|
+
if (!existing || !existing.busy) {
|
|
81
|
+
yield* status.set(sessionID, { type: "idle" })
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
yield* existing.cancel
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* (
|
|
88
|
+
sessionID: SessionID,
|
|
89
|
+
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
|
90
|
+
work: Effect.Effect<MessageV2.WithParts>,
|
|
91
|
+
) {
|
|
92
|
+
return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const startShell = Effect.fn("SessionRunState.startShell")(function* (
|
|
96
|
+
sessionID: SessionID,
|
|
97
|
+
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
|
98
|
+
work: Effect.Effect<MessageV2.WithParts>,
|
|
99
|
+
ready?: Latch.Latch,
|
|
100
|
+
) {
|
|
101
|
+
return yield* (yield* runner(sessionID, onInterrupt)).startShell(work, ready)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return Service.of({ assertNotBusy, cancel, ensureRunning, startShell })
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export const defaultLayer = layer.pipe(Layer.provide(SessionStatus.defaultLayer))
|
|
109
|
+
|
|
110
|
+
export * as SessionRunState from "./run-state"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
import { Identifier } from "@/id/id"
|
|
4
|
+
import { zod, ZodOverride } from "@/util/effect-zod"
|
|
5
|
+
import { withStatics } from "@/util/schema"
|
|
6
|
+
|
|
7
|
+
export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("session") }).pipe(
|
|
8
|
+
Schema.brand("SessionID"),
|
|
9
|
+
withStatics((s) => ({
|
|
10
|
+
descending: (id?: string) => s.make(Identifier.descending("session", id)),
|
|
11
|
+
zod: zod(s),
|
|
12
|
+
})),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export type SessionID = Schema.Schema.Type<typeof SessionID>
|
|
16
|
+
|
|
17
|
+
export const MessageID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("message") }).pipe(
|
|
18
|
+
Schema.brand("MessageID"),
|
|
19
|
+
withStatics((s) => ({
|
|
20
|
+
ascending: (id?: string) => s.make(Identifier.ascending("message", id)),
|
|
21
|
+
zod: zod(s),
|
|
22
|
+
})),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export type MessageID = Schema.Schema.Type<typeof MessageID>
|
|
26
|
+
|
|
27
|
+
export const PartID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("part") }).pipe(
|
|
28
|
+
Schema.brand("PartID"),
|
|
29
|
+
withStatics((s) => ({
|
|
30
|
+
ascending: (id?: string) => s.make(Identifier.ascending("part", id)),
|
|
31
|
+
zod: zod(s),
|
|
32
|
+
})),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export type PartID = Schema.Schema.Type<typeof PartID>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { Decimal } from "decimal.js"
|
|
3
|
+
import z from "zod"
|
|
4
|
+
import { type ProviderMetadata, type LanguageModelUsage } from "ai"
|
|
5
|
+
import { InstallationVersion } from "@saeeol/core/installation/version"
|
|
6
|
+
import { BusEvent } from "@/bus/bus-event"
|
|
7
|
+
import { SyncEvent } from "../../sync"
|
|
8
|
+
import { PartTable, SessionTable } from "./session.sql"
|
|
9
|
+
import * as Log from "@saeeol/core/util/log"
|
|
10
|
+
import { MessageV2 } from "../message/message-v2"
|
|
11
|
+
import { Snapshot } from "@/snapshot"
|
|
12
|
+
import { ProjectID } from "../../project/schema"
|
|
13
|
+
import { WorkspaceID } from "../../control-plane/schema"
|
|
14
|
+
import { SessionID, MessageID, PartID } from "./schema"
|
|
15
|
+
import type { Provider } from "@/provider/provider"
|
|
16
|
+
import { Permission } from "@/permission"
|
|
17
|
+
import { Global } from "@saeeol/core/global"
|
|
18
|
+
import { SaeeolSession } from "@/saeeol/session"
|
|
19
|
+
import { Effect, Schema, Types } from "effect"
|
|
20
|
+
import { zod } from "@/util/effect-zod"
|
|
21
|
+
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
|
|
22
|
+
|
|
23
|
+
const log = Log.create({ service: "session" })
|
|
24
|
+
|
|
25
|
+
const parentTitlePrefix = "New session - "
|
|
26
|
+
const childTitlePrefix = "Child session - "
|
|
27
|
+
|
|
28
|
+
export function createDefaultTitle(isChild = false) {
|
|
29
|
+
return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isDefaultTitle(title: string) {
|
|
33
|
+
return new RegExp(`^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`).test(title)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type SessionRow = typeof SessionTable.$inferSelect
|
|
37
|
+
|
|
38
|
+
export function fromRow(row: SessionRow): Info {
|
|
39
|
+
const summary = row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null
|
|
40
|
+
? { additions: row.summary_additions ?? 0, deletions: row.summary_deletions ?? 0, files: row.summary_files ?? 0, diffs: row.summary_diffs ?? undefined }
|
|
41
|
+
: undefined
|
|
42
|
+
const share = row.share_url ? { url: row.share_url } : undefined
|
|
43
|
+
return {
|
|
44
|
+
id: row.id, slug: row.slug, projectID: row.project_id, workspaceID: row.workspace_id ?? undefined,
|
|
45
|
+
directory: row.directory, path: row.path ?? undefined, parentID: row.parent_id ?? undefined,
|
|
46
|
+
title: row.title, version: row.version, summary, share, revert: row.revert ?? undefined,
|
|
47
|
+
permission: row.permission ?? undefined,
|
|
48
|
+
time: { created: row.time_created, updated: row.time_updated, compacting: row.time_compacting ?? undefined, archived: row.time_archived ?? undefined },
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function toRow(info: Info) {
|
|
53
|
+
return {
|
|
54
|
+
id: info.id, project_id: info.projectID, workspace_id: info.workspaceID, parent_id: info.parentID,
|
|
55
|
+
slug: info.slug, directory: info.directory, path: info.path, title: info.title, version: info.version,
|
|
56
|
+
share_url: info.share?.url, summary_additions: info.summary?.additions, summary_deletions: info.summary?.deletions,
|
|
57
|
+
summary_files: info.summary?.files, summary_diffs: info.summary?.diffs, revert: info.revert ?? null,
|
|
58
|
+
permission: info.permission, time_created: info.time.created, time_updated: info.time.updated,
|
|
59
|
+
time_compacting: info.time.compacting, time_archived: info.time.archived,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getForkedTitle(title: string): string {
|
|
64
|
+
const match = title.match(/^(.+) \(fork #(\d+)\)$/)
|
|
65
|
+
if (match) return `${match[1]} (fork #${parseInt(match[2], 10) + 1})`
|
|
66
|
+
return `${title} (fork #1)`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function sessionPath(worktree: string, cwd: string) {
|
|
70
|
+
return path.relative(path.resolve(worktree), cwd).replaceAll("\\", "/")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const Summary = Schema.Struct({ additions: NonNegativeInt, deletions: NonNegativeInt, files: NonNegativeInt, diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)) })
|
|
74
|
+
const Share = Schema.Struct({ url: Schema.String })
|
|
75
|
+
export const ArchivedTimestamp = Schema.Finite
|
|
76
|
+
const Time = Schema.Struct({ created: NonNegativeInt, updated: NonNegativeInt, compacting: optionalOmitUndefined(NonNegativeInt), archived: optionalOmitUndefined(ArchivedTimestamp) })
|
|
77
|
+
const Revert = Schema.Struct({ messageID: MessageID, partID: optionalOmitUndefined(PartID), snapshot: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String) })
|
|
78
|
+
|
|
79
|
+
export const Info = Schema.Struct({
|
|
80
|
+
id: SessionID, slug: Schema.String, projectID: ProjectID, workspaceID: optionalOmitUndefined(WorkspaceID),
|
|
81
|
+
directory: Schema.String, path: optionalOmitUndefined(Schema.String), parentID: optionalOmitUndefined(SessionID),
|
|
82
|
+
summary: optionalOmitUndefined(Summary), share: optionalOmitUndefined(Share), title: Schema.String,
|
|
83
|
+
version: Schema.String, time: Time, permission: optionalOmitUndefined(Permission.Ruleset), revert: optionalOmitUndefined(Revert),
|
|
84
|
+
}).annotate({ identifier: "Session" }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
85
|
+
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
|
86
|
+
|
|
87
|
+
export const ProjectInfo = Schema.Struct({ id: ProjectID, name: optionalOmitUndefined(Schema.String), worktree: Schema.String })
|
|
88
|
+
.annotate({ identifier: "ProjectSummary" }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
89
|
+
export type ProjectInfo = Types.DeepMutable<Schema.Schema.Type<typeof ProjectInfo>>
|
|
90
|
+
|
|
91
|
+
export const GlobalInfo = Schema.Struct({ ...Info.fields, project: Schema.NullOr(ProjectInfo), worktreeName: Schema.optional(Schema.String) })
|
|
92
|
+
.annotate({ identifier: "GlobalSession" }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
93
|
+
export type GlobalInfo = Types.DeepMutable<Schema.Schema.Type<typeof GlobalInfo>>
|
|
94
|
+
|
|
95
|
+
export const CreateInput = Schema.optional(Schema.Struct({
|
|
96
|
+
parentID: Schema.optional(SessionID), title: Schema.optional(Schema.String), permission: Schema.optional(Permission.Ruleset),
|
|
97
|
+
platform: Schema.optional(Schema.String), workspaceID: Schema.optional(WorkspaceID),
|
|
98
|
+
})).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
99
|
+
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
|
100
|
+
|
|
101
|
+
export const ForkInput = Schema.Struct({ sessionID: SessionID, messageID: Schema.optional(MessageID) }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
102
|
+
export const GetInput = SessionID
|
|
103
|
+
export const ChildrenInput = SessionID
|
|
104
|
+
export const RemoveInput = SessionID
|
|
105
|
+
export const SetTitleInput = Schema.Struct({ sessionID: SessionID, title: Schema.String }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
106
|
+
export const SetArchivedInput = Schema.Struct({ sessionID: SessionID, time: Schema.optional(ArchivedTimestamp) }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
107
|
+
export const SetPermissionInput = Schema.Struct({ sessionID: SessionID, permission: Permission.Ruleset }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
108
|
+
export const SetRevertInput = Schema.Struct({ sessionID: SessionID, revert: Schema.optional(Revert), summary: Schema.optional(Summary) }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
109
|
+
export const MessagesInput = Schema.Struct({ sessionID: SessionID, limit: Schema.optional(NonNegativeInt) }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
110
|
+
|
|
111
|
+
export type ListInput = { directory?: string; scope?: "project"; path?: string; workspaceID?: WorkspaceID; roots?: boolean; start?: number; search?: string; limit?: number }
|
|
112
|
+
|
|
113
|
+
const CreatedEventSchema = Schema.Struct({ sessionID: SessionID, info: Info })
|
|
114
|
+
const UpdatedShare = Schema.Struct({ url: Schema.optional(Schema.NullOr(Schema.String)) })
|
|
115
|
+
const UpdatedTime = Schema.Struct({ created: Schema.optional(Schema.NullOr(NonNegativeInt)), updated: Schema.optional(Schema.NullOr(NonNegativeInt)), compacting: Schema.optional(Schema.NullOr(NonNegativeInt)), archived: Schema.optional(Schema.NullOr(ArchivedTimestamp)) })
|
|
116
|
+
const UpdatedInfo = Schema.Struct({
|
|
117
|
+
id: Schema.optional(Schema.NullOr(SessionID)), slug: Schema.optional(Schema.NullOr(Schema.String)),
|
|
118
|
+
projectID: Schema.optional(Schema.NullOr(ProjectID)), workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
|
|
119
|
+
directory: Schema.optional(Schema.NullOr(Schema.String)), path: Schema.optional(Schema.NullOr(Schema.String)),
|
|
120
|
+
parentID: Schema.optional(Schema.NullOr(SessionID)), summary: Schema.optional(Schema.NullOr(Summary)),
|
|
121
|
+
share: Schema.optional(UpdatedShare), title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
122
|
+
version: Schema.optional(Schema.NullOr(Schema.String)), time: Schema.optional(UpdatedTime),
|
|
123
|
+
permission: Schema.optional(Schema.NullOr(Permission.Ruleset)), revert: Schema.optional(Schema.NullOr(Revert)),
|
|
124
|
+
})
|
|
125
|
+
const UpdatedEventSchema = Schema.Struct({ sessionID: SessionID, info: UpdatedInfo })
|
|
126
|
+
|
|
127
|
+
export const Event = {
|
|
128
|
+
Created: SyncEvent.define({ type: "session.created", version: 1, aggregate: "sessionID", schema: CreatedEventSchema }),
|
|
129
|
+
Updated: SyncEvent.define({ type: "session.updated", version: 1, aggregate: "sessionID", schema: UpdatedEventSchema, busSchema: CreatedEventSchema }),
|
|
130
|
+
Deleted: SyncEvent.define({ type: "session.deleted", version: 1, aggregate: "sessionID", schema: CreatedEventSchema }),
|
|
131
|
+
Diff: BusEvent.define("session.diff", Schema.Struct({ sessionID: SessionID, diff: Schema.Array(Snapshot.FileDiff) })),
|
|
132
|
+
Error: BusEvent.define("session.error", Schema.Struct({ sessionID: Schema.optional(SessionID), error: MessageV2.Assistant.fields.error })),
|
|
133
|
+
TurnOpen: SaeeolSession.Event.TurnOpen,
|
|
134
|
+
TurnClose: SaeeolSession.Event.TurnClose,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function plan(input: { slug: string; time: { created: number } }, instance: any) {
|
|
138
|
+
const base = instance.project.vcs ? path.join(instance.worktree, ".saeeol", "plans") : path.join(Global.Path.data, "plans")
|
|
139
|
+
return path.join(base, [input.time.created, input.slug].join("-") + ".md")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsage; metadata?: ProviderMetadata; provider?: Provider.Info }) => {
|
|
143
|
+
const safe = (v: number) => (Number.isFinite(v) ? v : 0)
|
|
144
|
+
const inputTokens = safe(input.usage.inputTokens ?? 0)
|
|
145
|
+
const outputTokens = safe(input.usage.outputTokens ?? 0)
|
|
146
|
+
const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0)
|
|
147
|
+
const cacheRead = safe(input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0)
|
|
148
|
+
const cacheWrite = safe(Number(input.usage.inputTokenDetails?.cacheWriteTokens ?? input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? (input.metadata as any)?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? (input.metadata as any)?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? 0))
|
|
149
|
+
const adjustedInput = safe(inputTokens - cacheRead - cacheWrite)
|
|
150
|
+
const tokens = { total: input.usage.totalTokens, input: adjustedInput, output: safe(outputTokens - reasoningTokens), reasoning: reasoningTokens, cache: { write: cacheWrite, read: cacheRead } }
|
|
151
|
+
const reported = SaeeolSession.providerCost({ metadata: input.metadata, usage: input.usage, provider: input.provider, providerID: input.model.providerID })
|
|
152
|
+
if (reported !== undefined) return { cost: safe(reported), tokens }
|
|
153
|
+
const costInfo = input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 ? input.model.cost.experimentalOver200K : input.model.cost
|
|
154
|
+
return {
|
|
155
|
+
cost: safe(new Decimal(0).add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)).add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)).add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)).add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)).add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)).toNumber()),
|
|
156
|
+
tokens,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class BusyError extends Error { constructor(public readonly sessionID: string) { super(`Session ${sessionID} is busy`) } }
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
|
|
2
|
+
import { ProjectTable } from "../../project/project.sql"
|
|
3
|
+
import type { MessageV2 } from "../message/message-v2"
|
|
4
|
+
import type { SessionEntry } from "../../v2/session-entry"
|
|
5
|
+
import type { Snapshot } from "../../snapshot"
|
|
6
|
+
import type { Permission } from "../../permission"
|
|
7
|
+
import type { ProjectID } from "../../project/schema"
|
|
8
|
+
import type { SessionID, MessageID, PartID } from "./schema"
|
|
9
|
+
import type { WorkspaceID } from "../../control-plane/schema"
|
|
10
|
+
import { Timestamps } from "../../storage/schema.sql"
|
|
11
|
+
|
|
12
|
+
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
|
|
13
|
+
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
|
|
14
|
+
|
|
15
|
+
export const SessionTable = sqliteTable(
|
|
16
|
+
"session",
|
|
17
|
+
{
|
|
18
|
+
id: text().$type<SessionID>().primaryKey(),
|
|
19
|
+
project_id: text()
|
|
20
|
+
.$type<ProjectID>()
|
|
21
|
+
.notNull()
|
|
22
|
+
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
|
23
|
+
workspace_id: text().$type<WorkspaceID>(),
|
|
24
|
+
parent_id: text().$type<SessionID>(),
|
|
25
|
+
slug: text().notNull(),
|
|
26
|
+
directory: text().notNull(),
|
|
27
|
+
path: text(),
|
|
28
|
+
title: text().notNull(),
|
|
29
|
+
version: text().notNull(),
|
|
30
|
+
share_url: text(),
|
|
31
|
+
summary_additions: integer(),
|
|
32
|
+
summary_deletions: integer(),
|
|
33
|
+
summary_files: integer(),
|
|
34
|
+
summary_diffs: text({ mode: "json" }).$type<Snapshot.SummaryFileDiff[]>(),
|
|
35
|
+
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
|
|
36
|
+
permission: text({ mode: "json" }).$type<Permission.Ruleset>(),
|
|
37
|
+
...Timestamps,
|
|
38
|
+
time_compacting: integer(),
|
|
39
|
+
time_archived: integer(),
|
|
40
|
+
},
|
|
41
|
+
(table) => [
|
|
42
|
+
index("session_project_idx").on(table.project_id),
|
|
43
|
+
index("session_workspace_idx").on(table.workspace_id),
|
|
44
|
+
index("session_parent_idx").on(table.parent_id),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export const MessageTable = sqliteTable(
|
|
49
|
+
"message",
|
|
50
|
+
{
|
|
51
|
+
id: text().$type<MessageID>().primaryKey(),
|
|
52
|
+
session_id: text()
|
|
53
|
+
.$type<SessionID>()
|
|
54
|
+
.notNull()
|
|
55
|
+
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
|
56
|
+
...Timestamps,
|
|
57
|
+
data: text({ mode: "json" }).notNull().$type<InfoData>(),
|
|
58
|
+
},
|
|
59
|
+
(table) => [index("message_session_time_created_id_idx").on(table.session_id, table.time_created, table.id)],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
export const PartTable = sqliteTable(
|
|
63
|
+
"part",
|
|
64
|
+
{
|
|
65
|
+
id: text().$type<PartID>().primaryKey(),
|
|
66
|
+
message_id: text()
|
|
67
|
+
.$type<MessageID>()
|
|
68
|
+
.notNull()
|
|
69
|
+
.references(() => MessageTable.id, { onDelete: "cascade" }),
|
|
70
|
+
session_id: text().$type<SessionID>().notNull(),
|
|
71
|
+
...Timestamps,
|
|
72
|
+
data: text({ mode: "json" }).notNull().$type<PartData>(),
|
|
73
|
+
},
|
|
74
|
+
(table) => [
|
|
75
|
+
index("part_message_id_id_idx").on(table.message_id, table.id),
|
|
76
|
+
index("part_session_idx").on(table.session_id),
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export const TodoTable = sqliteTable(
|
|
81
|
+
"todo",
|
|
82
|
+
{
|
|
83
|
+
session_id: text()
|
|
84
|
+
.$type<SessionID>()
|
|
85
|
+
.notNull()
|
|
86
|
+
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
|
87
|
+
content: text().notNull(),
|
|
88
|
+
status: text().notNull(),
|
|
89
|
+
priority: text().notNull(),
|
|
90
|
+
position: integer().notNull(),
|
|
91
|
+
...Timestamps,
|
|
92
|
+
},
|
|
93
|
+
(table) => [
|
|
94
|
+
primaryKey({ columns: [table.session_id, table.position] }),
|
|
95
|
+
index("todo_session_idx").on(table.session_id),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
export const SessionEntryTable = sqliteTable(
|
|
100
|
+
"session_entry",
|
|
101
|
+
{
|
|
102
|
+
id: text().$type<SessionEntry.ID>().primaryKey(),
|
|
103
|
+
session_id: text()
|
|
104
|
+
.$type<SessionID>()
|
|
105
|
+
.notNull()
|
|
106
|
+
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
|
107
|
+
type: text().$type<SessionEntry.Type>().notNull(),
|
|
108
|
+
...Timestamps,
|
|
109
|
+
data: text({ mode: "json" }).notNull().$type<Omit<SessionEntry.Entry, "type" | "id">>(),
|
|
110
|
+
},
|
|
111
|
+
(table) => [
|
|
112
|
+
index("session_entry_session_idx").on(table.session_id),
|
|
113
|
+
index("session_entry_session_type_idx").on(table.session_id, table.type),
|
|
114
|
+
index("session_entry_time_created_idx").on(table.time_created),
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
export const PermissionTable = sqliteTable("permission", {
|
|
119
|
+
project_id: text()
|
|
120
|
+
.primaryKey()
|
|
121
|
+
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
|
122
|
+
...Timestamps,
|
|
123
|
+
data: text({ mode: "json" }).notNull().$type<Permission.Ruleset>(),
|
|
124
|
+
})
|