@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.
- package/README.md +18 -5
- package/dist/loop/run.d.mts.map +1 -1
- package/dist/loop/run.mjs +38 -19
- package/dist/loop/run.mjs.map +1 -1
- package/dist/protocol/index.d.mts +2 -2
- package/dist/protocol/index.d.mts.map +1 -1
- package/dist/protocol/index.mjs +2 -2
- package/dist/protocol/message.d.mts +20 -1
- package/dist/protocol/message.d.mts.map +1 -1
- package/dist/protocol/message.mjs +59 -1
- package/dist/protocol/message.mjs.map +1 -1
- package/dist/providers/anthropic/claude-provider.d.mts.map +1 -1
- package/dist/providers/anthropic/claude-provider.mjs +2 -0
- package/dist/providers/anthropic/claude-provider.mjs.map +1 -1
- package/dist/providers/openai/codex-provider.d.mts.map +1 -1
- package/dist/providers/openai/codex-provider.mjs +2 -0
- package/dist/providers/openai/codex-provider.mjs.map +1 -1
- package/dist/providers/openai/provider.d.mts.map +1 -1
- package/dist/providers/openai/provider.mjs +2 -0
- package/dist/providers/openai/provider.mjs.map +1 -1
- package/dist/providers/transcript.d.mts +9 -0
- package/dist/providers/transcript.d.mts.map +1 -0
- package/dist/providers/transcript.mjs +17 -0
- package/dist/providers/transcript.mjs.map +1 -0
- package/package.json +1 -1
- package/src/loop/run.ts +56 -17
- package/src/protocol/index.ts +9 -1
- package/src/protocol/message.ts +127 -0
- package/src/providers/anthropic/claude-provider.ts +2 -0
- package/src/providers/openai/codex-provider.ts +2 -0
- package/src/providers/openai/provider.ts +2 -0
- package/src/providers/transcript.ts +24 -0
- package/src/tools/README.md +3 -2
package/src/protocol/message.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/tools/README.md
CHANGED
|
@@ -41,8 +41,9 @@ return Effect.fail(modelVisibleToolError({
|
|
|
41
41
|
}))
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|