@agi-cli/server 0.1.96 → 0.1.97
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/index.ts +4 -1
- package/src/runtime/agent-registry.ts +6 -1
- package/src/runtime/history/tool-history-tracker.ts +204 -0
- package/src/runtime/history-builder.ts +85 -29
- package/src/runtime/message-service.ts +12 -8
- package/src/runtime/prompt.ts +58 -4
- package/src/runtime/runner.ts +35 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.97",
|
|
4
4
|
"description": "HTTP API server for AGI CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agi-cli/sdk": "0.1.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
32
|
+
"@agi-cli/sdk": "0.1.97",
|
|
33
|
+
"@agi-cli/database": "0.1.97",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -228,7 +228,10 @@ export {
|
|
|
228
228
|
resolveAgentConfig,
|
|
229
229
|
defaultToolsForAgent,
|
|
230
230
|
} from './runtime/agent-registry.ts';
|
|
231
|
-
export {
|
|
231
|
+
export {
|
|
232
|
+
composeSystemPrompt,
|
|
233
|
+
type ComposedSystemPrompt,
|
|
234
|
+
} from './runtime/prompt.ts';
|
|
232
235
|
export {
|
|
233
236
|
AskServiceError,
|
|
234
237
|
handleAskRequest,
|
|
@@ -317,7 +317,12 @@ export async function resolveAgentConfig(
|
|
|
317
317
|
const provider = normalizeProvider(entry?.provider);
|
|
318
318
|
const model = normalizeModel(entry?.model);
|
|
319
319
|
debugLog(`[agent] ${name} prompt source: ${promptSource}`);
|
|
320
|
-
debugLog(
|
|
320
|
+
debugLog(
|
|
321
|
+
`[agent] ${name} prompt summary: ${JSON.stringify({
|
|
322
|
+
length: prompt.length,
|
|
323
|
+
lines: prompt.split('\n').length,
|
|
324
|
+
})}`,
|
|
325
|
+
);
|
|
321
326
|
return {
|
|
322
327
|
name,
|
|
323
328
|
prompt,
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { debugLog } from '../debug.ts';
|
|
2
|
+
|
|
3
|
+
type ToolResultPart = {
|
|
4
|
+
type: string;
|
|
5
|
+
state?: string;
|
|
6
|
+
toolCallId?: string;
|
|
7
|
+
input?: unknown;
|
|
8
|
+
output?: unknown;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ToolResultInfo = {
|
|
13
|
+
toolName: string;
|
|
14
|
+
callId: string;
|
|
15
|
+
args: unknown;
|
|
16
|
+
result: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TargetDescriptor = {
|
|
20
|
+
keys: string[];
|
|
21
|
+
summary: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TrackedPart = {
|
|
25
|
+
part: ToolResultPart;
|
|
26
|
+
summary: string;
|
|
27
|
+
summarized: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class ToolHistoryTracker {
|
|
31
|
+
private readonly targets = new Map<string, TrackedPart>();
|
|
32
|
+
|
|
33
|
+
register(part: ToolResultPart, info: ToolResultInfo) {
|
|
34
|
+
const descriptor = describeToolResult(info);
|
|
35
|
+
if (!descriptor) return;
|
|
36
|
+
|
|
37
|
+
const entry: TrackedPart = {
|
|
38
|
+
part,
|
|
39
|
+
summary: descriptor.summary,
|
|
40
|
+
summarized: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (const key of descriptor.keys) {
|
|
44
|
+
const previous = this.targets.get(key);
|
|
45
|
+
if (previous && previous.part !== part) {
|
|
46
|
+
this.applySummary(previous);
|
|
47
|
+
}
|
|
48
|
+
this.targets.set(key, entry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private applySummary(entry: TrackedPart) {
|
|
53
|
+
if (entry.summarized) return;
|
|
54
|
+
// Keep this entry as a tool result so the history still produces tool_result blocks.
|
|
55
|
+
entry.part.state = 'output-available';
|
|
56
|
+
entry.part.output = entry.summary;
|
|
57
|
+
(entry.part as { summaryText?: string }).summaryText = entry.summary;
|
|
58
|
+
delete (entry.part as { errorText?: unknown }).errorText;
|
|
59
|
+
delete (entry.part as { rawInput?: unknown }).rawInput;
|
|
60
|
+
delete (entry.part as { callProviderMetadata?: unknown })
|
|
61
|
+
.callProviderMetadata;
|
|
62
|
+
delete (entry.part as { providerMetadata?: unknown }).providerMetadata;
|
|
63
|
+
entry.summarized = true;
|
|
64
|
+
debugLog(`[history] summarized tool output -> ${entry.summary}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function describeToolResult(info: ToolResultInfo): TargetDescriptor | null {
|
|
69
|
+
const { toolName } = info;
|
|
70
|
+
switch (toolName) {
|
|
71
|
+
case 'read':
|
|
72
|
+
return describeRead(info);
|
|
73
|
+
case 'glob':
|
|
74
|
+
case 'grep':
|
|
75
|
+
return describePatternTool(info, toolName);
|
|
76
|
+
case 'write':
|
|
77
|
+
return describeWrite(info);
|
|
78
|
+
case 'apply_patch':
|
|
79
|
+
return describePatch(info);
|
|
80
|
+
default:
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function describeRead(info: ToolResultInfo): TargetDescriptor | null {
|
|
86
|
+
const args = getRecord(info.args);
|
|
87
|
+
if (!args) return null;
|
|
88
|
+
const path = getString(args.path);
|
|
89
|
+
if (!path) return null;
|
|
90
|
+
const startLine = getNumber(args.startLine);
|
|
91
|
+
const endLine = getNumber(args.endLine);
|
|
92
|
+
|
|
93
|
+
let rangeLabel = 'entire file';
|
|
94
|
+
let rangeKey = 'all';
|
|
95
|
+
if (startLine !== undefined || endLine !== undefined) {
|
|
96
|
+
const start = startLine ?? 1;
|
|
97
|
+
const end = endLine ?? 'end';
|
|
98
|
+
rangeLabel = `lines ${start}–${end}`;
|
|
99
|
+
rangeKey = `${start}-${end}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const key = `read:${normalizePath(path)}:${rangeKey}`;
|
|
103
|
+
const summary = `[previous read] ${normalizePath(path)} (${rangeLabel})`;
|
|
104
|
+
return { keys: [key], summary };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function describePatternTool(
|
|
108
|
+
info: ToolResultInfo,
|
|
109
|
+
toolName: string,
|
|
110
|
+
): TargetDescriptor | null {
|
|
111
|
+
const args = getRecord(info.args);
|
|
112
|
+
if (!args) return null;
|
|
113
|
+
const pattern =
|
|
114
|
+
getString(args.pattern) ??
|
|
115
|
+
getString(args.filePattern) ??
|
|
116
|
+
getString(args.path);
|
|
117
|
+
if (!pattern) return null;
|
|
118
|
+
const key = `${toolName}:${pattern}`;
|
|
119
|
+
const summary = `[previous ${toolName}] ${pattern}`;
|
|
120
|
+
return { keys: [key], summary };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function describeWrite(info: ToolResultInfo): TargetDescriptor | null {
|
|
124
|
+
const result = getRecord(info.result);
|
|
125
|
+
if (!result) return null;
|
|
126
|
+
const path = getString(result.path);
|
|
127
|
+
if (!path) return null;
|
|
128
|
+
const bytes = getNumber(result.bytes);
|
|
129
|
+
const sizeLabel =
|
|
130
|
+
typeof bytes === 'number' && Number.isFinite(bytes)
|
|
131
|
+
? `${bytes} bytes`
|
|
132
|
+
: 'unknown size';
|
|
133
|
+
const key = `write:${normalizePath(path)}`;
|
|
134
|
+
const summary = `[previous write] ${normalizePath(path)} (${sizeLabel})`;
|
|
135
|
+
return { keys: [key], summary };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function describePatch(info: ToolResultInfo): TargetDescriptor | null {
|
|
139
|
+
const result = getRecord(info.result);
|
|
140
|
+
if (!result) return null;
|
|
141
|
+
|
|
142
|
+
const files = new Set<string>();
|
|
143
|
+
|
|
144
|
+
const changes = getArray(result.changes);
|
|
145
|
+
if (changes) {
|
|
146
|
+
for (const change of changes) {
|
|
147
|
+
const changeObj = getRecord(change);
|
|
148
|
+
const filePath = changeObj && getString(changeObj.filePath);
|
|
149
|
+
if (filePath) files.add(normalizePath(filePath));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const artifact = getRecord(result.artifact);
|
|
154
|
+
if (artifact) {
|
|
155
|
+
const summary = getRecord(artifact.summary);
|
|
156
|
+
const summaryFiles = getArray(summary?.files);
|
|
157
|
+
if (summaryFiles) {
|
|
158
|
+
for (const item of summaryFiles) {
|
|
159
|
+
if (typeof item === 'string') files.add(normalizePath(item));
|
|
160
|
+
else {
|
|
161
|
+
const record = getRecord(item);
|
|
162
|
+
const value = record && getString(record.path);
|
|
163
|
+
if (value) files.add(normalizePath(value));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fileList = Array.from(files);
|
|
170
|
+
const keys = fileList.length
|
|
171
|
+
? fileList.map((file) => `apply_patch:${file}`)
|
|
172
|
+
: [`apply_patch:${info.callId}`];
|
|
173
|
+
|
|
174
|
+
const summary =
|
|
175
|
+
fileList.length > 0
|
|
176
|
+
? `[previous patch] ${fileList.join(', ')}`
|
|
177
|
+
: '[previous patch] (unknown files)';
|
|
178
|
+
|
|
179
|
+
return { keys, summary };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getRecord(value: unknown): Record<string, unknown> | null {
|
|
183
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
184
|
+
? (value as Record<string, unknown>)
|
|
185
|
+
: null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getArray(value: unknown): unknown[] | null {
|
|
189
|
+
return Array.isArray(value) ? value : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getString(value: unknown): string | null {
|
|
193
|
+
if (typeof value === 'string') return value;
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getNumber(value: unknown): number | undefined {
|
|
198
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizePath(path: string): string {
|
|
203
|
+
return path.replace(/\\/g, '/');
|
|
204
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { getDb } from '@agi-cli/database';
|
|
|
3
3
|
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
4
4
|
import { eq, asc } from 'drizzle-orm';
|
|
5
5
|
import { debugLog } from './debug.ts';
|
|
6
|
+
import { ToolHistoryTracker } from './history/tool-history-tracker.ts';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Builds the conversation history for a session from the database,
|
|
@@ -19,12 +20,14 @@ export async function buildHistoryMessages(
|
|
|
19
20
|
.orderBy(asc(messages.createdAt));
|
|
20
21
|
|
|
21
22
|
const ui: UIMessage[] = [];
|
|
23
|
+
const toolHistory = new ToolHistoryTracker();
|
|
22
24
|
|
|
23
25
|
for (const m of rows) {
|
|
24
26
|
if (m.role === 'assistant' && m.status !== 'complete') {
|
|
25
27
|
debugLog(
|
|
26
|
-
`[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status}`,
|
|
28
|
+
`[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status} (current turn still in progress)`,
|
|
27
29
|
);
|
|
30
|
+
logPendingToolParts(db, m.id);
|
|
28
31
|
continue;
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -103,13 +106,21 @@ export async function buildHistoryMessages(
|
|
|
103
106
|
// Skip error parts in history
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
const toolResultsById = new Map(
|
|
110
|
+
toolResults.map((result) => [result.callId, result]),
|
|
111
|
+
);
|
|
112
|
+
|
|
106
113
|
const hasIncompleteTools = toolCalls.some(
|
|
107
|
-
(call) => !
|
|
114
|
+
(call) => !toolResultsById.has(call.callId),
|
|
108
115
|
);
|
|
109
116
|
|
|
110
117
|
if (hasIncompleteTools) {
|
|
118
|
+
const pendingCalls = toolCalls
|
|
119
|
+
.filter((call) => !toolResultsById.has(call.callId))
|
|
120
|
+
.map((call) => `${call.name}#${call.callId}`)
|
|
121
|
+
.join(', ');
|
|
111
122
|
debugLog(
|
|
112
|
-
`[buildHistoryMessages] Incomplete tool calls for assistant message ${m.id},
|
|
123
|
+
`[buildHistoryMessages] Incomplete tool calls for assistant message ${m.id}, skipping tool data (pending: ${pendingCalls || 'unknown'})`,
|
|
113
124
|
);
|
|
114
125
|
if (assistantParts.length) {
|
|
115
126
|
ui.push({ id: m.id, role: 'assistant', parts: assistantParts });
|
|
@@ -122,47 +133,92 @@ export async function buildHistoryMessages(
|
|
|
122
133
|
if (call.name === 'finish') continue;
|
|
123
134
|
|
|
124
135
|
const toolType = `tool-${call.name}` as `tool-${string}`;
|
|
125
|
-
const result =
|
|
136
|
+
const result = toolResultsById.get(call.callId);
|
|
126
137
|
|
|
127
138
|
if (result) {
|
|
128
|
-
const
|
|
129
|
-
const r = result.result;
|
|
130
|
-
if (typeof r === 'string') return r;
|
|
131
|
-
try {
|
|
132
|
-
return JSON.stringify(r);
|
|
133
|
-
} catch {
|
|
134
|
-
return String(r);
|
|
135
|
-
}
|
|
136
|
-
})();
|
|
137
|
-
|
|
138
|
-
assistantParts.push({
|
|
139
|
+
const part = {
|
|
139
140
|
type: toolType,
|
|
140
141
|
state: 'output-available',
|
|
141
142
|
toolCallId: call.callId,
|
|
142
143
|
input: call.args,
|
|
143
|
-
output:
|
|
144
|
-
|
|
144
|
+
output: (() => {
|
|
145
|
+
const r = result.result;
|
|
146
|
+
if (typeof r === 'string') return r;
|
|
147
|
+
try {
|
|
148
|
+
return JSON.stringify(r);
|
|
149
|
+
} catch {
|
|
150
|
+
return String(r);
|
|
151
|
+
}
|
|
152
|
+
})(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
toolHistory.register(part, {
|
|
156
|
+
toolName: call.name,
|
|
157
|
+
callId: call.callId,
|
|
158
|
+
args: call.args,
|
|
159
|
+
result: result.result,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
assistantParts.push(part as never);
|
|
145
163
|
}
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
if (assistantParts.length) {
|
|
149
167
|
ui.push({ id: m.id, role: 'assistant', parts: assistantParts });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
150
171
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
return convertToModelMessages(ui);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function logPendingToolParts(
|
|
176
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
177
|
+
messageId: string,
|
|
178
|
+
) {
|
|
179
|
+
try {
|
|
180
|
+
const parts = await db
|
|
181
|
+
.select()
|
|
182
|
+
.from(messageParts)
|
|
183
|
+
.where(eq(messageParts.messageId, messageId))
|
|
184
|
+
.orderBy(asc(messageParts.index));
|
|
185
|
+
|
|
186
|
+
const pendingCalls: string[] = [];
|
|
187
|
+
for (const part of parts) {
|
|
188
|
+
if (part.type !== 'tool_call') continue;
|
|
189
|
+
try {
|
|
190
|
+
const obj = JSON.parse(part.content ?? '{}') as {
|
|
191
|
+
name?: string;
|
|
192
|
+
callId?: string;
|
|
193
|
+
};
|
|
194
|
+
if (obj.name && obj.callId) {
|
|
195
|
+
const resultExists = parts.some((candidate) => {
|
|
196
|
+
if (candidate.type !== 'tool_result') return false;
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(candidate.content ?? '{}') as {
|
|
199
|
+
callId?: string;
|
|
200
|
+
};
|
|
201
|
+
return parsed.callId === obj.callId;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
158
205
|
});
|
|
159
|
-
if (
|
|
160
|
-
|
|
206
|
+
if (!resultExists) {
|
|
207
|
+
pendingCalls.push(`${obj.name}#${obj.callId}`);
|
|
161
208
|
}
|
|
162
209
|
}
|
|
163
|
-
}
|
|
210
|
+
} catch {}
|
|
164
211
|
}
|
|
212
|
+
if (pendingCalls.length) {
|
|
213
|
+
debugLog(
|
|
214
|
+
`[buildHistoryMessages] Pending tool calls for assistant message ${messageId}: ${pendingCalls.join(', ')}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
debugLog(
|
|
219
|
+
`[buildHistoryMessages] Failed to inspect pending tool calls for ${messageId}: ${
|
|
220
|
+
err instanceof Error ? err.message : String(err)
|
|
221
|
+
}`,
|
|
222
|
+
);
|
|
165
223
|
}
|
|
166
|
-
|
|
167
|
-
return convertToModelMessages(ui);
|
|
168
224
|
}
|
|
@@ -246,19 +246,23 @@ async function generateSessionTitle(args: {
|
|
|
246
246
|
},
|
|
247
247
|
];
|
|
248
248
|
|
|
249
|
-
debugLog(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
debugLog(
|
|
253
|
-
|
|
249
|
+
debugLog(
|
|
250
|
+
`[TITLE_GEN] Using OAuth mode (prompts: spoof:${provider}, title-generator, user-request)`,
|
|
251
|
+
);
|
|
252
|
+
debugLog(
|
|
253
|
+
`[TITLE_GEN] User content preview: ${promptText.substring(0, 100)}...`,
|
|
254
|
+
);
|
|
254
255
|
} else {
|
|
255
256
|
// API key mode: normal flow
|
|
256
257
|
system = titlePrompt;
|
|
257
258
|
messagesArray = [{ role: 'user', content: promptText }];
|
|
258
259
|
|
|
259
|
-
debugLog(
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
debugLog(
|
|
261
|
+
`[TITLE_GEN] Using API key mode (prompts: title-generator, user-request)`,
|
|
262
|
+
);
|
|
263
|
+
debugLog(
|
|
264
|
+
`[TITLE_GEN] User content preview: ${promptText.substring(0, 100)}...`,
|
|
265
|
+
);
|
|
262
266
|
}
|
|
263
267
|
|
|
264
268
|
debugLog('[TITLE_GEN] Calling generateText...');
|
package/src/runtime/prompt.ts
CHANGED
|
@@ -12,6 +12,12 @@ import ANTHROPIC_SPOOF_PROMPT from '@agi-cli/sdk/prompts/providers/anthropicSpoo
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
import { getTerminalManager } from '@agi-cli/sdk';
|
|
15
|
+
|
|
16
|
+
export type ComposedSystemPrompt = {
|
|
17
|
+
prompt: string;
|
|
18
|
+
components: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
15
21
|
export async function composeSystemPrompt(options: {
|
|
16
22
|
provider: string;
|
|
17
23
|
model?: string;
|
|
@@ -22,9 +28,17 @@ export async function composeSystemPrompt(options: {
|
|
|
22
28
|
includeEnvironment?: boolean;
|
|
23
29
|
includeProjectTree?: boolean;
|
|
24
30
|
userContext?: string;
|
|
25
|
-
}): Promise<
|
|
31
|
+
}): Promise<ComposedSystemPrompt> {
|
|
32
|
+
const components: string[] = [];
|
|
26
33
|
if (options.spoofPrompt) {
|
|
27
|
-
|
|
34
|
+
const prompt = options.spoofPrompt.trim();
|
|
35
|
+
const providerComponent = options.provider
|
|
36
|
+
? `spoof:${options.provider}`
|
|
37
|
+
: 'spoof:unknown';
|
|
38
|
+
return {
|
|
39
|
+
prompt,
|
|
40
|
+
components: [providerComponent],
|
|
41
|
+
};
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
const parts: string[] = [];
|
|
@@ -41,6 +55,18 @@ export async function composeSystemPrompt(options: {
|
|
|
41
55
|
baseInstructions.trim(),
|
|
42
56
|
options.agentPrompt.trim(),
|
|
43
57
|
);
|
|
58
|
+
if (providerPrompt.trim()) {
|
|
59
|
+
const providerComponent = options.provider
|
|
60
|
+
? `provider:${options.provider}`
|
|
61
|
+
: 'provider:unknown';
|
|
62
|
+
components.push(providerComponent);
|
|
63
|
+
}
|
|
64
|
+
if (baseInstructions.trim()) {
|
|
65
|
+
components.push('base');
|
|
66
|
+
}
|
|
67
|
+
if (options.agentPrompt.trim()) {
|
|
68
|
+
components.push('agent');
|
|
69
|
+
}
|
|
44
70
|
|
|
45
71
|
if (options.oneShot) {
|
|
46
72
|
const oneShotBlock =
|
|
@@ -51,6 +77,7 @@ export async function composeSystemPrompt(options: {
|
|
|
51
77
|
'</system-reminder>',
|
|
52
78
|
].join('\n');
|
|
53
79
|
parts.push(oneShotBlock);
|
|
80
|
+
components.push('mode:oneshot');
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
if (options.includeEnvironment !== false) {
|
|
@@ -60,6 +87,10 @@ export async function composeSystemPrompt(options: {
|
|
|
60
87
|
);
|
|
61
88
|
if (envAndInstructions) {
|
|
62
89
|
parts.push(envAndInstructions);
|
|
90
|
+
components.push('environment');
|
|
91
|
+
if (options.includeProjectTree) {
|
|
92
|
+
components.push('project-tree');
|
|
93
|
+
}
|
|
63
94
|
}
|
|
64
95
|
}
|
|
65
96
|
|
|
@@ -71,6 +102,7 @@ export async function composeSystemPrompt(options: {
|
|
|
71
102
|
'</user-provided-state-context>',
|
|
72
103
|
].join('\n');
|
|
73
104
|
parts.push(userContextBlock);
|
|
105
|
+
components.push('user-context');
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
// Add terminal context if available
|
|
@@ -79,17 +111,27 @@ export async function composeSystemPrompt(options: {
|
|
|
79
111
|
const terminalContext = terminalManager.getContext();
|
|
80
112
|
if (terminalContext) {
|
|
81
113
|
parts.push(terminalContext);
|
|
114
|
+
components.push('terminal-context');
|
|
82
115
|
}
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
const composed = parts.filter(Boolean).join('\n\n').trim();
|
|
86
|
-
if (composed)
|
|
119
|
+
if (composed) {
|
|
120
|
+
return {
|
|
121
|
+
prompt: composed,
|
|
122
|
+
components: dedupeComponents(components),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
87
125
|
|
|
88
|
-
|
|
126
|
+
const fallback = [
|
|
89
127
|
'You are a concise, friendly coding agent.',
|
|
90
128
|
'Be precise and actionable. Use tools when needed, prefer small diffs.',
|
|
91
129
|
'Stream your answer; call finish when done.',
|
|
92
130
|
].join(' ');
|
|
131
|
+
return {
|
|
132
|
+
prompt: fallback,
|
|
133
|
+
components: dedupeComponents([...components, 'fallback']),
|
|
134
|
+
};
|
|
93
135
|
}
|
|
94
136
|
|
|
95
137
|
export function getProviderSpoofPrompt(provider: string): string | undefined {
|
|
@@ -98,3 +140,15 @@ export function getProviderSpoofPrompt(provider: string): string | undefined {
|
|
|
98
140
|
}
|
|
99
141
|
return undefined;
|
|
100
142
|
}
|
|
143
|
+
|
|
144
|
+
function dedupeComponents(input: string[]): string[] {
|
|
145
|
+
const seen = new Set<string>();
|
|
146
|
+
const out: string[] = [];
|
|
147
|
+
for (const item of input) {
|
|
148
|
+
if (!item) continue;
|
|
149
|
+
if (seen.has(item)) continue;
|
|
150
|
+
seen.add(item);
|
|
151
|
+
out.push(item);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
package/src/runtime/runner.ts
CHANGED
|
@@ -61,6 +61,12 @@ export async function runSessionLoop(sessionId: string) {
|
|
|
61
61
|
* Main function to run the assistant for a given request.
|
|
62
62
|
*/
|
|
63
63
|
async function runAssistant(opts: RunOpts) {
|
|
64
|
+
const separator = '='.repeat(72);
|
|
65
|
+
debugLog(separator);
|
|
66
|
+
debugLog(
|
|
67
|
+
`[RUNNER] Starting turn for session ${opts.sessionId}, message ${opts.assistantMessageId}`,
|
|
68
|
+
);
|
|
69
|
+
|
|
64
70
|
const cfgTimer = time('runner:loadConfig+db');
|
|
65
71
|
const cfg = await loadConfig(opts.projectRoot);
|
|
66
72
|
const db = await getDb(cfg.projectRoot);
|
|
@@ -99,14 +105,19 @@ async function runAssistant(opts: RunOpts) {
|
|
|
99
105
|
: undefined;
|
|
100
106
|
|
|
101
107
|
debugLog(`[RUNNER] needsSpoof (OAuth): ${needsSpoof}`);
|
|
102
|
-
debugLog(
|
|
108
|
+
debugLog(
|
|
109
|
+
`[RUNNER] spoofPrompt: ${spoofPrompt ? `present (${opts.provider})` : 'none'}`,
|
|
110
|
+
);
|
|
103
111
|
|
|
104
112
|
let system: string;
|
|
113
|
+
let systemComponents: string[] = [];
|
|
114
|
+
let oauthFullPromptComponents: string[] | undefined;
|
|
105
115
|
let additionalSystemMessages: Array<{ role: 'system'; content: string }> = [];
|
|
106
116
|
|
|
107
117
|
if (spoofPrompt) {
|
|
108
118
|
// OAuth mode: short spoof in system field, full instructions in messages array
|
|
109
119
|
system = spoofPrompt;
|
|
120
|
+
systemComponents = [`spoof:${opts.provider || 'unknown'}`];
|
|
110
121
|
const fullPrompt = await composeSystemPrompt({
|
|
111
122
|
provider: opts.provider,
|
|
112
123
|
model: opts.model,
|
|
@@ -117,26 +128,27 @@ async function runAssistant(opts: RunOpts) {
|
|
|
117
128
|
includeProjectTree: isFirstMessage,
|
|
118
129
|
userContext: opts.userContext,
|
|
119
130
|
});
|
|
131
|
+
oauthFullPromptComponents = fullPrompt.components;
|
|
120
132
|
|
|
121
133
|
// FIX: Always add the system message for OAuth because:
|
|
122
134
|
// 1. System messages are NOT stored in the database
|
|
123
135
|
// 2. buildHistoryMessages only returns user/assistant messages
|
|
124
136
|
// 3. We need the full instructions on every turn
|
|
125
|
-
additionalSystemMessages = [{ role: 'system', content: fullPrompt }];
|
|
137
|
+
additionalSystemMessages = [{ role: 'system', content: fullPrompt.prompt }];
|
|
126
138
|
|
|
127
139
|
debugLog('[RUNNER] OAuth mode: additionalSystemMessages created');
|
|
128
|
-
|
|
140
|
+
const includesUserContext =
|
|
141
|
+
!!opts.userContext && fullPrompt.prompt.includes(opts.userContext);
|
|
129
142
|
debugLog(
|
|
130
|
-
`[
|
|
143
|
+
`[system] oauth-full summary: ${JSON.stringify({
|
|
144
|
+
components: oauthFullPromptComponents ?? [],
|
|
145
|
+
length: fullPrompt.prompt.length,
|
|
146
|
+
includesUserContext,
|
|
147
|
+
})}`,
|
|
131
148
|
);
|
|
132
|
-
if (opts.userContext && fullPrompt.includes(opts.userContext)) {
|
|
133
|
-
debugLog('[RUNNER] ✅ userContext IS in fullPrompt');
|
|
134
|
-
} else if (opts.userContext) {
|
|
135
|
-
debugLog('[RUNNER] ❌ userContext NOT in fullPrompt!');
|
|
136
|
-
}
|
|
137
149
|
} else {
|
|
138
150
|
// API key mode: full instructions in system field
|
|
139
|
-
|
|
151
|
+
const composed = await composeSystemPrompt({
|
|
140
152
|
provider: opts.provider,
|
|
141
153
|
model: opts.model,
|
|
142
154
|
projectRoot: cfg.projectRoot,
|
|
@@ -146,10 +158,16 @@ async function runAssistant(opts: RunOpts) {
|
|
|
146
158
|
includeProjectTree: isFirstMessage,
|
|
147
159
|
userContext: opts.userContext,
|
|
148
160
|
});
|
|
161
|
+
system = composed.prompt;
|
|
162
|
+
systemComponents = composed.components;
|
|
149
163
|
}
|
|
150
164
|
systemTimer.end();
|
|
151
|
-
debugLog(
|
|
152
|
-
|
|
165
|
+
debugLog(
|
|
166
|
+
`[system] summary: ${JSON.stringify({
|
|
167
|
+
components: systemComponents,
|
|
168
|
+
length: system.length,
|
|
169
|
+
})}`,
|
|
170
|
+
);
|
|
153
171
|
|
|
154
172
|
const toolsTimer = time('runner:discoverTools');
|
|
155
173
|
const allTools = await discoverProjectTools(cfg.projectRoot);
|
|
@@ -500,5 +518,10 @@ async function runAssistant(opts: RunOpts) {
|
|
|
500
518
|
await completeAssistantMessage({}, opts, db);
|
|
501
519
|
} catch {}
|
|
502
520
|
throw err;
|
|
521
|
+
} finally {
|
|
522
|
+
debugLog(
|
|
523
|
+
`[RUNNER] Turn complete for session ${opts.sessionId}, message ${opts.assistantMessageId}`,
|
|
524
|
+
);
|
|
525
|
+
debugLog(separator);
|
|
503
526
|
}
|
|
504
527
|
}
|