@yolk-sdk/agent 0.1.0-canary.21 → 0.1.0-canary.22

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 (33) hide show
  1. package/README.md +18 -5
  2. package/dist/loop/run.d.mts.map +1 -1
  3. package/dist/loop/run.mjs +38 -19
  4. package/dist/loop/run.mjs.map +1 -1
  5. package/dist/protocol/index.d.mts +2 -2
  6. package/dist/protocol/index.d.mts.map +1 -1
  7. package/dist/protocol/index.mjs +2 -2
  8. package/dist/protocol/message.d.mts +20 -1
  9. package/dist/protocol/message.d.mts.map +1 -1
  10. package/dist/protocol/message.mjs +59 -1
  11. package/dist/protocol/message.mjs.map +1 -1
  12. package/dist/providers/anthropic/claude-provider.d.mts.map +1 -1
  13. package/dist/providers/anthropic/claude-provider.mjs +2 -0
  14. package/dist/providers/anthropic/claude-provider.mjs.map +1 -1
  15. package/dist/providers/openai/codex-provider.d.mts.map +1 -1
  16. package/dist/providers/openai/codex-provider.mjs +2 -0
  17. package/dist/providers/openai/codex-provider.mjs.map +1 -1
  18. package/dist/providers/openai/provider.d.mts.map +1 -1
  19. package/dist/providers/openai/provider.mjs +2 -0
  20. package/dist/providers/openai/provider.mjs.map +1 -1
  21. package/dist/providers/transcript.d.mts +9 -0
  22. package/dist/providers/transcript.d.mts.map +1 -0
  23. package/dist/providers/transcript.mjs +17 -0
  24. package/dist/providers/transcript.mjs.map +1 -0
  25. package/package.json +1 -1
  26. package/src/loop/run.ts +56 -17
  27. package/src/protocol/index.ts +9 -1
  28. package/src/protocol/message.ts +127 -0
  29. package/src/providers/anthropic/claude-provider.ts +2 -0
  30. package/src/providers/openai/codex-provider.ts +2 -0
  31. package/src/providers/openai/provider.ts +2 -0
  32. package/src/providers/transcript.ts +24 -0
  33. package/src/tools/README.md +3 -2
