@cmodjs/core 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/docs/commands.md +5 -5
  2. package/docs/index.md +12 -11
  3. package/docs/jobs.md +12 -16
  4. package/docs/mod.md +117 -39
  5. package/docs/permissions.md +17 -21
  6. package/docs/session.md +171 -82
  7. package/docs/state.md +5 -5
  8. package/docs/testing.md +57 -41
  9. package/docs/ui.md +9 -9
  10. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  11. package/jobs/permissions/decide-permission.js +1 -1
  12. package/jobs/prompt.d.ts +1 -3
  13. package/jobs/prompt.d.ts.map +1 -1
  14. package/jobs/prompt.js +2 -21
  15. package/jobs/slash-command.d.ts +1 -1
  16. package/jobs/slash-command.d.ts.map +1 -1
  17. package/jobs/slash-command.js +1 -1
  18. package/jobs/status-line.d.ts +0 -1
  19. package/jobs/status-line.d.ts.map +1 -1
  20. package/jobs/status-line.js +3 -4
  21. package/mod.d.ts +53 -105
  22. package/mod.d.ts.map +1 -1
  23. package/mod.js +18 -80
  24. package/package.json +1 -1
  25. package/records.d.ts +3 -1
  26. package/records.d.ts.map +1 -1
  27. package/records.js +14 -5
  28. package/register.d.ts.map +1 -1
  29. package/register.js +3 -1
  30. package/runtime/before.d.ts +5 -1
  31. package/runtime/before.d.ts.map +1 -1
  32. package/runtime/before.js +81 -39
  33. package/runtime/claude.d.ts +6 -1
  34. package/runtime/claude.d.ts.map +1 -1
  35. package/runtime/conversations.d.ts.map +1 -1
  36. package/runtime/conversations.js +45 -26
  37. package/runtime/deadline.d.ts +17 -0
  38. package/runtime/deadline.d.ts.map +1 -1
  39. package/runtime/deadline.js +34 -0
  40. package/runtime/dependencies.d.ts +2 -0
  41. package/runtime/dependencies.d.ts.map +1 -1
  42. package/runtime/dependencies.js +12 -0
  43. package/runtime/grants.d.ts +1 -1
  44. package/runtime/grants.d.ts.map +1 -1
  45. package/runtime/grants.js +19 -12
  46. package/runtime/hooks.d.ts +28 -11
  47. package/runtime/hooks.d.ts.map +1 -1
  48. package/runtime/hooks.js +20 -123
  49. package/runtime/lifecycle.d.ts +2 -1
  50. package/runtime/lifecycle.d.ts.map +1 -1
  51. package/runtime/lifecycle.js +67 -50
  52. package/runtime/sessions.d.ts +31 -5
  53. package/runtime/sessions.d.ts.map +1 -1
  54. package/runtime/sessions.js +342 -81
  55. package/runtime/tool-calls.d.ts +0 -1
  56. package/runtime/tool-calls.d.ts.map +1 -1
  57. package/runtime/tool-calls.js +0 -19
  58. package/session/context/turn/message/instruction-file.d.ts +9 -0
  59. package/session/context/turn/message/instruction-file.d.ts.map +1 -0
  60. package/session/context/turn/message/instruction-file.js +4 -0
  61. package/session/context/turn/message/message.d.ts +5 -4
  62. package/session/context/turn/message/message.d.ts.map +1 -1
  63. package/session/context/turn/message/message.js +7 -6
  64. package/session/context/turn/message/sender.d.ts +2 -1
  65. package/session/context/turn/message/sender.d.ts.map +1 -1
  66. package/session/context/turn/message/sender.js +4 -1
  67. package/session/context/turn/message/tool-call.d.ts +5 -0
  68. package/session/context/turn/message/tool-call.d.ts.map +1 -1
  69. package/session/context/turn/message/tool-result.d.ts +6 -1
  70. package/session/context/turn/message/tool-result.d.ts.map +1 -1
  71. package/session/context/turn/turn.d.ts +7 -0
  72. package/session/context/turn/turn.d.ts.map +1 -1
  73. package/session/conversation.d.ts +10 -3
  74. package/session/conversation.d.ts.map +1 -1
  75. package/session/conversation.js +14 -5
  76. package/session/goal/goal.d.ts +1 -1
  77. package/session/goal/goal.d.ts.map +1 -1
  78. package/session/goal/task.d.ts +2 -2
  79. package/session/goal/task.d.ts.map +1 -1
  80. package/session/session.d.ts +361 -26
  81. package/session/session.d.ts.map +1 -1
  82. package/session/session.js +40 -0
  83. package/src/jobs/permissions/decide-permission.ts +2 -1
  84. package/src/jobs/prompt.ts +3 -20
  85. package/src/jobs/slash-command.ts +2 -2
  86. package/src/jobs/status-line.ts +4 -7
  87. package/src/mod.ts +52 -147
  88. package/src/records.ts +15 -5
  89. package/src/register.ts +3 -1
  90. package/src/runtime/before.ts +90 -37
  91. package/src/runtime/claude.ts +5 -1
  92. package/src/runtime/conversations.ts +41 -25
  93. package/src/runtime/deadline.ts +38 -0
  94. package/src/runtime/dependencies.ts +12 -0
  95. package/src/runtime/grants.ts +15 -10
  96. package/src/runtime/hooks.ts +48 -116
  97. package/src/runtime/lifecycle.ts +66 -51
  98. package/src/runtime/sessions.ts +366 -90
  99. package/src/runtime/tool-calls.ts +0 -19
  100. package/src/session/context/turn/message/instruction-file.ts +13 -0
  101. package/src/session/context/turn/message/message.ts +11 -9
  102. package/src/session/context/turn/message/sender.ts +5 -2
  103. package/src/session/context/turn/message/tool-call.ts +3 -0
  104. package/src/session/context/turn/message/tool-result.ts +6 -1
  105. package/src/session/context/turn/turn.ts +4 -0
  106. package/src/session/conversation.ts +21 -8
  107. package/src/session/goal/goal.ts +1 -1
  108. package/src/session/goal/task.ts +2 -2
  109. package/src/session/session.ts +227 -24
  110. package/src/testing/claude-code.ts +23 -0
  111. package/src/testing/fake-claude.ts +5 -1
  112. package/src/testing/fake-sessions.ts +38 -24
  113. package/src/testing.ts +84 -16
  114. package/testing/claude-code.d.ts +12 -0
  115. package/testing/claude-code.d.ts.map +1 -0
  116. package/testing/claude-code.js +10 -0
  117. package/testing/fake-claude.d.ts +4 -0
  118. package/testing/fake-claude.d.ts.map +1 -1
  119. package/testing/fake-claude.js +4 -1
  120. package/testing/fake-sessions.d.ts +8 -6
  121. package/testing/fake-sessions.d.ts.map +1 -1
  122. package/testing/fake-sessions.js +35 -20
  123. package/testing.d.ts +38 -9
  124. package/testing.d.ts.map +1 -1
  125. package/testing.js +54 -5
  126. package/docs/hooks.md +0 -259
  127. package/session/context/turn/message/document.d.ts +0 -7
  128. package/session/context/turn/message/document.d.ts.map +0 -1
  129. package/session/context/turn/message/document.js +0 -1
  130. package/src/session/context/turn/message/document.ts +0 -8
