Update formatting on worker
Build and Push Spoon Images / quality (push) Successful in 1m27s
Build and Push Spoon Images / build-images (push) Successful in 7m13s

This commit is contained in:
Gabriel Brown
2026-06-24 08:40:52 -04:00
parent 5f7d56369f
commit 65aae85369
11 changed files with 130 additions and 93 deletions
+27 -10
View File
@@ -71,7 +71,8 @@ const textFromPart = (part: Record<string, unknown>) => {
}; };
const commandString = (value: unknown) => { const commandString = (value: unknown) => {
if (Array.isArray(value)) return value.map((part) => stringify(part)).join(' '); if (Array.isArray(value))
return value.map((part) => stringify(part)).join(' ');
return stringify(value); return stringify(value);
}; };
@@ -82,8 +83,7 @@ const toolNameFromRecord = (record: Record<string, unknown> | null) =>
record?.toolName ?? record?.toolName ??
record?.name ?? record?.name ??
record?.function ?? record?.function ??
(stringify(record?.type).toLowerCase().includes('exec') || (stringify(record?.type).toLowerCase().includes('exec') || record?.command
record?.command
? 'Command' ? 'Command'
: record?.type) ?? : record?.type) ??
'tool', 'tool',
@@ -132,7 +132,9 @@ const recordLooksLikeTool = (
recordType.includes('exec_command') || recordType.includes('exec_command') ||
recordType.includes('command') || recordType.includes('command') ||
recordType.includes('mcp') || recordType.includes('mcp') ||
Boolean(record?.tool ?? record?.tool_name ?? record?.name ?? record?.command) Boolean(
record?.tool ?? record?.tool_name ?? record?.name ?? record?.command,
)
); );
}; };
@@ -153,7 +155,10 @@ const normalizeCodexMsgEvent = (
); );
if (sessionId) events.push({ kind: 'session', sessionId }); if (sessionId) events.push({ kind: 'session', sessionId });
} }
if (msgType === 'agent_message_delta' || msgType === 'agent_reasoning_delta') { if (
msgType === 'agent_message_delta' ||
msgType === 'agent_reasoning_delta'
) {
const delta = stringify(msg.delta ?? msg.text); const delta = stringify(msg.delta ?? msg.text);
if (delta) events.push({ kind: 'assistant_delta', content: delta }); if (delta) events.push({ kind: 'assistant_delta', content: delta });
} }
@@ -177,7 +182,11 @@ const normalizeCodexMsgEvent = (
output: toolOutputFromRecord(msg), output: toolOutputFromRecord(msg),
}); });
} }
if (msgType === 'error' || msgType === 'turn_failed' || msgType === 'task_error') { if (
msgType === 'error' ||
msgType === 'turn_failed' ||
msgType === 'task_error'
) {
const message = stringify(msg.message ?? msg.error ?? msg); const message = stringify(msg.message ?? msg.error ?? msg);
if (isCodexConfigWarning(message)) { if (isCodexConfigWarning(message)) {
events.push({ kind: 'status', status: message }); events.push({ kind: 'status', status: message });
@@ -354,7 +363,8 @@ export const normalizeOpenCodeEvent = (
const event = asRecord(input); const event = asRecord(input);
if (!event) return []; if (!event) return [];
const type = stringify(event.type); const type = stringify(event.type);
const properties = asRecord(event.properties) ?? asRecord(event.data) ?? event; const properties =
asRecord(event.properties) ?? asRecord(event.data) ?? event;
const events: NormalizedAgentEvent[] = []; const events: NormalizedAgentEvent[] = [];
const sessionId = properties.sessionID ?? properties.sessionId; const sessionId = properties.sessionID ?? properties.sessionId;
if (typeof sessionId === 'string' && type.includes('session')) { if (typeof sessionId === 'string' && type.includes('session')) {
@@ -408,7 +418,8 @@ export const normalizeOpenCodeEvent = (
} }
if (type === 'file.edited') { if (type === 'file.edited') {
const file = properties.file; const file = properties.file;
if (typeof file === 'string') events.push({ kind: 'file_edited', path: file }); if (typeof file === 'string')
events.push({ kind: 'file_edited', path: file });
} }
if (type === 'command.executed') { if (type === 'command.executed') {
events.push({ events.push({
@@ -422,7 +433,9 @@ export const normalizeOpenCodeEvent = (
kind: 'permission_requested', kind: 'permission_requested',
externalRequestId: stringify(properties.permissionID ?? properties.id), externalRequestId: stringify(properties.permissionID ?? properties.id),
title: 'Permission requested', title: 'Permission requested',
body: stringify(properties.permission ?? properties.message ?? properties), body: stringify(
properties.permission ?? properties.message ?? properties,
),
metadata: stringify(properties), metadata: stringify(properties),
}); });
} }
@@ -443,7 +456,11 @@ export const normalizeOpenCodeEvent = (
}); });
} }
if (events.length === 0 && type) { if (events.length === 0 && type) {
events.push({ kind: 'status', status: type, metadata: stringify(properties) }); events.push({
kind: 'status',
status: type,
metadata: stringify(properties),
});
} }
return events; return events;
}; };
+2 -1
View File
@@ -25,7 +25,8 @@ export const env = {
process.env.SPOON_AGENT_CONTAINER_RUNTIME?.trim() ?? process.env.SPOON_AGENT_CONTAINER_RUNTIME?.trim() ??
process.env.SPOON_CONTAINER_RUNTIME?.trim() ?? process.env.SPOON_CONTAINER_RUNTIME?.trim() ??
'docker', 'docker',
containerVolumeOptions: process.env.SPOON_AGENT_CONTAINER_VOLUME_OPTIONS?.trim(), containerVolumeOptions:
process.env.SPOON_AGENT_CONTAINER_VOLUME_OPTIONS?.trim(),
containerAccess: containerAccess:
process.env.SPOON_AGENT_CONTAINER_ACCESS?.trim() === 'host_port' process.env.SPOON_AGENT_CONTAINER_ACCESS?.trim() === 'host_port'
? 'host_port' ? 'host_port'
+1 -2
View File
@@ -155,8 +155,7 @@ export const getWorktreeDiff = async (
if (diff.output.trim()) untrackedDiffs.push(diff.output); if (diff.output.trim()) untrackedDiffs.push(diff.output);
} }
return { return {
exitCode: exitCode: trackedDiff.exitCode === 0 && untracked.exitCode === 0 ? 0 : 1,
trackedDiff.exitCode === 0 && untracked.exitCode === 0 ? 0 : 1,
output: [trackedDiff.output, ...untrackedDiffs] output: [trackedDiff.output, ...untrackedDiffs]
.filter((part) => part.trim()) .filter((part) => part.trim())
.join('\n'), .join('\n'),
+5 -3
View File
@@ -1,5 +1,5 @@
import { createOpencodeClient } from '@opencode-ai/sdk';
import type { OpencodeClient } from '@opencode-ai/sdk'; import type { OpencodeClient } from '@opencode-ai/sdk';
import { createOpencodeClient } from '@opencode-ai/sdk';
import type { NormalizedAgentEvent } from './agent-events'; import type { NormalizedAgentEvent } from './agent-events';
import { normalizeOpenCodeEvent } from './agent-events'; import { normalizeOpenCodeEvent } from './agent-events';
@@ -115,11 +115,13 @@ export const replyOpenCodePermission = async (args: {
response: 'once' | 'always' | 'reject'; response: 'once' | 'always' | 'reject';
directory: string; directory: string;
}) => { }) => {
const result = await args.session.client.postSessionIdPermissionsPermissionId({ const result = await args.session.client.postSessionIdPermissionsPermissionId(
{
path: { id: args.session.sessionId, permissionID: args.permissionId }, path: { id: args.session.sessionId, permissionID: args.permissionId },
query: { directory: args.directory }, query: { directory: args.directory },
body: { response: args.response }, body: { response: args.response },
}); },
);
if (result.error) { if (result.error) {
throw new Error('OpenCode permission response was rejected.'); throw new Error('OpenCode permission response was rejected.');
} }
+10 -17
View File
@@ -1,5 +1,5 @@
import { execa } from 'execa';
import path from 'node:path'; import path from 'node:path';
import { execa } from 'execa';
import { env } from '../env'; import { env } from '../env';
@@ -134,15 +134,9 @@ export const startWorkspaceContainer = async (args: {
publishTcpPort?: number; publishTcpPort?: number;
}) => { }) => {
await ensureJobImagePulled(); await ensureJobImagePulled();
await execa( await execa(containerRuntime(), ['rm', '-f', args.containerName], {
containerRuntime(), reject: false,
[ });
'rm',
'-f',
args.containerName,
],
{ reject: false },
);
const result = await execa( const result = await execa(
containerRuntime(), containerRuntime(),
[ [
@@ -177,7 +171,10 @@ export const startWorkspaceContainer = async (args: {
}; };
}; };
const getPublishedPort = async (containerName: string, containerPort: number) => { const getPublishedPort = async (
containerName: string,
containerPort: number,
) => {
const result = await execa( const result = await execa(
containerRuntime(), containerRuntime(),
['port', containerName, `${containerPort}/tcp`], ['port', containerName, `${containerPort}/tcp`],
@@ -317,14 +314,10 @@ export const stopWorkspaceContainer = async (containerName: string) => {
}; };
export const inspectWorkspaceContainer = async (containerName: string) => { export const inspectWorkspaceContainer = async (containerName: string) => {
const result = await execa( const result = await execa(containerRuntime(), ['inspect', containerName], {
containerRuntime(),
['inspect', containerName],
{
all: true, all: true,
reject: false, reject: false,
}, });
);
return { return {
exists: result.exitCode === 0, exists: result.exitCode === 0,
output: result.all, output: result.all,
+3 -2
View File
@@ -128,8 +128,9 @@ export const startWorkerServer = () => {
sendJson(response, 200, await abortWorkspaceAgent(route.jobId)); sendJson(response, 200, await abortWorkspaceAgent(route.jobId));
return; return;
} }
const interactionMatch = const interactionMatch = /^interactions\/([^/]+)\/reply$/.exec(
/^interactions\/([^/]+)\/reply$/.exec(route.action); route.action,
);
if (request.method === 'POST' && interactionMatch?.[1]) { if (request.method === 'POST' && interactionMatch?.[1]) {
const body = await parseJson<{ const body = await parseJson<{
externalRequestId?: string; externalRequestId?: string;
+2 -1
View File
@@ -1,7 +1,8 @@
import type { Server } from 'node:http'; import type { Server } from 'node:http';
import type { Duplex } from 'node:stream'; import type { Duplex } from 'node:stream';
import type { WebSocket } from 'ws';
import Docker from 'dockerode'; import Docker from 'dockerode';
import { WebSocketServer, type WebSocket } from 'ws'; import { WebSocketServer } from 'ws';
import { env } from './env'; import { env } from './env';
import { containerVolumeSuffix, hostWorkspacePath } from './runtime/docker'; import { containerVolumeSuffix, hostWorkspacePath } from './runtime/docker';
+25 -11
View File
@@ -1,3 +1,4 @@
import { randomBytes } from 'node:crypto';
import { import {
access, access,
mkdir, mkdir,
@@ -7,7 +8,6 @@ import {
stat, stat,
writeFile, writeFile,
} from 'node:fs/promises'; } from 'node:fs/promises';
import { randomBytes } from 'node:crypto';
import path from 'node:path'; import path from 'node:path';
import { ConvexHttpClient } from 'convex/browser'; import { ConvexHttpClient } from 'convex/browser';
@@ -15,6 +15,7 @@ import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
import { api } from '@spoon/backend/convex/_generated/api.js'; import { api } from '@spoon/backend/convex/_generated/api.js';
import type { NormalizedAgentEvent } from './agent-events'; import type { NormalizedAgentEvent } from './agent-events';
import type { OpenCodeSession } from './opencode-session';
import { normalizeCodexJsonLine } from './agent-events'; import { normalizeCodexJsonLine } from './agent-events';
import { import {
codexContainerRepo, codexContainerRepo,
@@ -30,7 +31,6 @@ import {
run, run,
} from './git'; } from './git';
import { getInstallationToken, openDraftPullRequest } from './github'; import { getInstallationToken, openDraftPullRequest } from './github';
import type { OpenCodeSession } from './opencode-session';
import { import {
abortOpenCodeSession, abortOpenCodeSession,
createOpenCodeSession, createOpenCodeSession,
@@ -432,7 +432,7 @@ const opencodeModel = (claim: Claim) => {
const codexModel = (claim: Claim) => { const codexModel = (claim: Claim) => {
const model = claim.aiProviderProfile?.model ?? claim.openai.model; const model = claim.aiProviderProfile?.model ?? claim.openai.model;
return model.includes('/') ? model.split('/').at(-1) ?? model : model; return model.includes('/') ? (model.split('/').at(-1) ?? model) : model;
}; };
const codexModelArgs = (claim: Claim) => const codexModelArgs = (claim: Claim) =>
@@ -531,8 +531,7 @@ const handleAgentEvent = async (args: {
return; return;
} }
if (event.kind === 'tool_started' || event.kind === 'tool_completed') { if (event.kind === 'tool_started' || event.kind === 'tool_completed') {
const detail = const detail = event.kind === 'tool_started' ? event.input : event.output;
event.kind === 'tool_started' ? event.input : event.output;
await appendMessage({ await appendMessage({
jobId, jobId,
role: 'tool', role: 'tool',
@@ -653,14 +652,17 @@ const ensureOpenCodeSession = async (workspace: ActiveWorkspace) => {
directory: '/workspace/repo', directory: '/workspace/repo',
title: workspace.claim.job.prompt.slice(0, 80) || 'Spoon workspace', title: workspace.claim.job.prompt.slice(0, 80) || 'Spoon workspace',
onEvent: async (event) => { onEvent: async (event) => {
const messageId = workspaceCurrentMessage.get(workspace.claim.job._id); const messageId = workspaceCurrentMessage.get(
workspace.claim.job._id,
);
if (!messageId) return; if (!messageId) return;
await handleAgentEvent({ await handleAgentEvent({
workspace, workspace,
event, event,
assistantMessageId: messageId, assistantMessageId: messageId,
assistantContent: assistantContent: workspaceCurrentContent.get(
workspaceCurrentContent.get(workspace.claim.job._id) ?? { workspace.claim.job._id,
) ?? {
value: '', value: '',
}, },
}); });
@@ -1491,7 +1493,12 @@ export const abortWorkspaceAgent = async (jobId: string) => {
workspace.agentTurnActive = false; workspace.agentTurnActive = false;
workspace.resolveTurn?.(); workspace.resolveTurn?.();
workspace.resolveTurn = undefined; workspace.resolveTurn = undefined;
await appendEvent(workspace.claim.job._id, 'warn', 'cleanup', 'Agent turn aborted.'); await appendEvent(
workspace.claim.job._id,
'warn',
'cleanup',
'Agent turn aborted.',
);
return { success: true }; return { success: true };
} }
if (workspace.runtimeMode === 'codex_exec') { if (workspace.runtimeMode === 'codex_exec') {
@@ -1605,7 +1612,13 @@ export const sendWorkspaceMessage = async (
const secretEnv = Object.fromEntries( const secretEnv = Object.fromEntries(
claim.secrets.map((secret) => [secret.name, secret.value]), claim.secrets.map((secret) => [secret.name, secret.value]),
); );
const result = await run('bash', ['-lc', `opencode run --format json --model ${quoteShell(opencodeModel(claim))} ${quoteShell(prompt)}`], { const result = await run(
'bash',
[
'-lc',
`opencode run --format json --model ${quoteShell(opencodeModel(claim))} ${quoteShell(prompt)}`,
],
{
cwd: workspace.repoDir, cwd: workspace.repoDir,
env: { env: {
...aiEnv, ...aiEnv,
@@ -1613,7 +1626,8 @@ export const sendWorkspaceMessage = async (
}, },
redact, redact,
timeoutMs: env.jobTimeoutMs, timeoutMs: env.jobTimeoutMs,
}); },
);
await updateMessage({ await updateMessage({
messageId: assistantMessageId, messageId: assistantMessageId,
status: result.exitCode === 0 ? 'completed' : 'failed', status: result.exitCode === 0 ? 'completed' : 'failed',
@@ -271,7 +271,8 @@ describe('agent event normalization', () => {
externalRequestId: 'perm-1', externalRequestId: 'perm-1',
title: 'Permission requested', title: 'Permission requested',
body: 'Run bun test?', body: 'Run bun test?',
metadata: '{\n "permissionID": "perm-1",\n "message": "Run bun test?"\n}', metadata:
'{\n "permissionID": "perm-1",\n "message": "Run bun test?"\n}',
}); });
expect( expect(
@@ -1,4 +1,11 @@
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import {
mkdir,
mkdtemp,
readFile,
rm,
stat,
writeFile,
} from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { afterEach, describe, expect, test } from 'vitest'; import { afterEach, describe, expect, test } from 'vitest';
@@ -3,7 +3,6 @@ import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
type TestWorkspace = { type TestWorkspace = {
@@ -53,7 +52,9 @@ const writeConfig = async (
config: Record<string, unknown> | string, config: Record<string, unknown> | string,
) => { ) => {
const content = const content =
typeof config === 'string' ? config : `${JSON.stringify(config, null, 2)}\n`; typeof config === 'string'
? config
: `${JSON.stringify(config, null, 2)}\n`;
await writeFile(configPath(workspace), content); await writeFile(configPath(workspace), content);
}; };