mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
Trim issue run polling payloads
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
06223c78b7
commit
07c2ea3421
2 files changed
+186
-2
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { agents, companies, createDb, heartbeatRuns } from "@paperclipai/db";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
startEmbeddedPostgresTestDatabase,
|
||||
} from "./helpers/embedded-postgres.js";
|
||||
import { activityService } from "../services/activity.ts";
|
||||
|
||||
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||
|
||||
if (!embeddedPostgresSupport.supported) {
|
||||
console.warn(
|
||||
`Skipping embedded Postgres activity service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||
);
|
||||
}
|
||||
|
||||
describeEmbeddedPostgres("activity service", () => {
|
||||
let db!: ReturnType<typeof createDb>;
|
||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-activity-service-");
|
||||
db = createDb(tempDb.connectionString);
|
||||
}, 20_000);
|
||||
|
||||
afterEach(async () => {
|
||||
await db.delete(heartbeatRuns);
|
||||
await db.delete(agents);
|
||||
await db.delete(companies);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
it("returns compact usage and result summaries for issue runs", async () => {
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "running",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: runId,
|
||||
companyId,
|
||||
agentId,
|
||||
invocationSource: "assignment",
|
||||
status: "succeeded",
|
||||
contextSnapshot: { issueId },
|
||||
usageJson: {
|
||||
inputTokens: 11,
|
||||
output_tokens: 7,
|
||||
cache_read_input_tokens: 3,
|
||||
billingType: "metered",
|
||||
costUsd: 0.42,
|
||||
enormousBlob: "x".repeat(256_000),
|
||||
},
|
||||
resultJson: {
|
||||
billing_type: "metered",
|
||||
total_cost_usd: 0.42,
|
||||
summary: "done",
|
||||
nestedHuge: { payload: "y".repeat(256_000) },
|
||||
},
|
||||
});
|
||||
|
||||
const runs = await activityService(db).runsForIssue(companyId, issueId);
|
||||
|
||||
expect(runs).toHaveLength(1);
|
||||
expect(runs[0]).toMatchObject({
|
||||
runId,
|
||||
agentId,
|
||||
invocationSource: "assignment",
|
||||
});
|
||||
expect(runs[0]?.usageJson).toEqual({
|
||||
inputTokens: 11,
|
||||
input_tokens: 11,
|
||||
outputTokens: 7,
|
||||
output_tokens: 7,
|
||||
cachedInputTokens: 3,
|
||||
cached_input_tokens: 3,
|
||||
cache_read_input_tokens: 3,
|
||||
billingType: "metered",
|
||||
billing_type: "metered",
|
||||
costUsd: 0.42,
|
||||
cost_usd: 0.42,
|
||||
total_cost_usd: 0.42,
|
||||
});
|
||||
expect(runs[0]?.resultJson).toEqual({
|
||||
billingType: "metered",
|
||||
billing_type: "metered",
|
||||
costUsd: 0.42,
|
||||
cost_usd: 0.42,
|
||||
total_cost_usd: 0.42,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,74 @@ export interface ActivityFilters {
|
||||
|
||||
export function activityService(db: Db) {
|
||||
const issueIdAsText = sql<string>`${issues.id}::text`;
|
||||
const summarizedUsageJson = sql<Record<string, unknown> | null>`
|
||||
case
|
||||
when ${heartbeatRuns.usageJson} is null then null
|
||||
else jsonb_strip_nulls(jsonb_build_object(
|
||||
'inputTokens', coalesce(${heartbeatRuns.usageJson} -> 'inputTokens', ${heartbeatRuns.usageJson} -> 'input_tokens'),
|
||||
'input_tokens', coalesce(${heartbeatRuns.usageJson} -> 'input_tokens', ${heartbeatRuns.usageJson} -> 'inputTokens'),
|
||||
'outputTokens', coalesce(${heartbeatRuns.usageJson} -> 'outputTokens', ${heartbeatRuns.usageJson} -> 'output_tokens'),
|
||||
'output_tokens', coalesce(${heartbeatRuns.usageJson} -> 'output_tokens', ${heartbeatRuns.usageJson} -> 'outputTokens'),
|
||||
'cachedInputTokens', coalesce(
|
||||
${heartbeatRuns.usageJson} -> 'cachedInputTokens',
|
||||
${heartbeatRuns.usageJson} -> 'cached_input_tokens',
|
||||
${heartbeatRuns.usageJson} -> 'cache_read_input_tokens'
|
||||
),
|
||||
'cached_input_tokens', coalesce(
|
||||
${heartbeatRuns.usageJson} -> 'cached_input_tokens',
|
||||
${heartbeatRuns.usageJson} -> 'cachedInputTokens',
|
||||
${heartbeatRuns.usageJson} -> 'cache_read_input_tokens'
|
||||
),
|
||||
'cache_read_input_tokens', coalesce(
|
||||
${heartbeatRuns.usageJson} -> 'cache_read_input_tokens',
|
||||
${heartbeatRuns.usageJson} -> 'cached_input_tokens',
|
||||
${heartbeatRuns.usageJson} -> 'cachedInputTokens'
|
||||
),
|
||||
'billingType', coalesce(${heartbeatRuns.usageJson} -> 'billingType', ${heartbeatRuns.usageJson} -> 'billing_type'),
|
||||
'billing_type', coalesce(${heartbeatRuns.usageJson} -> 'billing_type', ${heartbeatRuns.usageJson} -> 'billingType'),
|
||||
'costUsd', coalesce(
|
||||
${heartbeatRuns.usageJson} -> 'costUsd',
|
||||
${heartbeatRuns.usageJson} -> 'cost_usd',
|
||||
${heartbeatRuns.usageJson} -> 'total_cost_usd'
|
||||
),
|
||||
'cost_usd', coalesce(
|
||||
${heartbeatRuns.usageJson} -> 'cost_usd',
|
||||
${heartbeatRuns.usageJson} -> 'costUsd',
|
||||
${heartbeatRuns.usageJson} -> 'total_cost_usd'
|
||||
),
|
||||
'total_cost_usd', coalesce(
|
||||
${heartbeatRuns.usageJson} -> 'total_cost_usd',
|
||||
${heartbeatRuns.usageJson} -> 'cost_usd',
|
||||
${heartbeatRuns.usageJson} -> 'costUsd'
|
||||
)
|
||||
))
|
||||
end
|
||||
`.as("usageJson");
|
||||
const summarizedResultJson = sql<Record<string, unknown> | null>`
|
||||
case
|
||||
when ${heartbeatRuns.resultJson} is null then null
|
||||
else jsonb_strip_nulls(jsonb_build_object(
|
||||
'billingType', coalesce(${heartbeatRuns.resultJson} -> 'billingType', ${heartbeatRuns.resultJson} -> 'billing_type'),
|
||||
'billing_type', coalesce(${heartbeatRuns.resultJson} -> 'billing_type', ${heartbeatRuns.resultJson} -> 'billingType'),
|
||||
'costUsd', coalesce(
|
||||
${heartbeatRuns.resultJson} -> 'costUsd',
|
||||
${heartbeatRuns.resultJson} -> 'cost_usd',
|
||||
${heartbeatRuns.resultJson} -> 'total_cost_usd'
|
||||
),
|
||||
'cost_usd', coalesce(
|
||||
${heartbeatRuns.resultJson} -> 'cost_usd',
|
||||
${heartbeatRuns.resultJson} -> 'costUsd',
|
||||
${heartbeatRuns.resultJson} -> 'total_cost_usd'
|
||||
),
|
||||
'total_cost_usd', coalesce(
|
||||
${heartbeatRuns.resultJson} -> 'total_cost_usd',
|
||||
${heartbeatRuns.resultJson} -> 'cost_usd',
|
||||
${heartbeatRuns.resultJson} -> 'costUsd'
|
||||
)
|
||||
))
|
||||
end
|
||||
`.as("resultJson");
|
||||
|
||||
return {
|
||||
list: (filters: ActivityFilters) => {
|
||||
const conditions = [eq(activityLog.companyId, filters.companyId)];
|
||||
@@ -71,8 +139,8 @@ export function activityService(db: Db) {
|
||||
finishedAt: heartbeatRuns.finishedAt,
|
||||
createdAt: heartbeatRuns.createdAt,
|
||||
invocationSource: heartbeatRuns.invocationSource,
|
||||
usageJson: heartbeatRuns.usageJson,
|
||||
resultJson: heartbeatRuns.resultJson,
|
||||
usageJson: summarizedUsageJson,
|
||||
resultJson: summarizedResultJson,
|
||||
logBytes: heartbeatRuns.logBytes,
|
||||
})
|
||||
.from(heartbeatRuns)
|
||||
|
||||
Reference in new issue
Block a user