Skip to content

Commit ecaaeac

Browse files
Merge pull request #1532 from kirill-markin/loop-claude/lean-agent-sql-write-payload
Shrink agent SQL write payloads and stop post-commit write failures
2 parents 9f2f625 + 5a6cade commit ecaaeac

7 files changed

Lines changed: 153 additions & 65 deletions

File tree

apps/backend/src/aiTools/agentSql.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import { executeSqlReadBatch, executeSqlReadStatement } from "./agentSql/readExe
1515
import {
1616
isSqlMutationStatement,
1717
isSqlReadStatement,
18+
type AgentSqlBatchExecutionResult,
1819
type AgentSqlContext,
1920
type AgentSqlExecutionResult,
21+
type AgentSqlMutationExecutionResult,
2022
type AgentSqlPayload,
2123
type AgentSqlSinglePayload,
2224
} from "./agentSql/shared";
@@ -85,15 +87,18 @@ function toStatementSqls(sql: string, statements: ReadonlyArray<ParsedSqlStateme
8587
}
8688

8789
/**
88-
* Single source of truth for the agent SQL result-size budget shared by both
89-
* the MCP surface (`sql_query` / `sql_execute`) and the REST surface
90-
* (`POST /agent/sql/query` / `POST /agent/sql/execute`).
90+
* Read-path result-size budget shared by the MCP surface (`sql_query`) and the
91+
* REST surface (`POST /agent/sql/query`).
9192
*
9293
* The agent envelope serializes `result.data`, so the budget is measured
9394
* against the serialized `data` payload. On overflow we fail with an actionable
9495
* error (matching the repo's "clear, actionable errors / no silent fallbacks"
9596
* principle) instead of returning a payload that exceeds the directory's
96-
* tool-result token limit. The remedies are concrete: narrow the result set.
97+
* tool-result token limit. Nothing is committed on a read, and the remedies are
98+
* concrete: narrow the result set.
99+
*
100+
* Writes must never reach this: their transaction is already committed when the
101+
* size is measured, so they drop rows instead (see the reducers below).
97102
*/
98103
function assertSqlResultWithinSizeBudget<T extends AgentSqlExecutionResult>(result: T): T {
99104
const serializedLength = JSON.stringify(result.data).length;
@@ -108,6 +113,57 @@ function assertSqlResultWithinSizeBudget<T extends AgentSqlExecutionResult>(resu
108113
return result;
109114
}
110115

116+
/**
117+
* Appended to the instructions of a committed write whose payload had to shrink,
118+
* so the model knows the rows are missing by design and how to get them.
119+
*/
120+
const OMITTED_MUTATION_ROWS_INSTRUCTION =
121+
"The affected rows were omitted from this result because the payload exceeded the result-size budget. The write itself succeeded, so do not repeat it: run a follow-up SELECT query if you need the affected rows.";
122+
123+
/**
124+
* Shrinks an oversized committed write result instead of rejecting it.
125+
*
126+
* `executeSqlMutationStatement` and `executeSqlMutationBatch` return after their
127+
* transaction committed, so answering with `QUERY_RESULT_TOO_LARGE` would report
128+
* a successful write as a failure and invite the caller to retry it and
129+
* duplicate the data. Only the returned rows are dropped; the counts, the echoed
130+
* SQL, and the atomicity contract stay intact.
131+
*/
132+
function reduceMutationResultToSizeBudget(
133+
result: AgentSqlMutationExecutionResult,
134+
): AgentSqlMutationExecutionResult {
135+
if (JSON.stringify(result.data).length <= MAX_SQL_RESULT_CHARS) {
136+
return result;
137+
}
138+
139+
return {
140+
data: {
141+
...result.data,
142+
rows: [],
143+
},
144+
instructions: `${result.instructions} ${OMITTED_MUTATION_ROWS_INSTRUCTION}`,
145+
};
146+
}
147+
148+
function reduceBatchMutationResultToSizeBudget(
149+
result: AgentSqlBatchExecutionResult,
150+
): AgentSqlBatchExecutionResult {
151+
if (JSON.stringify(result.data).length <= MAX_SQL_RESULT_CHARS) {
152+
return result;
153+
}
154+
155+
return {
156+
data: {
157+
...result.data,
158+
statements: result.data.statements.map((statement) => ({
159+
...statement,
160+
rows: [],
161+
})),
162+
},
163+
instructions: `${result.instructions} ${OMITTED_MUTATION_ROWS_INSTRUCTION}`,
164+
};
165+
}
166+
111167
function getAgentSqlFingerprint(sql: string): string {
112168
return createHash("sha256")
113169
.update(sql)
@@ -320,12 +376,12 @@ export async function runSqlExecute(
320376

321377
if (statements.every(isSqlMutationStatement)) {
322378
if (statements.length === 1) {
323-
return assertSqlResultWithinSizeBudget(
379+
return reduceMutationResultToSizeBudget(
324380
await executeSqlMutationStatement(dependencies, context, sql, statements[0]),
325381
);
326382
}
327383

328-
return assertSqlResultWithinSizeBudget(
384+
return reduceBatchMutationResultToSizeBudget(
329385
await executeSqlMutationBatch(dependencies, context, sql, statements, statementSqls),
330386
);
331387
}

apps/backend/src/aiTools/agentSql/batchMutation.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ import {
3131
buildCreateDeckInput,
3232
makeBatchNormalizedSql,
3333
requireSqlMutationTargetIds,
34+
toCardIdRows,
3435
toCardRow,
35-
toCreatedCardRows,
36-
toCreatedDeckRows,
36+
toDeckIdRows,
3737
toDeckRow,
3838
wrapBatchExecutionError,
39+
type AgentSqlBatchExecutionResult,
3940
type AgentSqlContext,
40-
type AgentSqlExecutionResult,
4141
type AgentSqlMutationAssignment,
4242
type AgentSqlMutationStatement,
4343
type AgentSqlSinglePayload,
@@ -233,7 +233,7 @@ export async function executeSqlMutationBatch(
233233
sql: string,
234234
statements: ReadonlyArray<AgentSqlMutationStatement>,
235235
statementSqls: ReadonlyArray<string>,
236-
): Promise<AgentSqlExecutionResult> {
236+
): Promise<AgentSqlBatchExecutionResult> {
237237
const replicaId = await dependencies.ensureAgentSyncReplica(
238238
context.workspaceId,
239239
context.userId,
@@ -273,9 +273,7 @@ export async function executeSqlMutationBatch(
273273
payloads.push({
274274
statementType: "insert",
275275
resource: "cards",
276-
sql: statementSql,
277-
normalizedSql: statement.normalizedSql,
278-
rows: toCreatedCardRows(createdCards),
276+
rows: toCardIdRows(createdCards),
279277
affectedCount: createdCards.length,
280278
});
281279
continue;
@@ -309,9 +307,7 @@ export async function executeSqlMutationBatch(
309307
payloads.push({
310308
statementType: "insert",
311309
resource: "decks",
312-
sql: statementSql,
313-
normalizedSql: statement.normalizedSql,
314-
rows: toCreatedDeckRows(createdDecks),
310+
rows: toDeckIdRows(createdDecks),
315311
affectedCount: createdDecks.length,
316312
});
317313
continue;
@@ -351,9 +347,7 @@ export async function executeSqlMutationBatch(
351347
payloads.push({
352348
statementType: "update",
353349
resource: "cards",
354-
sql: statementSql,
355-
normalizedSql: statement.normalizedSql,
356-
rows: toCreatedCardRows(updatedCards),
350+
rows: toCardIdRows(updatedCards),
357351
affectedCount: updatedCards.length,
358352
});
359353
continue;
@@ -385,9 +379,7 @@ export async function executeSqlMutationBatch(
385379
payloads.push({
386380
statementType: "update",
387381
resource: "decks",
388-
sql: statementSql,
389-
normalizedSql: statement.normalizedSql,
390-
rows: toCreatedDeckRows(updatedDecks),
382+
rows: toDeckIdRows(updatedDecks),
391383
affectedCount: updatedDecks.length,
392384
});
393385
continue;
@@ -411,8 +403,6 @@ export async function executeSqlMutationBatch(
411403
payloads.push({
412404
statementType: "delete",
413405
resource: "cards",
414-
sql: statementSql,
415-
normalizedSql: statement.normalizedSql,
416406
rows: [],
417407
affectedCount: targetIds.length,
418408
});
@@ -436,8 +426,6 @@ export async function executeSqlMutationBatch(
436426
payloads.push({
437427
statementType: "delete",
438428
resource: "decks",
439-
sql: statementSql,
440-
normalizedSql: statement.normalizedSql,
441429
rows: [],
442430
affectedCount: targetIds.length,
443431
});

apps/backend/src/aiTools/agentSql/readExecution.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
type AgentSqlExecutionResult,
2727
type AgentSqlReadExecutionResult,
2828
type AgentSqlReadStatement,
29+
type AgentSqlReadStatementPayload,
2930
type AgentSqlSinglePayload,
3031
} from "./shared";
3132

@@ -140,12 +141,19 @@ export async function loadSelectRows(
140141
return collectReviewEventRows(dependencies, context.userId, context.workspaceId);
141142
}
142143

143-
export async function executeSqlReadStatement(
144+
/**
145+
* Runs one read statement into a batch-entry payload, which carries no echo of
146+
* the submitted SQL. `executeSqlReadStatement` adds that echo for the
147+
* single-statement top-level payload.
148+
*/
149+
async function executeReadStatementPayload(
144150
dependencies: AgentToolOperationDependencies,
145151
context: AgentSqlContext,
146-
sql: string,
147152
statement: AgentSqlReadStatement,
148-
): Promise<AgentSqlReadExecutionResult> {
153+
): Promise<Readonly<{
154+
data: AgentSqlReadStatementPayload;
155+
instructions: string;
156+
}>> {
149157
if (statement.type === "show_tables") {
150158
const rows = getSqlResourceDescriptors()
151159
.filter((descriptor) => statement.likePattern === null || likePatternToRegExp(statement.likePattern).test(descriptor.resourceName))
@@ -159,8 +167,6 @@ export async function executeSqlReadStatement(
159167
data: {
160168
statementType: "show_tables",
161169
resource: null,
162-
sql,
163-
normalizedSql: statement.normalizedSql,
164170
rows,
165171
rowCount: rows.length,
166172
limit: null,
@@ -186,8 +192,6 @@ export async function executeSqlReadStatement(
186192
data: {
187193
statementType: "describe",
188194
resource: statement.resourceName,
189-
sql,
190-
normalizedSql: statement.normalizedSql,
191195
rows,
192196
rowCount: rows.length,
193197
limit: null,
@@ -204,8 +208,6 @@ export async function executeSqlReadStatement(
204208
data: {
205209
statementType: "select",
206210
resource: statement.source.resourceName,
207-
sql,
208-
normalizedSql: statement.normalizedSql,
209211
rows: result.rows,
210212
rowCount: result.rowCount,
211213
limit: result.limit,
@@ -216,6 +218,24 @@ export async function executeSqlReadStatement(
216218
};
217219
}
218220

221+
export async function executeSqlReadStatement(
222+
dependencies: AgentToolOperationDependencies,
223+
context: AgentSqlContext,
224+
sql: string,
225+
statement: AgentSqlReadStatement,
226+
): Promise<AgentSqlReadExecutionResult> {
227+
const result = await executeReadStatementPayload(dependencies, context, statement);
228+
229+
return {
230+
data: {
231+
...result.data,
232+
sql,
233+
normalizedSql: statement.normalizedSql,
234+
},
235+
instructions: result.instructions,
236+
};
237+
}
238+
219239
export async function executeSqlReadBatch(
220240
dependencies: AgentToolOperationDependencies,
221241
context: AgentSqlContext,
@@ -227,12 +247,7 @@ export async function executeSqlReadBatch(
227247

228248
for (const [index, statement] of statements.entries()) {
229249
try {
230-
const result = await executeSqlReadStatement(
231-
dependencies,
232-
context,
233-
statementSqls[index] ?? statement.normalizedSql,
234-
statement,
235-
);
250+
const result = await executeReadStatementPayload(dependencies, context, statement);
236251
payloads.push(result.data);
237252
} catch (error) {
238253
wrapBatchExecutionError(error, index, statementSqls[index] ?? statement.normalizedSql);

0 commit comments

Comments
 (0)