@@ -5,7 +5,6 @@ export type Agent = { agentId?: string; agentType?: string }
5
5
 
6
6
  export type ToolCalls = {
7
7
  agentOf(toolUseId: string | undefined): Promise<Agent>
8
- cwdOf(toolUseId: string): string | undefined
9
8
  }
10
9
 
11
10
  export const reservedKeys: readonly string[] = ['tool', 'tool_use_id', 'consent', 'agentId']
@@ -16,12 +15,9 @@ export function toolInputOf(envelope: object): Record<string, unknown> {
16
15
 
17
16
  type AgentFields = { readonly agent_id?: string; readonly agent_type?: string }
18
17
 
19
- const keptCalls = 100
20
-
21
18
  export function toolCalls(claude: Claude, router: Router): ToolCalls {
22
19
  let mainAgentType: string | undefined
23
20
  const agentIds = new Map<string, string>()
24
- const cwds = new Map<string, string>()
25
21
  const noteAgent = (e: AgentFields) => {
26
22
  if (e.agent_id === undefined) mainAgentType = e.agent_type
27
23
  }
@@ -34,11 +30,6 @@ export function toolCalls(claude: Claude, router: Router): ToolCalls {
34
30
  return next(e)
35
31
  })
36
32
  router.add('tool.call', async (e, next) => {
37
- cwds.set(e.tool_use_id, await claude.session.cwd())
38
- for (const oldest of cwds.keys()) {
39
- if (cwds.size <= keptCalls) break
40
- cwds.delete(oldest)
41
- }
42
33
  if (e.agentId === undefined) return next(e)
43
34
  agentIds.set(e.tool_use_id, e.agentId)
44
35
  try {
@@ -47,21 +38,11 @@ export function toolCalls(claude: Claude, router: Router): ToolCalls {
47
38
  agentIds.delete(e.tool_use_id)
48
39
  }
49
40
  })
50
- for (const event of ['classic.PostToolUse', 'classic.PostToolUseFailure'] as const) {
51
- router.add(event, async (e, next) => {
52
- try {
53
- return await next(e)
54
- } finally {
55
- cwds.delete(e.tool_use_id)
56
- }
57
- })
58
- }
59
41
  return {
60
42
  async agentOf(toolUseId) {
61
43
  const agentId = toolUseId === undefined ? undefined : agentIds.get(toolUseId)
62
44
  const agentType = agentId === undefined ? mainAgentType : (await claude.agent.list()).find((agent) => agent.id === agentId)?.type
63
45
  return { ...(agentId === undefined ? {} : { agentId }), ...(agentType === undefined ? {} : { agentType }) }
64
46
  },
65
- cwdOf: (toolUseId) => cwds.get(toolUseId),
66
47
  }
67
48
  }
@@ -0,0 +1,13 @@
1
+ import type { MessageOf } from './message.js'
2
+
3
+ export const InstructionFile = 'instructionFile'
4
+
5
+ export type InstructionFile = MessageOf<typeof InstructionFile> & {
6
+ readonly path: string
7
+ readonly text: string
8
+ readonly isRule: boolean
9
+ }
10
+
11
+ export function isRulePath(path: string): boolean {
12
+ return /(^|\/)\.claude[^/]*\/rules\//.test(path)
13
+ }
@@ -1,18 +1,18 @@
1
1
  import { isObject } from '../../../../records.js'
2
2
  import { Task } from '../../../goal/task.js'
3
- import { Document } from './document.js'
3
+ import { InstructionFile, isRulePath } from './instruction-file.js'
4
4
  import { Note } from './note.js'
5
5
  import { Notice } from './notice.js'
6
6
  import { Prompt } from './prompt.js'
7
7
  import { Reply } from './reply.js'
8
8
  import { senderOf } from './sender.js'
9
9
  import { Skill } from './skill.js'
10
- import { ToolCall } from './tool-call.js'
10
+ import { ToolCall, type CallFiles } from './tool-call.js'
11
11
  import { ToolResult } from './tool-result.js'
12
12
 
13
13
  export const Message = 'message'
14
14
 
15
- export type MessageType = typeof Prompt | typeof Note | typeof Reply | typeof ToolCall | typeof ToolResult | typeof Skill | typeof Document | typeof Notice
15
+ export type MessageType = typeof Prompt | typeof Note | typeof Reply | typeof ToolCall | typeof ToolResult | typeof Skill | typeof InstructionFile | typeof Notice
16
16
 
17
17
  export type MessageOf<Type extends MessageType> = {
18
18
  readonly id: string
@@ -24,7 +24,7 @@ export type MessageOf<Type extends MessageType> = {
24
24
  readonly task?: string
25
25
  }
26
26
 
27
- export type Message = Prompt | Note | Reply | ToolCall | ToolResult | Skill | Document | Notice
27
+ export type Message = Prompt | Note | Reply | ToolCall | ToolResult | Skill | InstructionFile | Notice
28
28
 
29
29
  export type Placed = 'session' | 'context' | 'turn' | 'task'
30
30
 
@@ -44,7 +44,7 @@ export type Row = {
44
44
  readonly model?: string
45
45
  }
46
46
 
47
- export const messageTypes: readonly MessageType[] = [Prompt, Note, Reply, ToolCall, ToolResult, Skill, Document, Notice]
47
+ export const messageTypes: readonly MessageType[] = [Prompt, Note, Reply, ToolCall, ToolResult, Skill, InstructionFile, Notice]
48
48
 
49
49
  export const skillPrefix = 'Base directory for this skill:'
50
50
 
@@ -94,6 +94,8 @@ export function rowOfAppend(append: { readonly uuid: string; readonly door: stri
94
94
  }
95
95
  }
96
96
 
97
+ export const noFiles: CallFiles = { read: [], changed: [] }
98
+
97
99
  export function messagesOf(row: Row, isInTask: boolean): Unplaced[] {
98
100
  const found = unplacedOf(row, isInTask)
99
101
  return found.length === 1 ? found : found.map((message, index) => ({ ...message, id: `${row.id}:${index}` }))
@@ -103,17 +105,17 @@ function unplacedOf(row: Row, isInTask: boolean): Unplaced[] {
103
105
  const base = { id: row.id, at: row.at }
104
106
  const text = textOf(row.content).trim()
105
107
  if (row.type === 'system') return row.name === 'turn_duration' || row.name === 'compact_boundary' || text === '' ? [] : [{ ...base, type: Notice, text }]
106
- if (row.type === 'attachment') return row.name === 'nested_memory' ? [{ ...base, type: Document, path: row.path ?? '', text }] : []
108
+ if (row.type === 'attachment') return row.name === 'nested_memory' ? [{ ...base, type: InstructionFile, path: row.path ?? '', text, isRule: isRulePath(row.path ?? '') }] : []
107
109
  if (row.type === 'assistant') {
108
110
  return row.content.filter(isObject).flatMap((block): Unplaced[] => {
109
111
  if (block['type'] === 'text' && typeof block['text'] === 'string' && block['text'].trim() !== '') return [{ ...base, type: Reply, text: block['text'].trim() }]
110
112
  if (block['type'] !== 'tool_use' || typeof block['id'] !== 'string' || typeof block['name'] !== 'string') return []
111
- return [{ ...base, type: ToolCall, callId: block['id'], name: block['name'], input: isObject(block['input']) ? block['input'] : {}, cwd: row.cwd ?? '' }]
113
+ return [{ ...base, type: ToolCall, callId: block['id'], name: block['name'], input: isObject(block['input']) ? block['input'] : {}, cwd: row.cwd ?? '', files: noFiles }]
112
114
  })
113
115
  }
114
116
  if (row.isSummary) return []
115
117
  const results = row.content.filter(isToolResultBlock)
116
- if (results.length > 0) return results.map((block) => ({ ...base, type: ToolResult, callId: block['tool_use_id'] as string, text: textOf(block['content']), isError: block['is_error'] === true }))
118
+ if (results.length > 0) return results.map((block) => ({ ...base, type: ToolResult, callId: block['tool_use_id'] as string, name: '', input: {}, cwd: row.cwd ?? '', files: noFiles, output: textOf(block['content']), isError: block['is_error'] === true }))
117
119
  if (text === '') return []
118
120
  if (text.startsWith(skillPrefix)) return [{ ...base, type: Skill, name: basename(text.slice(skillPrefix.length).split('\n')[0]?.trim() ?? ''), text }]
119
121
  if (text.startsWith('<task-notification')) return [{ ...base, type: Note, text: tagOf(text, 'summary') ?? text, from: { type: Task } }]
@@ -175,7 +177,7 @@ const toolValueChars = 1024
175
177
  export function searchText(message: Unplaced): string {
176
178
  if (message.type === ToolCall) return [message.name, ...stringsIn(message.input).map((value) => value.slice(0, toolValueChars))].join('\n')
177
179
  if (message.type === ToolResult) return ''
178
- if (message.type === Document) return `${message.path}\n${message.text}`
180
+ if (message.type === InstructionFile) return `${message.path}\n${message.text}`
179
181
  return message.text
180
182
  }
181
183
 
@@ -11,7 +11,9 @@ export const Channel = 'channel'
11
11
 
12
12
  export const Sdk = 'sdk'
13
13
 
14
- export type SenderType = typeof Person | typeof Mod | 'session' | typeof Task | typeof Schedule | typeof Channel | typeof Sdk
14
+ export const Engine = 'engine'
15
+
16
+ export type SenderType = typeof Person | typeof Mod | 'session' | typeof Task | typeof Schedule | typeof Channel | typeof Sdk | typeof Engine
15
17
 
16
18
  export type Sender = {
17
19
  readonly type: SenderType
@@ -19,7 +21,7 @@ export type Sender = {
19
21
  readonly as?: typeof Person
20
22
  }
21
23
 
22
- const personOrigins: readonly string[] = ['human', 'composer', 'bridge', 'slack-ping', 'auto-continuation']
24
+ const personOrigins: readonly string[] = ['human', 'composer', 'bridge', 'slack-ping']
23
25
 
24
26
  const sessionOrigins: readonly string[] = ['peer', 'peer-send-message', 'coordinator', 'projects-relay', 'observer', 'observer-activity']
25
27
 
@@ -29,6 +31,7 @@ export function senderOf(origin: unknown, isInTask: boolean): Sender {
29
31
  if (kind === undefined) return { type: isInTask ? Task : Person }
30
32
  if (personOrigins.includes(kind)) return { type: Person }
31
33
  if (sessionOrigins.includes(kind)) return { type: 'session' }
34
+ if (kind === 'auto-continuation') return { type: Engine }
32
35
  if (kind === 'plugin' && typeof fields['name'] === 'string') return { type: Mod, name: fields['name'], ...(fields['asUser'] === true ? { as: Person } : {}) }
33
36
  if (kind === 'scheduled-trigger') return { type: Schedule }
34
37
  if (kind === 'channel' && typeof fields['server'] === 'string') return { type: Channel, name: fields['server'] }
@@ -2,9 +2,12 @@ import type { MessageOf } from './message.js'
2
2
 
3
3
  export const ToolCall = 'toolCall'
4
4
 
5
+ export type CallFiles = { readonly read: readonly string[]; readonly changed: readonly string[] }
6
+
5
7
  export type ToolCall = MessageOf<typeof ToolCall> & {
6
8
  readonly callId: string
7
9
  readonly name: string
8
10
  readonly input: Readonly<Record<string, unknown>>
9
11
  readonly cwd: string
12
+ readonly files: CallFiles
10
13
  }
@@ -1,9 +1,14 @@
1
1
  import type { MessageOf } from './message.js'
2
+ import type { CallFiles } from './tool-call.js'
2
3
 
3
4
  export const ToolResult = 'toolResult'
4
5
 
5
6
  export type ToolResult = MessageOf<typeof ToolResult> & {
6
7
  readonly callId: string
7
- readonly text: string
8
+ readonly name: string
9
+ readonly input: Readonly<Record<string, unknown>>
10
+ readonly cwd: string
11
+ readonly files: CallFiles
12
+ readonly output: unknown
8
13
  readonly isError: boolean
9
14
  }
@@ -11,5 +11,9 @@ export type Turn = RecordPart & {
11
11
  readonly context: number
12
12
  readonly openedAt: string
13
13
  readonly closedAt?: string
14
+ readonly answer?: string
15
+ readonly reason?: 'answer' | 'error'
16
+ readonly failure?: { readonly type: string; readonly text: string }
14
17
  messages(): Promise<Message[]>
18
+ abort(): Promise<void>
15
19
  }
@@ -5,7 +5,7 @@ import { Prompt } from './context/turn/message/prompt.js'
5
5
  import { ToolCall } from './context/turn/message/tool-call.js'
6
6
  import { ToolResult } from './context/turn/message/tool-result.js'
7
7
  import { TurnEnd, TurnStart, type Turn } from './context/turn/turn.js'
8
- import { GoalChange, type Goal } from './goal/goal.js'
8
+ import { GoalChange } from './goal/goal.js'
9
9
  import { Task, TaskEnd, TaskStart } from './goal/task.js'
10
10
  import type { UsageChange, Usage } from './session.js'
11
11
 
@@ -24,13 +24,15 @@ export type SessionData = {
24
24
 
25
25
  export type ContextData = Pick<Context, 'number' | 'openedAt' | 'closedAt' | 'task'>
26
26
 
27
- export type TurnData = Pick<Turn, 'prompt' | 'context' | 'openedAt' | 'closedAt'>
27
+ export type TurnData = Pick<Turn, 'prompt' | 'context' | 'openedAt' | 'closedAt' | 'answer' | 'reason' | 'failure'>
28
+
29
+ export type TurnEnding = Pick<Turn, 'answer' | 'reason' | 'failure'>
28
30
 
29
31
  export type TaskData = Pick<Task, 'id' | 'title' | 'openedBy' | 'openedAt' | 'closedAt'> & { readonly agentType: string; readonly model?: string; readonly parent?: string }
30
32
 
31
33
  export type RunningTask = TaskData & { readonly context: number; readonly contextOpenedAt?: string }
32
34
 
33
- export type GoalData = Pick<Goal, 'text'> & { readonly task?: string }
35
+ export type GoalData = { readonly text: string; readonly task?: string }
34
36
 
35
37
  export type SessionChange =
36
38
  | { readonly type: typeof Message; readonly message: Message }
@@ -50,12 +52,16 @@ export type ConversationState = {
50
52
  readonly endedCalls: Readonly<Record<string, string>>
51
53
  readonly tasks: Readonly<Record<string, RunningTask>>
52
54
  readonly taskTurns: Readonly<Record<string, TurnData>>
55
+ readonly openCalls?: Readonly<Record<string, OpenCall>>
53
56
  }
54
57
 
58
+ type OpenCall = Pick<ToolCall, 'name' | 'input' | 'cwd'>
59
+
55
60
  export type Conversation = {
56
61
  add(row: Row, task?: string): SessionChange[]
57
62
  startTask(task: TaskData): SessionChange[]
58
63
  startContext(at: string): SessionChange[]
64
+ endTurn(at: string, ending: TurnEnding, task?: string): SessionChange[]
59
65
  changeGoal(text: string): SessionChange[]
60
66
  readonly state: ConversationState
61
67
  }
@@ -76,12 +82,13 @@ export function createConversation(start: ConversationState): Conversation {
76
82
  const endedCalls = new Map(Object.entries(start.endedCalls))
77
83
  const tasks = new Map(Object.entries(start.tasks))
78
84
  const taskTurns = new Map(Object.entries(start.taskTurns))
85
+ const openCalls = new Map(Object.entries(start.openCalls ?? {}))
79
86
 
80
- const endTaskTurn = (task: string, at: string): SessionChange[] => {
87
+ const endTaskTurn = (task: string, at: string, ending: TurnEnding = {}): SessionChange[] => {
81
88
  const open = taskTurns.get(task)
82
89
  if (open === undefined) return []
83
90
  taskTurns.delete(task)
84
- return open.closedAt === undefined ? [{ type: TurnEnd, turn: { ...open, closedAt: at } }] : []
91
+ return open.closedAt === undefined ? [{ type: TurnEnd, turn: { ...open, ...ending, closedAt: at } }] : []
85
92
  }
86
93
 
87
94
  const startContext = (at: string): SessionChange[] => {
@@ -103,9 +110,9 @@ export function createConversation(start: ConversationState): Conversation {
103
110
  ]
104
111
  }
105
112
 
106
- const endTurn = (at: string): SessionChange[] => {
113
+ const endTurn = (at: string, ending: TurnEnding = {}): SessionChange[] => {
107
114
  if (turn === undefined) return []
108
- const ended = { ...turn, closedAt: at }
115
+ const ended = { ...turn, ...ending, closedAt: at }
109
116
  turn = undefined
110
117
  return [{ type: TurnEnd, turn: ended }]
111
118
  }
@@ -148,8 +155,12 @@ export function createConversation(start: ConversationState): Conversation {
148
155
  if (task === undefined && goal === undefined) changes.push(...changeGoal(unplaced.text))
149
156
  }
150
157
  const open = task === undefined ? turn : taskTurns.get(task)
158
+ const call = unplaced.type === ToolResult ? openCalls.get(unplaced.callId) : undefined
159
+ if (unplaced.type === ToolCall) openCalls.set(unplaced.callId, { name: unplaced.name, input: unplaced.input, cwd: unplaced.cwd })
160
+ if (unplaced.type === ToolResult) openCalls.delete(unplaced.callId)
151
161
  const placed = {
152
162
  ...unplaced,
163
+ ...call,
153
164
  session: start.session,
154
165
  context: placedIn,
155
166
  ...(open === undefined ? {} : { turn: open.prompt.id }),
@@ -157,7 +168,7 @@ export function createConversation(start: ConversationState): Conversation {
157
168
  } as Message
158
169
  changes.push({ type: Message, message: placed })
159
170
  if (placed.type === ToolCall && agentTools.includes(placed.name)) agentCalls.add(placed.callId)
160
- if (placed.type === ToolResult && agentCalls.has(placed.callId) && !placed.text.startsWith(asyncLaunch)) changes.push(...endTask(placed.callId, row.at))
171
+ if (placed.type === ToolResult && agentCalls.has(placed.callId) && !String(placed.output).startsWith(asyncLaunch)) changes.push(...endTask(placed.callId, row.at))
161
172
  if (placed.type === Note && placed.from.type === Task) {
162
173
  const call = tagOf(textOf(row.content), 'tool-use-id')
163
174
  if (call !== undefined && agentCalls.has(call)) changes.push(...endTask(call, row.at, tagOf(textOf(row.content), 'task-id')))
@@ -176,6 +187,7 @@ export function createConversation(start: ConversationState): Conversation {
176
187
  return changes
177
188
  },
178
189
  startContext,
190
+ endTurn: (at, ending, task) => (task === undefined ? endTurn(at, ending) : endTaskTurn(task, at, ending)),
179
191
  changeGoal,
180
192
  get state() {
181
193
  return {
@@ -188,6 +200,7 @@ export function createConversation(start: ConversationState): Conversation {
188
200
  endedCalls: Object.fromEntries(endedCalls),
189
201
  tasks: Object.fromEntries(tasks),
190
202
  taskTurns: Object.fromEntries(taskTurns),
203
+ openCalls: Object.fromEntries(openCalls),
191
204
  }
192
205
  },
193
206
  }
@@ -4,7 +4,7 @@ import type { Task } from './task.js'
4
4
  export const GoalChange = 'goalChange'
5
5
 
6
6
  export type Goal = RecordPart & {
7
- readonly text: string
7
+ text(): Promise<string>
8
8
  tasks(): Promise<Task[]>
9
9
  update(change: { readonly text: string }): Promise<void>
10
10
  }
@@ -1,5 +1,5 @@
1
1
  import type { Message } from '../context/turn/message/message.js'
2
- import type { RecordPart, Session } from '../session.js'
2
+ import type { RecordPart, TaskOwner } from '../session.js'
3
3
 
4
4
  export const Task = 'task'
5
5
 
@@ -10,7 +10,7 @@ export const TaskEnd = 'taskEnd'
10
10
  export type Task = RecordPart & {
11
11
  readonly id: string
12
12
  readonly title: string
13
- readonly owner: Session
13
+ readonly owner: TaskOwner
14
14
  readonly openedBy: string
15
15
  readonly openedAt: string
16
16
  readonly closedAt?: string