@cmodjs/core 1.0.0 → 1.5.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/docs/commands.md +22 -11
- package/docs/dependencies.md +6 -4
- package/docs/hooks.md +26 -3
- package/docs/index.md +1 -1
- package/docs/install-steps.md +5 -3
- package/docs/mod.md +29 -3
- package/docs/permissions.md +4 -4
- package/docs/session.md +126 -63
- package/docs/testing.md +7 -5
- package/docs/ui.md +9 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +7 -2
- package/mod.d.ts +30 -6
- package/mod.d.ts.map +1 -1
- package/mod.js +34 -1
- package/package.json +1 -1
- package/records.d.ts +4 -1
- package/records.d.ts.map +1 -1
- package/records.js +6 -1
- package/register.d.ts.map +1 -1
- package/register.js +17 -0
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +2 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/claude.d.ts +2 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +1 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +333 -61
- package/runtime/dependencies.js +1 -1
- package/runtime/engine.d.ts +2 -6
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +8 -8
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +19 -4
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +6 -5
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +130 -59
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +5 -4
- package/runtime/sessions.d.ts +26 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +315 -117
- package/runtime/toaster.d.ts +5 -12
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +158 -25
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +26 -6
- package/session/context/context.d.ts +3 -1
- package/session/context/context.d.ts.map +1 -1
- package/session/context/turn/message/message.d.ts +1 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +42 -1
- package/session/context/turn/turn.d.ts +2 -1
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +20 -11
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +50 -27
- package/session/goal/goal.d.ts +3 -2
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +4 -4
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +80 -21
- package/session/session.d.ts.map +1 -1
- package/session/session.js +1 -0
- package/src/jobs/program.ts +7 -2
- package/src/mod.ts +48 -5
- package/src/records.ts +6 -1
- package/src/register.ts +17 -0
- package/src/runtime/announce.ts +2 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/claude.ts +3 -0
- package/src/runtime/conversations.ts +337 -60
- package/src/runtime/dependencies.ts +1 -1
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/grants.ts +8 -10
- package/src/runtime/hooks.ts +20 -4
- package/src/runtime/installer.ts +6 -5
- package/src/runtime/lifecycle.ts +136 -56
- package/src/runtime/metadata.ts +5 -4
- package/src/runtime/sessions.ts +316 -111
- package/src/runtime/toaster.ts +183 -45
- package/src/runtime/ui.ts +23 -4
- package/src/session/context/context.ts +3 -1
- package/src/session/context/turn/message/message.ts +39 -1
- package/src/session/context/turn/turn.ts +2 -1
- package/src/session/conversation.ts +56 -31
- package/src/session/goal/goal.ts +3 -2
- package/src/session/goal/task.ts +4 -4
- package/src/session/session.ts +62 -15
- package/src/testing/fake-claude.ts +4 -2
- package/src/testing/fake-sessions.ts +94 -20
- package/src/testing.ts +27 -3
- package/src/utils/call-effects.ts +2 -2
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +1 -1
- package/src/utils/paths.ts +6 -3
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -1
- package/testing/fake-sessions.d.ts +8 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +97 -18
- package/testing.d.ts +1 -0
- package/testing.d.ts.map +1 -1
- package/testing.js +23 -2
- package/utils/call-effects.d.ts +1 -0
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +1 -1
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.js +1 -1
- package/utils/paths.d.ts +2 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +6 -3
|
@@ -7,6 +7,7 @@ import { ToolResult } from './context/turn/message/tool-result.js'
|
|
|
7
7
|
import { TurnEnd, TurnStart, type Turn } from './context/turn/turn.js'
|
|
8
8
|
import { GoalChange, type Goal } from './goal/goal.js'
|
|
9
9
|
import { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
10
|
+
import type { UsageChange, Usage } from './session.js'
|
|
10
11
|
|
|
11
12
|
export type SessionData = {
|
|
12
13
|
readonly id: string
|
|
@@ -18,22 +19,26 @@ export type SessionData = {
|
|
|
18
19
|
readonly lastAt: string
|
|
19
20
|
readonly forkedFrom?: string
|
|
20
21
|
readonly isRunning: boolean
|
|
22
|
+
readonly agentType?: string
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export type ContextData =
|
|
25
|
+
export type ContextData = Pick<Context, 'number' | 'openedAt' | 'closedAt' | 'task'>
|
|
24
26
|
|
|
25
|
-
export type TurnData =
|
|
27
|
+
export type TurnData = Pick<Turn, 'prompt' | 'context' | 'openedAt' | 'closedAt'>
|
|
26
28
|
|
|
27
|
-
export type TaskData =
|
|
29
|
+
export type TaskData = Pick<Task, 'id' | 'title' | 'openedBy' | 'openedAt' | 'closedAt'> & { readonly agentType: string; readonly model?: string; readonly parent?: string }
|
|
28
30
|
|
|
29
|
-
export type
|
|
31
|
+
export type RunningTask = TaskData & { readonly context: number; readonly contextOpenedAt?: string }
|
|
32
|
+
|
|
33
|
+
export type GoalData = Pick<Goal, 'text'> & { readonly task?: string }
|
|
30
34
|
|
|
31
35
|
export type SessionChange =
|
|
32
36
|
| { readonly type: typeof Message; readonly message: Message }
|
|
33
37
|
| { readonly type: typeof TurnStart | typeof TurnEnd; readonly turn: TurnData }
|
|
34
38
|
| { readonly type: typeof ContextStart | typeof ContextEnd; readonly context: ContextData }
|
|
35
|
-
| { readonly type: typeof TaskStart | typeof TaskEnd; readonly task:
|
|
39
|
+
| { readonly type: typeof TaskStart | typeof TaskEnd; readonly task: RunningTask }
|
|
36
40
|
| { readonly type: typeof GoalChange; readonly goal: GoalData }
|
|
41
|
+
| { readonly type: typeof UsageChange; readonly usage: Usage }
|
|
37
42
|
|
|
38
43
|
export type ConversationState = {
|
|
39
44
|
readonly session: string
|
|
@@ -43,13 +48,13 @@ export type ConversationState = {
|
|
|
43
48
|
readonly goal?: string
|
|
44
49
|
readonly agentCalls: readonly string[]
|
|
45
50
|
readonly endedCalls: Readonly<Record<string, string>>
|
|
46
|
-
readonly tasks: Readonly<Record<string,
|
|
51
|
+
readonly tasks: Readonly<Record<string, RunningTask>>
|
|
52
|
+
readonly taskTurns: Readonly<Record<string, TurnData>>
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export type Conversation = {
|
|
50
56
|
add(row: Row, task?: string): SessionChange[]
|
|
51
57
|
startTask(task: TaskData): SessionChange[]
|
|
52
|
-
endContext(at: string): SessionChange[]
|
|
53
58
|
startContext(at: string): SessionChange[]
|
|
54
59
|
changeGoal(text: string): SessionChange[]
|
|
55
60
|
readonly state: ConversationState
|
|
@@ -62,7 +67,7 @@ const agentTools: readonly string[] = ['Agent', 'Task']
|
|
|
62
67
|
const asyncLaunch = 'Async agent launched'
|
|
63
68
|
|
|
64
69
|
export function emptyConversation(session: string): ConversationState {
|
|
65
|
-
return { session, context: 0, contextOpenedAt: '', agentCalls: [], endedCalls: {}, tasks: {} }
|
|
70
|
+
return { session, context: 0, contextOpenedAt: '', agentCalls: [], endedCalls: {}, tasks: {}, taskTurns: {} }
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
export function createConversation(start: ConversationState): Conversation {
|
|
@@ -70,6 +75,14 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
70
75
|
const agentCalls = new Set(start.agentCalls)
|
|
71
76
|
const endedCalls = new Map(Object.entries(start.endedCalls))
|
|
72
77
|
const tasks = new Map(Object.entries(start.tasks))
|
|
78
|
+
const taskTurns = new Map(Object.entries(start.taskTurns))
|
|
79
|
+
|
|
80
|
+
const endTaskTurn = (task: string, at: string): SessionChange[] => {
|
|
81
|
+
const open = taskTurns.get(task)
|
|
82
|
+
if (open === undefined) return []
|
|
83
|
+
taskTurns.delete(task)
|
|
84
|
+
return open.closedAt === undefined ? [{ type: TurnEnd, turn: { ...open, closedAt: at } }] : []
|
|
85
|
+
}
|
|
73
86
|
|
|
74
87
|
const startContext = (at: string): SessionChange[] => {
|
|
75
88
|
context += 1
|
|
@@ -79,6 +92,17 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
79
92
|
|
|
80
93
|
const endContext = (at: string): SessionChange[] => (context === 0 ? [] : [{ type: ContextEnd, context: { number: context, openedAt: contextOpenedAt, closedAt: at } }])
|
|
81
94
|
|
|
95
|
+
const compactTask = (task: string, at: string): SessionChange[] => {
|
|
96
|
+
const owner = tasks.get(task)
|
|
97
|
+
if (owner === undefined) return []
|
|
98
|
+
const next = { ...owner, context: owner.context + 1, contextOpenedAt: at }
|
|
99
|
+
tasks.set(task, next)
|
|
100
|
+
return [
|
|
101
|
+
{ type: ContextEnd, context: { number: owner.context, openedAt: owner.contextOpenedAt ?? owner.openedAt, closedAt: at, task } },
|
|
102
|
+
{ type: ContextStart, context: { number: next.context, openedAt: at, task } },
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
const endTurn = (at: string): SessionChange[] => {
|
|
83
107
|
if (turn === undefined) return []
|
|
84
108
|
const ended = { ...turn, closedAt: at }
|
|
@@ -86,17 +110,19 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
86
110
|
return [{ type: TurnEnd, turn: ended }]
|
|
87
111
|
}
|
|
88
112
|
|
|
89
|
-
const endTask = (call: string, at: string): SessionChange[] => {
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
113
|
+
const endTask = (call: string, at: string, only?: string): SessionChange[] => {
|
|
114
|
+
const opened = [...tasks.values()].filter((each) => each.openedBy === call && (only === undefined || each.id === only))
|
|
115
|
+
if (opened.length === 0) {
|
|
92
116
|
if (agentCalls.has(call)) endedCalls.set(call, at)
|
|
93
117
|
return []
|
|
94
118
|
}
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
if (only === undefined) agentCalls.delete(call)
|
|
120
|
+
return opened.flatMap((task) => {
|
|
121
|
+
if (task.closedAt !== undefined) return []
|
|
122
|
+
const ended = { ...task, closedAt: at }
|
|
123
|
+
tasks.set(task.id, ended)
|
|
124
|
+
return [...endTaskTurn(task.id, at), { type: TaskEnd, task: ended }]
|
|
125
|
+
})
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
const changeGoal = (text: string): SessionChange[] => {
|
|
@@ -109,30 +135,32 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
109
135
|
const changes: SessionChange[] = []
|
|
110
136
|
if (task === undefined && context === 0) changes.push(...startContext(row.at))
|
|
111
137
|
if (task === undefined && row.type === 'system' && row.name === 'turn_duration') return [...changes, ...endTurn(row.at)]
|
|
112
|
-
if (
|
|
138
|
+
if (row.type === 'system' && row.name === 'compact_boundary') return [...changes, ...(task === undefined ? [...endContext(row.at), ...startContext(row.at)] : compactTask(task, row.at))]
|
|
113
139
|
const owner = task === undefined ? undefined : tasks.get(task)
|
|
140
|
+
const placedIn = owner?.context ?? context
|
|
114
141
|
for (const unplaced of messagesOf(row, task !== undefined)) {
|
|
115
|
-
if (
|
|
116
|
-
changes.push(...endTurn(row.at))
|
|
117
|
-
const opened: TurnData = { prompt: { ...unplaced, session: start.session, context, turn: unplaced.id }, context, openedAt: row.at }
|
|
118
|
-
turn = opened
|
|
142
|
+
if (unplaced.type === Prompt) {
|
|
143
|
+
changes.push(...(task === undefined ? endTurn(row.at) : endTaskTurn(task, row.at)))
|
|
144
|
+
const opened: TurnData = { prompt: { ...unplaced, session: start.session, context: placedIn, turn: unplaced.id, ...(task === undefined ? {} : { task }) }, context: placedIn, openedAt: row.at, ...(owner?.closedAt === undefined ? {} : { closedAt: owner.closedAt }) }
|
|
145
|
+
if (task === undefined) turn = opened
|
|
146
|
+
else taskTurns.set(task, opened)
|
|
119
147
|
changes.push({ type: TurnStart, turn: opened })
|
|
120
|
-
if (goal === undefined) changes.push(...changeGoal(unplaced.text))
|
|
148
|
+
if (task === undefined && goal === undefined) changes.push(...changeGoal(unplaced.text))
|
|
121
149
|
}
|
|
150
|
+
const open = task === undefined ? turn : taskTurns.get(task)
|
|
122
151
|
const placed = {
|
|
123
152
|
...unplaced,
|
|
124
153
|
session: start.session,
|
|
125
|
-
context:
|
|
126
|
-
...(
|
|
154
|
+
context: placedIn,
|
|
155
|
+
...(open === undefined ? {} : { turn: open.prompt.id }),
|
|
127
156
|
...(task === undefined ? {} : { task }),
|
|
128
157
|
} as Message
|
|
129
158
|
changes.push({ type: Message, message: placed })
|
|
130
|
-
if (task !== undefined) continue
|
|
131
159
|
if (placed.type === ToolCall && agentTools.includes(placed.name)) agentCalls.add(placed.callId)
|
|
132
160
|
if (placed.type === ToolResult && agentCalls.has(placed.callId) && !placed.text.startsWith(asyncLaunch)) changes.push(...endTask(placed.callId, row.at))
|
|
133
161
|
if (placed.type === Note && placed.from.type === Task) {
|
|
134
162
|
const call = tagOf(textOf(row.content), 'tool-use-id')
|
|
135
|
-
if (call !== undefined && agentCalls.has(call)) changes.push(...endTask(call, row.at))
|
|
163
|
+
if (call !== undefined && agentCalls.has(call)) changes.push(...endTask(call, row.at, tagOf(textOf(row.content), 'task-id')))
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
166
|
return changes
|
|
@@ -144,13 +172,9 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
144
172
|
agentCalls.add(data.openedBy)
|
|
145
173
|
const changes: SessionChange[] = [{ type: TaskStart, task: started }]
|
|
146
174
|
const endedAt = endedCalls.get(data.openedBy)
|
|
147
|
-
if (endedAt !== undefined)
|
|
148
|
-
endedCalls.delete(data.openedBy)
|
|
149
|
-
changes.push(...endTask(data.openedBy, endedAt))
|
|
150
|
-
}
|
|
175
|
+
if (endedAt !== undefined) changes.push(...endTask(data.openedBy, endedAt, data.id))
|
|
151
176
|
return changes
|
|
152
177
|
},
|
|
153
|
-
endContext,
|
|
154
178
|
startContext,
|
|
155
179
|
changeGoal,
|
|
156
180
|
get state() {
|
|
@@ -163,6 +187,7 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
163
187
|
agentCalls: [...agentCalls],
|
|
164
188
|
endedCalls: Object.fromEntries(endedCalls),
|
|
165
189
|
tasks: Object.fromEntries(tasks),
|
|
190
|
+
taskTurns: Object.fromEntries(taskTurns),
|
|
166
191
|
}
|
|
167
192
|
},
|
|
168
193
|
}
|
package/src/session/goal/goal.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { RecordPart } from '../session.js'
|
|
1
2
|
import type { Task } from './task.js'
|
|
2
3
|
|
|
3
4
|
export const GoalChange = 'goalChange'
|
|
4
5
|
|
|
5
|
-
export type Goal = {
|
|
6
|
+
export type Goal = RecordPart & {
|
|
6
7
|
readonly text: string
|
|
7
|
-
update(change: { readonly text: string }): Promise<void>
|
|
8
8
|
tasks(): Promise<Task[]>
|
|
9
|
+
update(change: { readonly text: string }): Promise<void>
|
|
9
10
|
}
|
package/src/session/goal/task.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from '../context/turn/message/message.js'
|
|
2
|
+
import type { RecordPart, Session } from '../session.js'
|
|
2
3
|
|
|
3
4
|
export const Task = 'task'
|
|
4
5
|
|
|
@@ -6,14 +7,13 @@ export const TaskStart = 'taskStart'
|
|
|
6
7
|
|
|
7
8
|
export const TaskEnd = 'taskEnd'
|
|
8
9
|
|
|
9
|
-
export type Task = {
|
|
10
|
+
export type Task = RecordPart & {
|
|
10
11
|
readonly id: string
|
|
11
12
|
readonly title: string
|
|
12
|
-
readonly
|
|
13
|
-
readonly model?: string
|
|
13
|
+
readonly owner: Session
|
|
14
14
|
readonly openedBy: string
|
|
15
15
|
readonly openedAt: string
|
|
16
16
|
readonly closedAt?: string
|
|
17
|
-
messages(): Promise<Message[]>
|
|
18
17
|
tasks(): Promise<Task[]>
|
|
18
|
+
messages(): Promise<Message[]>
|
|
19
19
|
}
|
package/src/session/session.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Disposable } from '../
|
|
1
|
+
import type { Disposable } from '../utils/disposable.js'
|
|
2
2
|
import type { Matcher } from '../utils/matcher.js'
|
|
3
3
|
import type { Context, ContextEnd, ContextStart } from './context/context.js'
|
|
4
4
|
import type { Document } from './context/turn/message/document.js'
|
|
@@ -17,7 +17,9 @@ import type { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
|
17
17
|
|
|
18
18
|
export const Session = 'session'
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export const UsageChange = 'usageChange'
|
|
21
|
+
|
|
22
|
+
export type SessionEvent = typeof Message | MessageType | typeof ContextStart | typeof ContextEnd | typeof TurnStart | typeof TurnEnd | typeof TaskStart | typeof TaskEnd | typeof GoalChange | typeof UsageChange
|
|
21
23
|
|
|
22
24
|
export type SessionPayload = {
|
|
23
25
|
readonly message: Message
|
|
@@ -36,16 +38,53 @@ export type SessionPayload = {
|
|
|
36
38
|
readonly taskStart: Task
|
|
37
39
|
readonly taskEnd: Task
|
|
38
40
|
readonly goalChange: Goal
|
|
41
|
+
readonly usageChange: Usage
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Refusal = { readonly refuse: string }
|
|
45
|
+
|
|
46
|
+
export type BeforeAnswer = {
|
|
47
|
+
readonly prompt: { readonly text: string } | Refusal
|
|
48
|
+
readonly note: { readonly text: string }
|
|
49
|
+
readonly skill: { readonly text: string }
|
|
50
|
+
readonly document: { readonly text: string } | Refusal
|
|
51
|
+
readonly reply: { readonly text: string }
|
|
52
|
+
readonly toolCall: { readonly input: Readonly<Record<string, unknown>> } | { readonly result: string } | Refusal
|
|
53
|
+
readonly toolResult: { readonly text: string }
|
|
54
|
+
readonly notice: { readonly text: string }
|
|
55
|
+
readonly taskStart: { readonly owner: { readonly model: string } } | Refusal
|
|
56
|
+
readonly taskEnd: Refusal
|
|
57
|
+
readonly turnEnd: Refusal
|
|
58
|
+
readonly contextEnd: Refusal
|
|
39
59
|
}
|
|
40
60
|
|
|
61
|
+
export type BeforeEvent = keyof BeforeAnswer
|
|
62
|
+
|
|
41
63
|
export type Usage = {
|
|
42
64
|
readonly model: string
|
|
43
|
-
readonly context: { readonly tokens?: number; readonly window
|
|
65
|
+
readonly context: { readonly tokens?: number; readonly window?: number; readonly percent?: number }
|
|
44
66
|
readonly cost?: { readonly usd: number }
|
|
45
67
|
}
|
|
46
68
|
|
|
69
|
+
export type Client = 'terminal' | 'desktop' | 'mobile' | 'vscode'
|
|
70
|
+
|
|
47
71
|
export type Listener<Event extends SessionEvent> = (payload: SessionPayload[Event]) => unknown
|
|
48
72
|
|
|
73
|
+
export type BeforeListener<Event extends BeforeEvent> = (payload: SessionPayload[Event]) => BeforeAnswer[Event] | void | Promise<BeforeAnswer[Event] | void>
|
|
74
|
+
|
|
75
|
+
export type SearchOptions = {
|
|
76
|
+
readonly limit?: number
|
|
77
|
+
readonly before?: Pick<Message, 'session' | 'id'>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type RecordPart = {
|
|
81
|
+
search(text: string, options?: SearchOptions): Promise<Message[]>
|
|
82
|
+
on<Event extends SessionEvent>(event: Event, listener: Listener<Event>): Disposable
|
|
83
|
+
on<Event extends SessionEvent>(event: Event, matcher: Matcher<SessionPayload[Event]>, listener: Listener<Event>): Disposable
|
|
84
|
+
before<Event extends BeforeEvent>(event: Event, listener: BeforeListener<Event>): Disposable
|
|
85
|
+
before<Event extends BeforeEvent>(event: Event, matcher: Matcher<SessionPayload[Event]>, listener: BeforeListener<Event>): Disposable
|
|
86
|
+
}
|
|
87
|
+
|
|
49
88
|
export type SessionSend = {
|
|
50
89
|
readonly type: typeof Prompt | typeof Note
|
|
51
90
|
readonly text: string
|
|
@@ -60,25 +99,33 @@ export type TaskStartArgs = {
|
|
|
60
99
|
readonly cwd?: string
|
|
61
100
|
}
|
|
62
101
|
|
|
63
|
-
export type
|
|
102
|
+
export type ToolCallArgs = {
|
|
103
|
+
readonly name: string
|
|
104
|
+
readonly input: Readonly<Record<string, unknown>>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type Session = RecordPart & {
|
|
64
108
|
readonly id: string
|
|
65
109
|
readonly title: string
|
|
66
110
|
readonly project: string
|
|
67
|
-
readonly
|
|
111
|
+
readonly agentType?: string
|
|
68
112
|
readonly model: string
|
|
113
|
+
readonly cwd: string
|
|
114
|
+
readonly isRunning: boolean
|
|
69
115
|
readonly startedAt: string
|
|
70
116
|
readonly lastAt: string
|
|
71
117
|
readonly forkedFrom?: string
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
118
|
+
goal(): Promise<Goal>
|
|
119
|
+
context(): Promise<Context>
|
|
120
|
+
contexts(): Promise<Context[]>
|
|
121
|
+
turn(): Promise<Turn | undefined>
|
|
122
|
+
turns(options?: { readonly last?: number }): Promise<Turn[]>
|
|
123
|
+
messages(options?: { readonly after?: string; readonly last?: number }): Promise<Message[]>
|
|
76
124
|
send(message: SessionSend): Promise<void>
|
|
77
125
|
startTask(task: TaskStartArgs): Promise<Task>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
goal(): Promise<Goal>
|
|
126
|
+
callTool(call: ToolCallArgs): Promise<ToolResult>
|
|
127
|
+
usage(): Promise<Usage>
|
|
128
|
+
clients(): Promise<Client[]>
|
|
82
129
|
}
|
|
83
130
|
|
|
84
131
|
export type SessionScope = {
|
|
@@ -87,8 +134,8 @@ export type SessionScope = {
|
|
|
87
134
|
readonly everyProject?: boolean
|
|
88
135
|
}
|
|
89
136
|
|
|
90
|
-
export type Sessions = {
|
|
137
|
+
export type Sessions = Omit<RecordPart, 'search'> & {
|
|
91
138
|
readonly current: Session
|
|
92
139
|
list(options?: SessionScope): Promise<Session[]>
|
|
93
|
-
search(text: string, options?: SessionScope &
|
|
140
|
+
search(text: string, options?: SessionScope & SearchOptions): Promise<Message[]>
|
|
94
141
|
}
|
|
@@ -15,7 +15,7 @@ export type Fakes = {
|
|
|
15
15
|
http: { fetch?: Claude['http']['fetch'] }
|
|
16
16
|
settings: { read?: Claude['settings']['read'] }
|
|
17
17
|
config: { list?: Claude['config']['list'] }
|
|
18
|
-
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll']; copy?: Claude['ui']['copy'] }
|
|
18
|
+
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll']; copy?: Claude['ui']['copy']; focus?: Claude['ui']['focus'] }
|
|
19
19
|
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append']; send?: Claude['session']['send'] }
|
|
20
20
|
prompt: { submit?: Claude['prompt']['submit'] }
|
|
21
21
|
model: { complete?: Claude['model']['complete'] }
|
|
@@ -119,6 +119,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
119
119
|
calls.push({ call: 'ui.open', args: [pane] })
|
|
120
120
|
const answer = fakes.ui.open === undefined ? { isPlaced: true as const } : await fakes.ui.open(pane)
|
|
121
121
|
if (answer.isPlaced) {
|
|
122
|
+
shown.openPanes.delete(pane.id)
|
|
122
123
|
shown.openPanes.add(pane.id)
|
|
123
124
|
unplacedPanes.delete(pane.id)
|
|
124
125
|
if (pane.focus === true) focusedPane = pane.id
|
|
@@ -134,11 +135,12 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
134
135
|
if (focusedPane === pane.id) focusedPane = undefined
|
|
135
136
|
},
|
|
136
137
|
panes: async () => [
|
|
137
|
-
...[...shown.openPanes].map((id) => ({ id, title: id, isShown:
|
|
138
|
+
...[...shown.openPanes].map((id) => ({ id, title: id, isShown: id === [...shown.openPanes].at(-1), isFocused: id === focusedPane, isPlaced: true })),
|
|
138
139
|
...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
|
|
139
140
|
],
|
|
140
141
|
scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
|
|
141
142
|
copy: rejected('ui.copy', () => fakes.ui.copy ?? (async () => ({ isCopied: true as const }))),
|
|
143
|
+
focus: rejected('ui.focus', () => fakes.ui.focus ?? (async () => ({}))),
|
|
142
144
|
resolve: () => elements,
|
|
143
145
|
},
|
|
144
146
|
process: {
|
|
@@ -6,12 +6,15 @@ import { Notice } from '../session/context/turn/message/notice.js'
|
|
|
6
6
|
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
7
7
|
import { Reply } from '../session/context/turn/message/reply.js'
|
|
8
8
|
import { Mod, Person, Schedule, type Sender } from '../session/context/turn/message/sender.js'
|
|
9
|
+
import { GoalChange } from '../session/goal/goal.js'
|
|
9
10
|
import { Task } from '../session/goal/task.js'
|
|
10
11
|
import { Skill } from '../session/context/turn/message/skill.js'
|
|
11
12
|
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
12
13
|
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
13
14
|
import { createConversation, emptyConversation, searchLimit, type ContextData, type Conversation, type SessionChange, type SessionData, type TaskData, type TurnData } from '../session/conversation.js'
|
|
14
|
-
import {
|
|
15
|
+
import type { Place } from '../runtime/before.js'
|
|
16
|
+
import type { Claude } from '../runtime/claude.js'
|
|
17
|
+
import { conversationMethods, everySession, isHeardBy, resultOf, type Subscription } from '../runtime/sessions.js'
|
|
15
18
|
import { receivedMatcher } from '../utils/matcher.js'
|
|
16
19
|
import { firstLine, messageOf } from '../utils/text.js'
|
|
17
20
|
|
|
@@ -46,16 +49,20 @@ type Heard = { readonly event: Subscription['event']; readonly matcher: unknown
|
|
|
46
49
|
export type FakeSessions = {
|
|
47
50
|
answer(caller: string, method: string, input: unknown): unknown
|
|
48
51
|
add(draft: MessageDraft, task?: string): Message[]
|
|
52
|
+
place(): Place
|
|
53
|
+
wants(mod: string, event: string): boolean
|
|
49
54
|
endTurn(): void
|
|
50
55
|
compact(): void
|
|
51
|
-
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string }): string
|
|
56
|
+
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string }): string
|
|
52
57
|
start(id: string): void
|
|
53
|
-
readonly sent: { readonly session: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
58
|
+
readonly sent: { readonly session: string; readonly task?: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
export function fakeSessions({ project, given, push }: { readonly project: () => string; readonly given: readonly FakeSession[]; readonly push: (mod: string, session: string, change: SessionChange) => void }): FakeSessions {
|
|
61
|
+
export function fakeSessions({ project, given, push, callTool }: { readonly project: () => string; readonly given: readonly FakeSession[]; readonly push: (mod: string, session: string, change: SessionChange) => void; readonly callTool: Claude['tool']['call'] }): FakeSessions {
|
|
57
62
|
const kept = new Map<string, Kept>()
|
|
58
63
|
const heard = new Map<string, Map<string, readonly Heard[]>>()
|
|
64
|
+
const watches = new Map<string, Map<string, readonly string[]>>()
|
|
65
|
+
const goals = new Map<string, string>()
|
|
59
66
|
const sent: FakeSessions['sent'] = []
|
|
60
67
|
let current = ''
|
|
61
68
|
let clock = Date.parse('2026-01-01T00:00:00.000Z')
|
|
@@ -76,7 +83,7 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
76
83
|
if ('task' in change) session.tasks.set(change.task.id, change.task)
|
|
77
84
|
session.data = { ...session.data, lastAt: change.type === Message ? change.message.at : session.data.lastAt, ...(session.data.title === '' && change.type === Message && change.message.type === Prompt && change.message.from.type === Person ? { title: firstLine(change.message.text) } : {}) }
|
|
78
85
|
for (const [mod, sessions] of heard) {
|
|
79
|
-
if ((sessions.get(session.data.id) ?? []).some(({ event, matcher }) => isHeardBy(event, matcher, change))) push(mod, session.data.id, change)
|
|
86
|
+
if ([...(sessions.get(session.data.id) ?? []), ...(sessions.get(everySession) ?? [])].some(({ event, matcher }) => isHeardBy(event, matcher, change))) push(mod, session.data.id, change)
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
}
|
|
@@ -101,6 +108,24 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
101
108
|
|
|
102
109
|
const currentSession = () => sessionOf(current)
|
|
103
110
|
|
|
111
|
+
const place = (): Place => {
|
|
112
|
+
const { session, context, contextOpenedAt, turn, tasks, taskTurns } = currentSession().conversation.state
|
|
113
|
+
return { session, context, contextOpenedAt, ...(turn === undefined ? {} : { turn }), tasks, taskTurns }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const taskOf = (session: Kept, id: string) => {
|
|
117
|
+
const task = session.tasks.get(id)
|
|
118
|
+
if (task === undefined) throw new Error(`Session ${session.data.id} has no task ${id}. Start it with tested.startTask.`)
|
|
119
|
+
return task
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const turnsOf = (session: Kept, task: unknown) => [...session.turns.values()].filter((turn) => turn.prompt.task === (typeof task === 'string' ? task : undefined))
|
|
123
|
+
|
|
124
|
+
const isUnder = (session: Kept | undefined, task: string, ancestor: string) => {
|
|
125
|
+
for (let at: string | undefined = task, steps = 0; at !== undefined && steps < 64; at = session?.tasks.get(at)?.parent, steps += 1) if (at === ancestor) return true
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
104
129
|
const lastOf = <Item>(items: readonly Item[], last: unknown): Item[] => (typeof last === 'number' ? items.slice(-last) : [...items])
|
|
105
130
|
|
|
106
131
|
const isInScope = (session: Kept, request: Readonly<Record<string, unknown>>) =>
|
|
@@ -115,10 +140,18 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
115
140
|
[conversationMethods.search](request) {
|
|
116
141
|
const text = String(request['text'])
|
|
117
142
|
if ([...text].length < 3) throw new Error('cmod history search takes at least 3 characters.')
|
|
143
|
+
const inRecord = (message: Message) => {
|
|
144
|
+
const session = kept.get(message.session)
|
|
145
|
+
const task = request['task']
|
|
146
|
+
if (request['session'] !== undefined && message.session !== request['session']) return false
|
|
147
|
+
if (typeof task === 'string' && (message.task === undefined || !isUnder(session, message.task, task) || (request['isGoal'] === true && message.task === task))) return false
|
|
148
|
+
if (task === undefined && request['isGoal'] === true && message.task === undefined) return false
|
|
149
|
+
return (request['turn'] === undefined || message.turn === request['turn']) && (request['context'] === undefined || message.context === request['context'])
|
|
150
|
+
}
|
|
118
151
|
const found = [...kept.values()]
|
|
119
|
-
.filter((session) => isInScope(session, request))
|
|
152
|
+
.filter((session) => request['session'] !== undefined || isInScope(session, request))
|
|
120
153
|
.flatMap((session) => session.messages)
|
|
121
|
-
.filter((message) => searchText(message).toLowerCase().includes(text.toLowerCase()))
|
|
154
|
+
.filter((message) => inRecord(message) && searchText(message).toLowerCase().includes(text.toLowerCase()))
|
|
122
155
|
.toSorted((first, second) => second.at.localeCompare(first.at))
|
|
123
156
|
const before = isObject(request['before']) ? request['before'] : undefined
|
|
124
157
|
const start = before === undefined ? 0 : found.findIndex((message) => message.session === before['session'] && message.id === before['id']) + 1
|
|
@@ -130,15 +163,59 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
130
163
|
const after = typeof request['after'] === 'string' ? fits.findIndex((message) => message.id === request['after']) + 1 : 0
|
|
131
164
|
return lastOf(fits.slice(after), request['last'])
|
|
132
165
|
},
|
|
133
|
-
[conversationMethods.turns]: (request) => lastOf(
|
|
134
|
-
[conversationMethods.contexts]
|
|
135
|
-
|
|
136
|
-
|
|
166
|
+
[conversationMethods.turns]: (request) => lastOf(turnsOf(sessionOf(String(request['session'])), request['task']).filter((turn) => request['context'] === undefined || turn.context === request['context']), request['last']),
|
|
167
|
+
[conversationMethods.contexts](request) {
|
|
168
|
+
const session = sessionOf(String(request['session']))
|
|
169
|
+
if (typeof request['task'] !== 'string') return [...session.contexts.values()]
|
|
170
|
+
const task = taskOf(session, request['task'])
|
|
171
|
+
return [{ number: session.conversation.state.tasks[task.id]?.context ?? 1, openedAt: task.openedAt, ...(task.closedAt === undefined ? {} : { closedAt: task.closedAt }) }]
|
|
172
|
+
},
|
|
173
|
+
[conversationMethods.tasks]: (request) => [...sessionOf(String(request['session'])).tasks.values()].filter((task) => request['all'] === true || task.parent === request['task']),
|
|
174
|
+
[conversationMethods.goal](request) {
|
|
175
|
+
const session = sessionOf(String(request['session']))
|
|
176
|
+
const task = request['task']
|
|
177
|
+
if (typeof task !== 'string') return { text: session.conversation.state.goal ?? '' }
|
|
178
|
+
return { text: goals.get(task) ?? turnsOf(session, task)[0]?.prompt.text ?? '' }
|
|
179
|
+
},
|
|
137
180
|
[conversationMethods.updateGoal](request) {
|
|
138
181
|
const session = sessionOf(String(request['session']))
|
|
139
|
-
|
|
182
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
183
|
+
const text = String(request['text'])
|
|
184
|
+
if (task === undefined) record(session, session.conversation.changeGoal(text))
|
|
185
|
+
else {
|
|
186
|
+
goals.set(task, text)
|
|
187
|
+
record(session, [{ type: GoalChange, goal: { text, task } }])
|
|
188
|
+
}
|
|
140
189
|
return null
|
|
141
190
|
},
|
|
191
|
+
[conversationMethods.watchBefore](request, caller) {
|
|
192
|
+
const targets = watches.get(caller) ?? new Map<string, readonly string[]>()
|
|
193
|
+
targets.set(String(request['session']), (Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []).map(({ event }) => event))
|
|
194
|
+
watches.set(caller, targets)
|
|
195
|
+
return null
|
|
196
|
+
},
|
|
197
|
+
async [conversationMethods.act](request) {
|
|
198
|
+
const session = sessionOf(String(request['session']))
|
|
199
|
+
const action = String(request['action'])
|
|
200
|
+
const input = isObject(request['input']) ? request['input'] : {}
|
|
201
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
202
|
+
const isLive = task === undefined ? session.data.isRunning : session.data.isRunning && taskOf(session, task).closedAt === undefined
|
|
203
|
+
if (action === 'usage') return { model: task === undefined ? session.data.model : (taskOf(session, task).model ?? session.data.model), context: {} }
|
|
204
|
+
if (action === 'clients') return isLive ? ['terminal'] : []
|
|
205
|
+
if (!isLive) throw new Error(`session ${task ?? session.data.id} has ended`)
|
|
206
|
+
if (action === 'send') {
|
|
207
|
+
sent.push({ session: session.data.id, ...(task === undefined ? {} : { task }), type: String(input['type']), text: String(input['text']), ...(typeof input['as'] === 'string' ? { as: input['as'] } : {}) })
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
if (action === 'startTask') {
|
|
211
|
+
const id = `task-${(ids += 1)}`
|
|
212
|
+
const data: TaskData = { id, title: typeof input['name'] === 'string' ? input['name'] : '', agentType: typeof input['agentType'] === 'string' ? input['agentType'] : 'general-purpose', openedBy: '', openedAt: now(), ...(typeof input['model'] === 'string' ? { model: input['model'] } : {}) }
|
|
213
|
+
record(session, session.conversation.startTask(data))
|
|
214
|
+
return data
|
|
215
|
+
}
|
|
216
|
+
if (session.data.id === current) return resultOf(place(), await callTool({ ...input['input'] as object, tool: String(input['name']) } as never))
|
|
217
|
+
return { id: '', type: ToolResult, at: now(), session: session.data.id, context: session.conversation.state.context, callId: '', text: '', isError: false }
|
|
218
|
+
},
|
|
142
219
|
[conversationMethods.subscribe](request, caller) {
|
|
143
220
|
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []
|
|
144
221
|
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
@@ -146,10 +223,6 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
146
223
|
heard.set(caller, sessions)
|
|
147
224
|
return null
|
|
148
225
|
},
|
|
149
|
-
[conversationMethods.send](request) {
|
|
150
|
-
sent.push({ session: String(request['session']), type: String(request['type']), text: String(request['text']), ...(typeof request['as'] === 'string' ? { as: request['as'] } : {}) })
|
|
151
|
-
return null
|
|
152
|
-
},
|
|
153
226
|
}
|
|
154
227
|
|
|
155
228
|
return {
|
|
@@ -163,19 +236,20 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
163
236
|
}
|
|
164
237
|
},
|
|
165
238
|
add: (draft, task) => add(currentSession(), draft, task),
|
|
239
|
+
place,
|
|
240
|
+
wants: (mod, event) => [current, everySession].some((target) => (watches.get(mod)?.get(target) ?? []).includes(event)),
|
|
166
241
|
endTurn() {
|
|
167
242
|
const session = currentSession()
|
|
168
243
|
record(session, session.conversation.add({ id: `turn-end-${(ids += 1)}`, at: now(), type: 'system', name: 'turn_duration', isMeta: false, isSummary: false, content: [] }))
|
|
169
244
|
},
|
|
170
245
|
compact() {
|
|
171
246
|
const session = currentSession()
|
|
172
|
-
|
|
173
|
-
record(session, [...session.conversation.endContext(at), ...session.conversation.startContext(at)])
|
|
247
|
+
record(session, session.conversation.add({ id: `compact-${(ids += 1)}`, at: now(), type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] }))
|
|
174
248
|
},
|
|
175
|
-
startTask({ title, agentType = 'general-purpose', model, openedBy = '' }) {
|
|
249
|
+
startTask({ title, agentType = 'general-purpose', model, openedBy = '', parent }) {
|
|
176
250
|
const session = currentSession()
|
|
177
251
|
const id = `task-${(ids += 1)}`
|
|
178
|
-
record(session, session.conversation.startTask({ id, title, agentType, openedBy, openedAt: now(), ...(model === undefined ? {} : { model }) }))
|
|
252
|
+
record(session, session.conversation.startTask({ id, title, agentType, openedBy, openedAt: now(), ...(model === undefined ? {} : { model }), ...(parent === undefined ? {} : { parent }) }))
|
|
179
253
|
return id
|
|
180
254
|
},
|
|
181
255
|
start(id) {
|