@@ -87,6 +87,25 @@ export class ToolResultMessage extends Schema.TaggedClass<ToolResultMessage>()('
87
87
  export const AgentMessage = Schema.Union([UserMessage, AssistantAgentMessage, ToolResultMessage])
88
88
  export type AgentMessage = typeof AgentMessage.Type
89
89
 
90
+ export type DanglingHostToolCall = {
91
+ readonly call: ToolCall
92
+ readonly assistantMessageIndex: number
93
+ readonly beforeMessageIndex?: number
94
+ }
95
+
96
+ export type TranscriptInvariantValidation =
97
+ | { readonly _tag: 'Valid' }
98
+ | {
99
+ readonly _tag: 'DanglingHostToolCalls'
100
+ readonly calls: ReadonlyArray<DanglingHostToolCall>
101
+ readonly message: string
102
+ }
103
+
104
+ export type RepairDanglingHostToolCallsOptions = {
105
+ readonly content?: (call: ToolCall) => Content
106
+ readonly structuredContent?: (call: ToolCall) => unknown
107
+ }
108
+
90
109
  export const assistantContent = (message: AssistantAgentMessage): Content => {
91
110
  const parts = message.parts.flatMap(part => (part._tag === 'Text' ? [part.content] : []))
92
111
  const first = parts[0]
@@ -108,6 +127,114 @@ export const assistantReasoningText = (message: AssistantAgentMessage) =>
108
127
  export const assistantHostToolCalls = (message: AssistantAgentMessage) =>
109
128
  message.parts.flatMap(part => (part._tag === 'HostToolCall' ? [part.call] : []))
110
129
 
130
+ type PendingHostToolCall = {
131
+ readonly call: ToolCall
132
+ readonly assistantMessageIndex: number
133
+ }
134
+
135
+ const pendingHostToolCalls = (message: AgentMessage, messageIndex: number) =>
136
+ message._tag === 'Assistant'
137
+ ? assistantHostToolCalls(message).map(call => ({ call, assistantMessageIndex: messageIndex }))
138
+ : []
139
+
140
+ const danglingHostToolCall = (
141
+ pending: PendingHostToolCall,
142
+ beforeMessageIndex: number | undefined
143
+ ): DanglingHostToolCall => ({
144
+ call: pending.call,
145
+ assistantMessageIndex: pending.assistantMessageIndex,
146
+ ...(beforeMessageIndex === undefined ? {} : { beforeMessageIndex })
147
+ })
148
+
149
+ const danglingHostToolCallSummary = (calls: ReadonlyArray<DanglingHostToolCall>) =>
150
+ calls.map(({ call }) => `${call.name} (${call.id})`).join(', ')
151
+
152
+ const danglingHostToolResultContent = (call: ToolCall) =>
153
+ `Tool ${call.name} did not return a result before the transcript continued.`
154
+
155
+ const danglingHostToolResultMessage = (
156
+ call: ToolCall,
157
+ options: RepairDanglingHostToolCallsOptions | undefined
158
+ ) => {
159
+ const structuredContent = options?.structuredContent?.(call)
160
+
161
+ return ToolResultMessage.make({
162
+ toolCallId: call.id,
163
+ content: options?.content?.(call) ?? danglingHostToolResultContent(call),
164
+ isError: true,
165
+ ...(structuredContent === undefined ? {} : { structuredContent })
166
+ })
167
+ }
168
+
169
+ export const danglingHostToolCalls = (
170
+ messages: ReadonlyArray<AgentMessage>
171
+ ): ReadonlyArray<DanglingHostToolCall> => {
172
+ const dangling: Array<DanglingHostToolCall> = []
173
+ let pending: ReadonlyArray<PendingHostToolCall> = []
174
+
175
+ for (const [messageIndex, message] of messages.entries()) {
176
+ if (message._tag !== 'ToolResult' && pending.length > 0) {
177
+ dangling.push(...pending.map(call => danglingHostToolCall(call, messageIndex)))
178
+ pending = []
179
+ }
180
+
181
+ pending = [...pending, ...pendingHostToolCalls(message, messageIndex)]
182
+
183
+ if (message._tag === 'ToolResult') {
184
+ pending = pending.filter(call => call.call.id !== message.toolCallId)
185
+ }
186
+ }
187
+
188
+ dangling.push(...pending.map(call => danglingHostToolCall(call, undefined)))
189
+
190
+ return dangling
191
+ }
192
+
193
+ export const validateNoDanglingHostToolCalls = (
194
+ messages: ReadonlyArray<AgentMessage>
195
+ ): TranscriptInvariantValidation => {
196
+ const dangling = danglingHostToolCalls(messages)
197
+
198
+ if (dangling.length === 0) {
199
+ return { _tag: 'Valid' }
200
+ }
201
+
202
+ return {
203
+ _tag: 'DanglingHostToolCalls',
204
+ calls: dangling,
205
+ message: `Transcript has host tool calls without tool results: ${danglingHostToolCallSummary(dangling)}`
206
+ }
207
+ }
208
+
209
+ export const repairDanglingHostToolCalls = (
210
+ messages: ReadonlyArray<AgentMessage>,
211
+ options?: RepairDanglingHostToolCallsOptions
212
+ ): ReadonlyArray<AgentMessage> => {
213
+ const repaired: Array<AgentMessage> = []
214
+ let pending: ReadonlyArray<ToolCall> = []
215
+
216
+ for (const message of messages) {
217
+ if (message._tag !== 'ToolResult' && pending.length > 0) {
218
+ repaired.push(...pending.map(call => danglingHostToolResultMessage(call, options)))
219
+ pending = []
220
+ }
221
+
222
+ repaired.push(message)
223
+
224
+ if (message._tag === 'Assistant') {
225
+ pending = [...pending, ...assistantHostToolCalls(message)]
226
+ }
227
+
228
+ if (message._tag === 'ToolResult') {
229
+ pending = pending.filter(call => call.id !== message.toolCallId)
230
+ }
231
+ }
232
+
233
+ repaired.push(...pending.map(call => danglingHostToolResultMessage(call, options)))
234
+
235
+ return repaired
236
+ }
237
+
111
238
  const formatCreatedAtMs = (createdAtMs: number) =>
112
239
  Number.isFinite(createdAtMs) ? new Date(createdAtMs).toISOString() : String(createdAtMs)
113
240
 
@@ -48,6 +48,7 @@ import {
48
48
  providerFailureCause,
49
49
  providerFailureRetryable
50
50
  } from '../provider-error.ts'
51
+ import { validateProviderTranscript } from '../transcript.ts'
51
52
 
52
53
  export type AnthropicClaudeProviderConfig = {
53
54
  readonly token: OAuthAccessToken
@@ -802,6 +803,7 @@ export const toAnthropicClaudeRequestBody = (
802
803
  config?: { readonly maxTokens?: number; readonly stream?: boolean }
803
804
  ): Effect.Effect<AnthropicRequestBody, LLMError> =>
804
805
  Effect.gen(function* () {
806
+ yield* validateProviderTranscript(request.messages)
805
807
  const rawMessages = yield* Effect.forEach(request.messages, toAnthropicMessage)
806
808
  const billingSystemBlock = yield* makeAnthropicClaudeBillingSystemBlock(rawMessages)
807
809
  const messages = prependSystemPromptToFirstUserMessage(rawMessages, request.systemPrompt)
@@ -43,6 +43,7 @@ import {
43
43
  providerFailureCause,
44
44
  providerFailureRetryable
45
45
  } from '../provider-error.ts'
46
+ import { validateProviderTranscript } from '../transcript.ts'
46
47
 
47
48
  export type OpenAiCodexReasoningSummary = 'auto' | 'concise' | 'detailed'
48
49
 
@@ -377,6 +378,7 @@ export const toOpenAiCodexRequestBody = (
377
378
  }
378
379
  ): Effect.Effect<OpenAiCodexRequestBody, LLMError> =>
379
380
  Effect.gen(function* () {
381
+ yield* validateProviderTranscript(request.messages)
380
382
  const input = Arr.flatten(yield* Effect.forEach(request.messages, messageToCodexInput))
381
383
 
382
384
  const body: Omit<OpenAiCodexRequestBody, 'tools'> = {
@@ -40,6 +40,7 @@ import {
40
40
  providerFailureCause,
41
41
  providerFailureRetryable
42
42
  } from '../provider-error.ts'
43
+ import { validateProviderTranscript } from '../transcript.ts'
43
44
 
44
45
  export type OpenAiProviderConfig = {
45
46
  readonly chatCompletionsUrl?: string
@@ -340,6 +341,7 @@ export const toOpenAiRequestBody = (
340
341
  config?: { readonly maxCompletionTokens?: number }
341
342
  ): Effect.Effect<OpenAiRequestBody, LLMError> =>
342
343
  Effect.gen(function* () {
344
+ yield* validateProviderTranscript(request.messages)
343
345
  const systemMessage: OpenAiMessage = { role: 'system', content: request.systemPrompt }
344
346
  const requestMessages = yield* Effect.forEach(request.messages, toOpenAiMessage)
345
347
  const messages = [systemMessage, ...requestMessages]
@@ -0,0 +1,24 @@
1
+ import { Effect } from 'effect'
2
+ import {
3
+ validateNoDanglingHostToolCalls,
4
+ type AgentMessage
5
+ } from '@yolk-sdk/agent/protocol'
6
+ import { LLMError } from '@yolk-sdk/agent/loop'
7
+
8
+ export const validateProviderTranscript = (
9
+ messages: ReadonlyArray<AgentMessage>
10
+ ): Effect.Effect<void, LLMError> => {
11
+ const validation = validateNoDanglingHostToolCalls(messages)
12
+
13
+ if (validation._tag === 'Valid') {
14
+ return Effect.void
15
+ }
16
+
17
+ return Effect.fail(
18
+ new LLMError({
19
+ cause: 'validation_error',
20
+ message: validation.message,
21
+ retryable: false
22
+ })
23
+ )
24
+ }
@@ -41,8 +41,9 @@ return Effect.fail(modelVisibleToolError({
41
41
  }))
42
42
  ```
43
43
 
44
- Use `ToolError` for fatal runtime/tool infrastructure failures such as broken wiring, auth/config,
45
- storage outages, or implementation bugs.
44
+ Thrown `ToolError`s become model-visible failed tool results plus `ToolExecutionError` events,
45
+ so keep messages safe and non-secret. Reserve stream failure for provider/runtime defects,
46
+ aborts, and implementation bugs outside typed tool execution.
46
47
 
47
48
  ## Task subagents
48
49