@ottocode/server 0.1.227 → 0.1.230
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 +3 -3
- package/src/openapi/paths/ask.ts +11 -0
- package/src/openapi/paths/config.ts +15 -0
- package/src/openapi/paths/messages.ts +6 -0
- package/src/openapi/schemas.ts +5 -0
- package/src/routes/ask.ts +8 -0
- package/src/routes/config/defaults.ts +9 -1
- package/src/routes/config/main.ts +1 -0
- package/src/routes/session-messages.ts +6 -1
- package/src/routes/sessions.ts +4 -1
- package/src/runtime/agent/oauth-codex-continuation.ts +6 -1
- package/src/runtime/agent/runner-setup.ts +43 -34
- package/src/runtime/agent/runner.ts +182 -11
- package/src/runtime/ask/service.ts +16 -0
- package/src/runtime/debug/turn-dump.ts +330 -0
- package/src/runtime/message/history-builder.ts +99 -91
- package/src/runtime/message/service.ts +8 -1
- package/src/runtime/prompt/builder.ts +8 -6
- package/src/runtime/provider/reasoning.ts +291 -0
- package/src/runtime/session/queue.ts +2 -0
- package/src/tools/adapter.ts +84 -7
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { getLocalDataDir } from '@ottocode/sdk';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import { isDebugEnabled } from './state.ts';
|
|
5
|
+
|
|
6
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
7
|
+
|
|
8
|
+
function isDumpEnabled(): boolean {
|
|
9
|
+
const explicit = process.env.OTTO_DEBUG_DUMP;
|
|
10
|
+
if (explicit) return TRUTHY.has(explicit.trim().toLowerCase());
|
|
11
|
+
return isDebugEnabled();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TurnDumpData {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
messageId: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
model: string;
|
|
20
|
+
agent: string;
|
|
21
|
+
continuationCount?: number;
|
|
22
|
+
system: {
|
|
23
|
+
prompt: string;
|
|
24
|
+
components: string[];
|
|
25
|
+
length: number;
|
|
26
|
+
};
|
|
27
|
+
additionalSystemMessages: Array<{ role: string; content: string }>;
|
|
28
|
+
history: Array<{
|
|
29
|
+
role: string;
|
|
30
|
+
content: unknown;
|
|
31
|
+
_contentLength?: number;
|
|
32
|
+
}>;
|
|
33
|
+
finalMessages: Array<{
|
|
34
|
+
role: string;
|
|
35
|
+
content: unknown;
|
|
36
|
+
_contentLength?: number;
|
|
37
|
+
}>;
|
|
38
|
+
tools: {
|
|
39
|
+
names: string[];
|
|
40
|
+
count: number;
|
|
41
|
+
};
|
|
42
|
+
modelConfig: {
|
|
43
|
+
maxOutputTokens: number | undefined;
|
|
44
|
+
effectiveMaxOutputTokens: number | undefined;
|
|
45
|
+
providerOptions: Record<string, unknown>;
|
|
46
|
+
isOpenAIOAuth: boolean;
|
|
47
|
+
needsSpoof: boolean;
|
|
48
|
+
};
|
|
49
|
+
stream: {
|
|
50
|
+
toolCalls: Array<{
|
|
51
|
+
stepIndex: number;
|
|
52
|
+
name: string;
|
|
53
|
+
callId: string;
|
|
54
|
+
args: unknown;
|
|
55
|
+
timestamp: string;
|
|
56
|
+
}>;
|
|
57
|
+
toolResults: Array<{
|
|
58
|
+
stepIndex: number;
|
|
59
|
+
name: string;
|
|
60
|
+
callId: string;
|
|
61
|
+
result: unknown;
|
|
62
|
+
_resultLength?: number;
|
|
63
|
+
timestamp: string;
|
|
64
|
+
}>;
|
|
65
|
+
textDeltas: Array<{
|
|
66
|
+
stepIndex: number;
|
|
67
|
+
textSnapshot: string;
|
|
68
|
+
length: number;
|
|
69
|
+
timestamp: string;
|
|
70
|
+
}>;
|
|
71
|
+
steps: Array<{
|
|
72
|
+
stepIndex: number;
|
|
73
|
+
finishReason: string | undefined;
|
|
74
|
+
usage?: {
|
|
75
|
+
inputTokens?: number;
|
|
76
|
+
outputTokens?: number;
|
|
77
|
+
};
|
|
78
|
+
timestamp: string;
|
|
79
|
+
}>;
|
|
80
|
+
finishReason?: string;
|
|
81
|
+
rawFinishReason?: string;
|
|
82
|
+
finishObserved: boolean;
|
|
83
|
+
aborted: boolean;
|
|
84
|
+
};
|
|
85
|
+
error?: {
|
|
86
|
+
message: string;
|
|
87
|
+
name?: string;
|
|
88
|
+
stack?: string;
|
|
89
|
+
};
|
|
90
|
+
duration?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class TurnDumpCollector {
|
|
94
|
+
private data: TurnDumpData;
|
|
95
|
+
private startTime: number;
|
|
96
|
+
private lastTextSnapshot: string = '';
|
|
97
|
+
private textSnapshotInterval = 2000;
|
|
98
|
+
private lastTextSnapshotTime = 0;
|
|
99
|
+
|
|
100
|
+
constructor(opts: {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
messageId: string;
|
|
103
|
+
provider: string;
|
|
104
|
+
model: string;
|
|
105
|
+
agent: string;
|
|
106
|
+
continuationCount?: number;
|
|
107
|
+
}) {
|
|
108
|
+
this.startTime = Date.now();
|
|
109
|
+
this.data = {
|
|
110
|
+
sessionId: opts.sessionId,
|
|
111
|
+
messageId: opts.messageId,
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
provider: opts.provider,
|
|
114
|
+
model: opts.model,
|
|
115
|
+
agent: opts.agent,
|
|
116
|
+
continuationCount: opts.continuationCount,
|
|
117
|
+
system: { prompt: '', components: [], length: 0 },
|
|
118
|
+
additionalSystemMessages: [],
|
|
119
|
+
history: [],
|
|
120
|
+
finalMessages: [],
|
|
121
|
+
tools: { names: [], count: 0 },
|
|
122
|
+
modelConfig: {
|
|
123
|
+
maxOutputTokens: undefined,
|
|
124
|
+
effectiveMaxOutputTokens: undefined,
|
|
125
|
+
providerOptions: {},
|
|
126
|
+
isOpenAIOAuth: false,
|
|
127
|
+
needsSpoof: false,
|
|
128
|
+
},
|
|
129
|
+
stream: {
|
|
130
|
+
toolCalls: [],
|
|
131
|
+
toolResults: [],
|
|
132
|
+
textDeltas: [],
|
|
133
|
+
steps: [],
|
|
134
|
+
finishObserved: false,
|
|
135
|
+
aborted: false,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setSystemPrompt(prompt: string, components: string[]) {
|
|
141
|
+
this.data.system = {
|
|
142
|
+
prompt,
|
|
143
|
+
components,
|
|
144
|
+
length: prompt.length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setAdditionalSystemMessages(msgs: Array<{ role: string; content: string }>) {
|
|
149
|
+
this.data.additionalSystemMessages = msgs;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setHistory(history: Array<{ role: string; content: unknown }>) {
|
|
153
|
+
this.data.history = history.map((m) => {
|
|
154
|
+
const contentStr =
|
|
155
|
+
typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
156
|
+
return {
|
|
157
|
+
role: m.role,
|
|
158
|
+
content: m.content,
|
|
159
|
+
_contentLength: contentStr.length,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setFinalMessages(
|
|
165
|
+
msgs: Array<{ role: string; content: string | Array<unknown> }>,
|
|
166
|
+
) {
|
|
167
|
+
this.data.finalMessages = msgs.map((m) => {
|
|
168
|
+
const contentStr =
|
|
169
|
+
typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
170
|
+
return {
|
|
171
|
+
role: m.role,
|
|
172
|
+
content: m.content,
|
|
173
|
+
_contentLength: contentStr.length,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setTools(toolset: Record<string, unknown>) {
|
|
179
|
+
const names = Object.keys(toolset);
|
|
180
|
+
this.data.tools = { names, count: names.length };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setModelConfig(config: {
|
|
184
|
+
maxOutputTokens: number | undefined;
|
|
185
|
+
effectiveMaxOutputTokens: number | undefined;
|
|
186
|
+
providerOptions: Record<string, unknown>;
|
|
187
|
+
isOpenAIOAuth: boolean;
|
|
188
|
+
needsSpoof: boolean;
|
|
189
|
+
}) {
|
|
190
|
+
this.data.modelConfig = config;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
recordToolCall(
|
|
194
|
+
stepIndex: number,
|
|
195
|
+
name: string,
|
|
196
|
+
callId: string,
|
|
197
|
+
args: unknown,
|
|
198
|
+
) {
|
|
199
|
+
this.data.stream.toolCalls.push({
|
|
200
|
+
stepIndex,
|
|
201
|
+
name,
|
|
202
|
+
callId,
|
|
203
|
+
args,
|
|
204
|
+
timestamp: new Date().toISOString(),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
recordToolResult(
|
|
209
|
+
stepIndex: number,
|
|
210
|
+
name: string,
|
|
211
|
+
callId: string,
|
|
212
|
+
result: unknown,
|
|
213
|
+
) {
|
|
214
|
+
const resultStr =
|
|
215
|
+
typeof result === 'string' ? result : JSON.stringify(result);
|
|
216
|
+
const truncated =
|
|
217
|
+
resultStr.length > 50_000
|
|
218
|
+
? `${resultStr.slice(0, 50_000)}...[TRUNCATED]`
|
|
219
|
+
: result;
|
|
220
|
+
this.data.stream.toolResults.push({
|
|
221
|
+
stepIndex,
|
|
222
|
+
name,
|
|
223
|
+
callId,
|
|
224
|
+
result: truncated,
|
|
225
|
+
_resultLength: resultStr.length,
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
recordTextDelta(
|
|
231
|
+
stepIndex: number,
|
|
232
|
+
accumulated: string,
|
|
233
|
+
opts?: { force?: boolean },
|
|
234
|
+
) {
|
|
235
|
+
const force = opts?.force === true;
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
if (
|
|
238
|
+
!force &&
|
|
239
|
+
now - this.lastTextSnapshotTime < this.textSnapshotInterval &&
|
|
240
|
+
this.lastTextSnapshot.length > 0
|
|
241
|
+
) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (force && accumulated.length === 0 && this.lastTextSnapshot.length > 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (force && accumulated === this.lastTextSnapshot) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.lastTextSnapshotTime = now;
|
|
251
|
+
this.lastTextSnapshot = accumulated;
|
|
252
|
+
this.data.stream.textDeltas.push({
|
|
253
|
+
stepIndex,
|
|
254
|
+
textSnapshot:
|
|
255
|
+
accumulated.length > 5000
|
|
256
|
+
? `${accumulated.slice(0, 5000)}...[TRUNCATED at 5000 chars, total: ${accumulated.length}]`
|
|
257
|
+
: accumulated,
|
|
258
|
+
length: accumulated.length,
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
recordStepFinish(
|
|
264
|
+
stepIndex: number,
|
|
265
|
+
finishReason: string | undefined,
|
|
266
|
+
usage?: { inputTokens?: number; outputTokens?: number },
|
|
267
|
+
) {
|
|
268
|
+
this.data.stream.steps.push({
|
|
269
|
+
stepIndex,
|
|
270
|
+
finishReason,
|
|
271
|
+
usage,
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
recordStreamEnd(opts: {
|
|
277
|
+
finishReason?: string;
|
|
278
|
+
rawFinishReason?: string;
|
|
279
|
+
finishObserved: boolean;
|
|
280
|
+
aborted: boolean;
|
|
281
|
+
}) {
|
|
282
|
+
this.data.stream.finishReason = opts.finishReason;
|
|
283
|
+
this.data.stream.rawFinishReason = opts.rawFinishReason;
|
|
284
|
+
this.data.stream.finishObserved = opts.finishObserved;
|
|
285
|
+
this.data.stream.aborted = opts.aborted;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
recordError(err: unknown) {
|
|
289
|
+
this.data.error = {
|
|
290
|
+
message: err instanceof Error ? err.message : String(err),
|
|
291
|
+
name: err instanceof Error ? err.name : undefined,
|
|
292
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async flush(projectRoot: string) {
|
|
297
|
+
this.data.duration = Date.now() - this.startTime;
|
|
298
|
+
|
|
299
|
+
const dumpDir = join(getLocalDataDir(projectRoot), 'debug-dumps');
|
|
300
|
+
await mkdir(dumpDir, { recursive: true });
|
|
301
|
+
|
|
302
|
+
const ts = new Date()
|
|
303
|
+
.toISOString()
|
|
304
|
+
.replace(/[:.]/g, '-')
|
|
305
|
+
.replace('T', '_')
|
|
306
|
+
.replace('Z', '');
|
|
307
|
+
const sessionShort = this.data.sessionId.slice(0, 8);
|
|
308
|
+
const filename = `turn_${ts}_${sessionShort}.json`;
|
|
309
|
+
const filepath = join(dumpDir, filename);
|
|
310
|
+
|
|
311
|
+
await Bun.write(filepath, JSON.stringify(this.data, null, 2));
|
|
312
|
+
return filepath;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function shouldDumpTurn(): boolean {
|
|
317
|
+
return isDumpEnabled();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function createTurnDumpCollector(opts: {
|
|
321
|
+
sessionId: string;
|
|
322
|
+
messageId: string;
|
|
323
|
+
provider: string;
|
|
324
|
+
model: string;
|
|
325
|
+
agent: string;
|
|
326
|
+
continuationCount?: number;
|
|
327
|
+
}): TurnDumpCollector | null {
|
|
328
|
+
if (!shouldDumpTurn()) return null;
|
|
329
|
+
return new TurnDumpCollector(opts);
|
|
330
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
convertToModelMessages,
|
|
3
|
+
type FilePart,
|
|
4
|
+
type ModelMessage,
|
|
5
|
+
type TextPart,
|
|
6
|
+
type UIMessage,
|
|
7
|
+
} from 'ai';
|
|
2
8
|
import type { getDb } from '@ottocode/database';
|
|
3
9
|
import { messages, messageParts } from '@ottocode/database/schema';
|
|
4
10
|
import { eq, asc } from 'drizzle-orm';
|
|
@@ -20,7 +26,7 @@ export async function buildHistoryMessages(
|
|
|
20
26
|
.where(eq(messages.sessionId, sessionId))
|
|
21
27
|
.orderBy(asc(messages.createdAt));
|
|
22
28
|
|
|
23
|
-
const
|
|
29
|
+
const history: ModelMessage[] = [];
|
|
24
30
|
const toolHistory = new ToolHistoryTracker();
|
|
25
31
|
|
|
26
32
|
for (const m of rows) {
|
|
@@ -49,13 +55,13 @@ export async function buildHistoryMessages(
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
if (m.role === 'user') {
|
|
52
|
-
const
|
|
58
|
+
const userParts: Array<TextPart | FilePart> = [];
|
|
53
59
|
for (const p of parts) {
|
|
54
60
|
if (p.type === 'text') {
|
|
55
61
|
try {
|
|
56
62
|
const obj = JSON.parse(p.content ?? '{}');
|
|
57
63
|
const t = String(obj.text ?? '');
|
|
58
|
-
if (t)
|
|
64
|
+
if (t) userParts.push({ type: 'text', text: t });
|
|
59
65
|
} catch {}
|
|
60
66
|
} else if (p.type === 'image') {
|
|
61
67
|
try {
|
|
@@ -64,11 +70,11 @@ export async function buildHistoryMessages(
|
|
|
64
70
|
mediaType?: string;
|
|
65
71
|
};
|
|
66
72
|
if (obj.data && obj.mediaType) {
|
|
67
|
-
|
|
73
|
+
userParts.push({
|
|
68
74
|
type: 'file',
|
|
75
|
+
data: obj.data,
|
|
69
76
|
mediaType: obj.mediaType,
|
|
70
|
-
|
|
71
|
-
} as never);
|
|
77
|
+
});
|
|
72
78
|
}
|
|
73
79
|
} catch {}
|
|
74
80
|
} else if (p.type === 'file') {
|
|
@@ -81,41 +87,72 @@ export async function buildHistoryMessages(
|
|
|
81
87
|
textContent?: string;
|
|
82
88
|
};
|
|
83
89
|
if (obj.type === 'text' && obj.textContent) {
|
|
84
|
-
|
|
90
|
+
userParts.push({
|
|
85
91
|
type: 'text',
|
|
86
92
|
text: `<file name="${obj.name || 'file'}">\n${obj.textContent}\n</file>`,
|
|
87
93
|
});
|
|
88
94
|
} else if (obj.type === 'pdf' && obj.data && obj.mediaType) {
|
|
89
|
-
|
|
95
|
+
userParts.push({
|
|
90
96
|
type: 'file',
|
|
97
|
+
data: obj.data,
|
|
98
|
+
filename: obj.name,
|
|
91
99
|
mediaType: obj.mediaType,
|
|
92
|
-
|
|
93
|
-
} as never);
|
|
100
|
+
});
|
|
94
101
|
} else if (obj.type === 'image' && obj.data && obj.mediaType) {
|
|
95
|
-
|
|
102
|
+
userParts.push({
|
|
96
103
|
type: 'file',
|
|
104
|
+
data: obj.data,
|
|
105
|
+
filename: obj.name,
|
|
97
106
|
mediaType: obj.mediaType,
|
|
98
|
-
|
|
99
|
-
} as never);
|
|
107
|
+
});
|
|
100
108
|
}
|
|
101
109
|
} catch {}
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
|
-
if (
|
|
105
|
-
|
|
112
|
+
if (userParts.length) {
|
|
113
|
+
history.push({ role: 'user', content: userParts });
|
|
106
114
|
}
|
|
107
115
|
continue;
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
if (m.role === 'assistant') {
|
|
111
119
|
const assistantParts: UIMessage['parts'] = [];
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const flushAssistantParts = async () => {
|
|
121
|
+
if (!assistantParts.length) return;
|
|
122
|
+
history.push(
|
|
123
|
+
...(await convertToModelMessages([
|
|
124
|
+
{ role: 'assistant', parts: assistantParts },
|
|
125
|
+
])),
|
|
126
|
+
);
|
|
127
|
+
assistantParts.length = 0;
|
|
128
|
+
};
|
|
129
|
+
const toolResultsById = new Map<
|
|
130
|
+
string,
|
|
131
|
+
{
|
|
132
|
+
name: string;
|
|
133
|
+
callId: string;
|
|
134
|
+
result: unknown;
|
|
135
|
+
}
|
|
136
|
+
>();
|
|
137
|
+
|
|
138
|
+
for (const p of parts) {
|
|
139
|
+
if (p.type !== 'tool_result' || p.compactedAt) continue;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const obj = JSON.parse(p.content ?? '{}') as {
|
|
143
|
+
name?: string;
|
|
144
|
+
callId?: string;
|
|
145
|
+
result?: unknown;
|
|
146
|
+
};
|
|
147
|
+
if (obj.callId) {
|
|
148
|
+
toolResultsById.set(obj.callId, {
|
|
149
|
+
name: obj.name ?? 'tool',
|
|
150
|
+
callId: obj.callId,
|
|
151
|
+
result: obj.result,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
119
156
|
|
|
120
157
|
for (const p of parts) {
|
|
121
158
|
if (p.type === 'reasoning') continue;
|
|
@@ -135,89 +172,60 @@ export async function buildHistoryMessages(
|
|
|
135
172
|
callId?: string;
|
|
136
173
|
args?: unknown;
|
|
137
174
|
};
|
|
138
|
-
if (obj.callId
|
|
139
|
-
|
|
175
|
+
if (!obj.callId || !obj.name) continue;
|
|
176
|
+
if (obj.name === 'finish') continue;
|
|
177
|
+
|
|
178
|
+
const toolType = `tool-${obj.name}` as `tool-${string}`;
|
|
179
|
+
let result = toolResultsById.get(obj.callId);
|
|
180
|
+
|
|
181
|
+
if (!result) {
|
|
182
|
+
debugLog(
|
|
183
|
+
`[buildHistoryMessages] Synthesizing error result for incomplete tool call ${obj.name}#${obj.callId}`,
|
|
184
|
+
);
|
|
185
|
+
result = {
|
|
140
186
|
name: obj.name,
|
|
141
187
|
callId: obj.callId,
|
|
142
|
-
|
|
143
|
-
|
|
188
|
+
result:
|
|
189
|
+
'Error: The tool execution was interrupted or failed to return a result. You may need to retry this operation.',
|
|
190
|
+
};
|
|
144
191
|
}
|
|
145
|
-
} catch {}
|
|
146
|
-
} else if (p.type === 'tool_result') {
|
|
147
|
-
if (p.compactedAt) continue;
|
|
148
192
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
callId
|
|
153
|
-
|
|
193
|
+
const part = {
|
|
194
|
+
type: toolType,
|
|
195
|
+
state: 'output-available',
|
|
196
|
+
toolCallId: obj.callId,
|
|
197
|
+
input: obj.args,
|
|
198
|
+
output: (() => {
|
|
199
|
+
const r = result.result;
|
|
200
|
+
if (typeof r === 'string') return r;
|
|
201
|
+
try {
|
|
202
|
+
return JSON.stringify(r);
|
|
203
|
+
} catch {
|
|
204
|
+
return String(r);
|
|
205
|
+
}
|
|
206
|
+
})(),
|
|
154
207
|
};
|
|
155
|
-
if (obj.callId) {
|
|
156
|
-
toolResults.push({
|
|
157
|
-
name: obj.name ?? 'tool',
|
|
158
|
-
callId: obj.callId,
|
|
159
|
-
result: obj.result,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
} catch {}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const toolResultsById = new Map(
|
|
167
|
-
toolResults.map((result) => [result.callId, result]),
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
for (const call of toolCalls) {
|
|
171
|
-
if (call.name === 'finish') continue;
|
|
172
208
|
|
|
173
|
-
|
|
174
|
-
|
|
209
|
+
toolHistory.register(part, {
|
|
210
|
+
toolName: obj.name,
|
|
211
|
+
callId: obj.callId,
|
|
212
|
+
args: obj.args,
|
|
213
|
+
result: result.result,
|
|
214
|
+
});
|
|
175
215
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
180
|
-
result = {
|
|
181
|
-
name: call.name,
|
|
182
|
-
callId: call.callId,
|
|
183
|
-
result:
|
|
184
|
-
'Error: The tool execution was interrupted or failed to return a result. You may need to retry this operation.',
|
|
185
|
-
};
|
|
216
|
+
assistantParts.push(part as never);
|
|
217
|
+
await flushAssistantParts();
|
|
218
|
+
} catch {}
|
|
186
219
|
}
|
|
187
|
-
|
|
188
|
-
const part = {
|
|
189
|
-
type: toolType,
|
|
190
|
-
state: 'output-available',
|
|
191
|
-
toolCallId: call.callId,
|
|
192
|
-
input: call.args,
|
|
193
|
-
output: (() => {
|
|
194
|
-
const r = result.result;
|
|
195
|
-
if (typeof r === 'string') return r;
|
|
196
|
-
try {
|
|
197
|
-
return JSON.stringify(r);
|
|
198
|
-
} catch {
|
|
199
|
-
return String(r);
|
|
200
|
-
}
|
|
201
|
-
})(),
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
toolHistory.register(part, {
|
|
205
|
-
toolName: call.name,
|
|
206
|
-
callId: call.callId,
|
|
207
|
-
args: call.args,
|
|
208
|
-
result: result.result,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
assistantParts.push(part as never);
|
|
212
220
|
}
|
|
213
221
|
|
|
214
222
|
if (assistantParts.length) {
|
|
215
|
-
|
|
223
|
+
await flushAssistantParts();
|
|
216
224
|
}
|
|
217
225
|
}
|
|
218
226
|
}
|
|
219
227
|
|
|
220
|
-
return
|
|
228
|
+
return history;
|
|
221
229
|
}
|
|
222
230
|
|
|
223
231
|
async function _logPendingToolParts(
|
|
@@ -7,7 +7,11 @@ import { publish } from '../../events/bus.ts';
|
|
|
7
7
|
import { enqueueAssistantRun } from '../session/queue.ts';
|
|
8
8
|
import { runSessionLoop } from '../agent/runner.ts';
|
|
9
9
|
import { resolveModel } from '../provider/index.ts';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getFastModelForAuth,
|
|
12
|
+
type ProviderId,
|
|
13
|
+
type ReasoningLevel,
|
|
14
|
+
} from '@ottocode/sdk';
|
|
11
15
|
import { debugLog } from '../debug/index.ts';
|
|
12
16
|
import { isCompactCommand, buildCompactionContext } from './compaction.ts';
|
|
13
17
|
import { detectOAuth, adaptSimpleCall } from '../provider/oauth-adapter.ts';
|
|
@@ -25,6 +29,7 @@ type DispatchOptions = {
|
|
|
25
29
|
oneShot?: boolean;
|
|
26
30
|
userContext?: string;
|
|
27
31
|
reasoningText?: boolean;
|
|
32
|
+
reasoningLevel?: ReasoningLevel;
|
|
28
33
|
images?: Array<{ data: string; mediaType: string }>;
|
|
29
34
|
files?: Array<{
|
|
30
35
|
type: 'image' | 'pdf' | 'text';
|
|
@@ -49,6 +54,7 @@ export async function dispatchAssistantMessage(
|
|
|
49
54
|
oneShot,
|
|
50
55
|
userContext,
|
|
51
56
|
reasoningText,
|
|
57
|
+
reasoningLevel,
|
|
52
58
|
images,
|
|
53
59
|
files,
|
|
54
60
|
} = options;
|
|
@@ -187,6 +193,7 @@ export async function dispatchAssistantMessage(
|
|
|
187
193
|
oneShot: Boolean(oneShot),
|
|
188
194
|
userContext,
|
|
189
195
|
reasoningText,
|
|
196
|
+
reasoningLevel,
|
|
190
197
|
isCompactCommand: isCompact,
|
|
191
198
|
compactionContext,
|
|
192
199
|
toolApprovalMode,
|
|
@@ -71,21 +71,23 @@ export async function composeSystemPrompt(options: {
|
|
|
71
71
|
options.projectRoot,
|
|
72
72
|
);
|
|
73
73
|
const baseInstructions = (BASE_PROMPT || '').trim();
|
|
74
|
+
const agentInstructions = options.agentPrompt.trim();
|
|
75
|
+
const providerInstructions = providerResult.prompt.trim();
|
|
74
76
|
|
|
75
77
|
parts.push(
|
|
76
|
-
providerResult.prompt.trim(),
|
|
77
78
|
baseInstructions.trim(),
|
|
78
|
-
|
|
79
|
+
agentInstructions,
|
|
80
|
+
providerInstructions,
|
|
79
81
|
);
|
|
80
|
-
if (providerResult.prompt.trim()) {
|
|
81
|
-
components.push(`provider:${providerResult.resolvedType}`);
|
|
82
|
-
}
|
|
83
82
|
if (baseInstructions.trim()) {
|
|
84
83
|
components.push('base');
|
|
85
84
|
}
|
|
86
|
-
if (
|
|
85
|
+
if (agentInstructions) {
|
|
87
86
|
components.push('agent');
|
|
88
87
|
}
|
|
88
|
+
if (providerInstructions) {
|
|
89
|
+
components.push(`provider:${providerResult.resolvedType}`);
|
|
90
|
+
}
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
if (options.oneShot) {
|