Update formatting on worker
This commit is contained in:
@@ -71,7 +71,8 @@ const textFromPart = (part: Record<string, 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);
|
||||
};
|
||||
|
||||
@@ -82,8 +83,7 @@ const toolNameFromRecord = (record: Record<string, unknown> | null) =>
|
||||
record?.toolName ??
|
||||
record?.name ??
|
||||
record?.function ??
|
||||
(stringify(record?.type).toLowerCase().includes('exec') ||
|
||||
record?.command
|
||||
(stringify(record?.type).toLowerCase().includes('exec') || record?.command
|
||||
? 'Command'
|
||||
: record?.type) ??
|
||||
'tool',
|
||||
@@ -132,7 +132,9 @@ const recordLooksLikeTool = (
|
||||
recordType.includes('exec_command') ||
|
||||
recordType.includes('command') ||
|
||||
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 (msgType === 'agent_message_delta' || msgType === 'agent_reasoning_delta') {
|
||||
if (
|
||||
msgType === 'agent_message_delta' ||
|
||||
msgType === 'agent_reasoning_delta'
|
||||
) {
|
||||
const delta = stringify(msg.delta ?? msg.text);
|
||||
if (delta) events.push({ kind: 'assistant_delta', content: delta });
|
||||
}
|
||||
@@ -177,7 +182,11 @@ const normalizeCodexMsgEvent = (
|
||||
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);
|
||||
if (isCodexConfigWarning(message)) {
|
||||
events.push({ kind: 'status', status: message });
|
||||
@@ -354,7 +363,8 @@ export const normalizeOpenCodeEvent = (
|
||||
const event = asRecord(input);
|
||||
if (!event) return [];
|
||||
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 sessionId = properties.sessionID ?? properties.sessionId;
|
||||
if (typeof sessionId === 'string' && type.includes('session')) {
|
||||
@@ -408,7 +418,8 @@ export const normalizeOpenCodeEvent = (
|
||||
}
|
||||
if (type === 'file.edited') {
|
||||
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') {
|
||||
events.push({
|
||||
@@ -422,7 +433,9 @@ export const normalizeOpenCodeEvent = (
|
||||
kind: 'permission_requested',
|
||||
externalRequestId: stringify(properties.permissionID ?? properties.id),
|
||||
title: 'Permission requested',
|
||||
body: stringify(properties.permission ?? properties.message ?? properties),
|
||||
body: stringify(
|
||||
properties.permission ?? properties.message ?? properties,
|
||||
),
|
||||
metadata: stringify(properties),
|
||||
});
|
||||
}
|
||||
@@ -443,7 +456,11 @@ export const normalizeOpenCodeEvent = (
|
||||
});
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,8 @@ export const env = {
|
||||
process.env.SPOON_AGENT_CONTAINER_RUNTIME?.trim() ??
|
||||
process.env.SPOON_CONTAINER_RUNTIME?.trim() ??
|
||||
'docker',
|
||||
containerVolumeOptions: process.env.SPOON_AGENT_CONTAINER_VOLUME_OPTIONS?.trim(),
|
||||
containerVolumeOptions:
|
||||
process.env.SPOON_AGENT_CONTAINER_VOLUME_OPTIONS?.trim(),
|
||||
containerAccess:
|
||||
process.env.SPOON_AGENT_CONTAINER_ACCESS?.trim() === 'host_port'
|
||||
? 'host_port'
|
||||
|
||||
@@ -155,8 +155,7 @@ export const getWorktreeDiff = async (
|
||||
if (diff.output.trim()) untrackedDiffs.push(diff.output);
|
||||
}
|
||||
return {
|
||||
exitCode:
|
||||
trackedDiff.exitCode === 0 && untracked.exitCode === 0 ? 0 : 1,
|
||||
exitCode: trackedDiff.exitCode === 0 && untracked.exitCode === 0 ? 0 : 1,
|
||||
output: [trackedDiff.output, ...untrackedDiffs]
|
||||
.filter((part) => part.trim())
|
||||
.join('\n'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createOpencodeClient } from '@opencode-ai/sdk';
|
||||
import type { OpencodeClient } from '@opencode-ai/sdk';
|
||||
import { createOpencodeClient } from '@opencode-ai/sdk';
|
||||
|
||||
import type { NormalizedAgentEvent } from './agent-events';
|
||||
import { normalizeOpenCodeEvent } from './agent-events';
|
||||
@@ -115,11 +115,13 @@ export const replyOpenCodePermission = async (args: {
|
||||
response: 'once' | 'always' | 'reject';
|
||||
directory: string;
|
||||
}) => {
|
||||
const result = await args.session.client.postSessionIdPermissionsPermissionId({
|
||||
path: { id: args.session.sessionId, permissionID: args.permissionId },
|
||||
query: { directory: args.directory },
|
||||
body: { response: args.response },
|
||||
});
|
||||
const result = await args.session.client.postSessionIdPermissionsPermissionId(
|
||||
{
|
||||
path: { id: args.session.sessionId, permissionID: args.permissionId },
|
||||
query: { directory: args.directory },
|
||||
body: { response: args.response },
|
||||
},
|
||||
);
|
||||
if (result.error) {
|
||||
throw new Error('OpenCode permission response was rejected.');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execa } from 'execa';
|
||||
import path from 'node:path';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { env } from '../env';
|
||||
|
||||
@@ -134,15 +134,9 @@ export const startWorkspaceContainer = async (args: {
|
||||
publishTcpPort?: number;
|
||||
}) => {
|
||||
await ensureJobImagePulled();
|
||||
await execa(
|
||||
containerRuntime(),
|
||||
[
|
||||
'rm',
|
||||
'-f',
|
||||
args.containerName,
|
||||
],
|
||||
{ reject: false },
|
||||
);
|
||||
await execa(containerRuntime(), ['rm', '-f', args.containerName], {
|
||||
reject: false,
|
||||
});
|
||||
const result = await execa(
|
||||
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(
|
||||
containerRuntime(),
|
||||
['port', containerName, `${containerPort}/tcp`],
|
||||
@@ -317,14 +314,10 @@ export const stopWorkspaceContainer = async (containerName: string) => {
|
||||
};
|
||||
|
||||
export const inspectWorkspaceContainer = async (containerName: string) => {
|
||||
const result = await execa(
|
||||
containerRuntime(),
|
||||
['inspect', containerName],
|
||||
{
|
||||
all: true,
|
||||
reject: false,
|
||||
},
|
||||
);
|
||||
const result = await execa(containerRuntime(), ['inspect', containerName], {
|
||||
all: true,
|
||||
reject: false,
|
||||
});
|
||||
return {
|
||||
exists: result.exitCode === 0,
|
||||
output: result.all,
|
||||
|
||||
@@ -128,8 +128,9 @@ export const startWorkerServer = () => {
|
||||
sendJson(response, 200, await abortWorkspaceAgent(route.jobId));
|
||||
return;
|
||||
}
|
||||
const interactionMatch =
|
||||
/^interactions\/([^/]+)\/reply$/.exec(route.action);
|
||||
const interactionMatch = /^interactions\/([^/]+)\/reply$/.exec(
|
||||
route.action,
|
||||
);
|
||||
if (request.method === 'POST' && interactionMatch?.[1]) {
|
||||
const body = await parseJson<{
|
||||
externalRequestId?: string;
|
||||
@@ -166,21 +167,21 @@ export const startWorkerServer = () => {
|
||||
return;
|
||||
}
|
||||
sendJson(response, 404, { error: 'Not found' });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`Worker HTTP ${request.method ?? 'UNKNOWN'} ${request.url ?? '/'} failed: ${message}`,
|
||||
);
|
||||
const status =
|
||||
message === 'Unauthorized'
|
||||
? 401
|
||||
: message.includes('not supported')
|
||||
? 409
|
||||
: 500;
|
||||
sendJson(response, status, {
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
const status =
|
||||
message === 'Unauthorized'
|
||||
? 401
|
||||
: message.includes('not supported')
|
||||
? 409
|
||||
: 500;
|
||||
sendJson(response, status, {
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
})();
|
||||
});
|
||||
attachTerminalServer(server);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Server } from 'node:http';
|
||||
import type { Duplex } from 'node:stream';
|
||||
import type { WebSocket } from 'ws';
|
||||
import Docker from 'dockerode';
|
||||
import { WebSocketServer, type WebSocket } from 'ws';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
import { env } from './env';
|
||||
import { containerVolumeSuffix, hostWorkspacePath } from './runtime/docker';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
stat,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
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 type { NormalizedAgentEvent } from './agent-events';
|
||||
import type { OpenCodeSession } from './opencode-session';
|
||||
import { normalizeCodexJsonLine } from './agent-events';
|
||||
import {
|
||||
codexContainerRepo,
|
||||
@@ -30,7 +31,6 @@ import {
|
||||
run,
|
||||
} from './git';
|
||||
import { getInstallationToken, openDraftPullRequest } from './github';
|
||||
import type { OpenCodeSession } from './opencode-session';
|
||||
import {
|
||||
abortOpenCodeSession,
|
||||
createOpenCodeSession,
|
||||
@@ -432,7 +432,7 @@ const opencodeModel = (claim: Claim) => {
|
||||
|
||||
const codexModel = (claim: Claim) => {
|
||||
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) =>
|
||||
@@ -531,8 +531,7 @@ const handleAgentEvent = async (args: {
|
||||
return;
|
||||
}
|
||||
if (event.kind === 'tool_started' || event.kind === 'tool_completed') {
|
||||
const detail =
|
||||
event.kind === 'tool_started' ? event.input : event.output;
|
||||
const detail = event.kind === 'tool_started' ? event.input : event.output;
|
||||
await appendMessage({
|
||||
jobId,
|
||||
role: 'tool',
|
||||
@@ -647,22 +646,25 @@ const ensureOpenCodeSession = async (workspace: ActiveWorkspace) => {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
try {
|
||||
const session = await createOpenCodeSession({
|
||||
baseUrl,
|
||||
const session = await createOpenCodeSession({
|
||||
baseUrl,
|
||||
password,
|
||||
directory: '/workspace/repo',
|
||||
title: workspace.claim.job.prompt.slice(0, 80) || 'Spoon workspace',
|
||||
onEvent: async (event) => {
|
||||
const messageId = workspaceCurrentMessage.get(workspace.claim.job._id);
|
||||
const messageId = workspaceCurrentMessage.get(
|
||||
workspace.claim.job._id,
|
||||
);
|
||||
if (!messageId) return;
|
||||
await handleAgentEvent({
|
||||
workspace,
|
||||
event,
|
||||
assistantMessageId: messageId,
|
||||
assistantContent:
|
||||
workspaceCurrentContent.get(workspace.claim.job._id) ?? {
|
||||
value: '',
|
||||
},
|
||||
assistantContent: workspaceCurrentContent.get(
|
||||
workspace.claim.job._id,
|
||||
) ?? {
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1338,8 +1340,8 @@ const runClaim = async (claim: Claim) => {
|
||||
await sendWorkspaceMessage(jobId, systemPromptForJob(claim), {
|
||||
recordUserMessage: false,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await appendEvent(
|
||||
jobId,
|
||||
'error',
|
||||
@@ -1491,7 +1493,12 @@ export const abortWorkspaceAgent = async (jobId: string) => {
|
||||
workspace.agentTurnActive = false;
|
||||
workspace.resolveTurn?.();
|
||||
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 };
|
||||
}
|
||||
if (workspace.runtimeMode === 'codex_exec') {
|
||||
@@ -1605,15 +1612,22 @@ export const sendWorkspaceMessage = async (
|
||||
const secretEnv = Object.fromEntries(
|
||||
claim.secrets.map((secret) => [secret.name, secret.value]),
|
||||
);
|
||||
const result = await run('bash', ['-lc', `opencode run --format json --model ${quoteShell(opencodeModel(claim))} ${quoteShell(prompt)}`], {
|
||||
cwd: workspace.repoDir,
|
||||
env: {
|
||||
...aiEnv,
|
||||
...secretEnv,
|
||||
const result = await run(
|
||||
'bash',
|
||||
[
|
||||
'-lc',
|
||||
`opencode run --format json --model ${quoteShell(opencodeModel(claim))} ${quoteShell(prompt)}`,
|
||||
],
|
||||
{
|
||||
cwd: workspace.repoDir,
|
||||
env: {
|
||||
...aiEnv,
|
||||
...secretEnv,
|
||||
},
|
||||
redact,
|
||||
timeoutMs: env.jobTimeoutMs,
|
||||
},
|
||||
redact,
|
||||
timeoutMs: env.jobTimeoutMs,
|
||||
});
|
||||
);
|
||||
await updateMessage({
|
||||
messageId: assistantMessageId,
|
||||
status: result.exitCode === 0 ? 'completed' : 'failed',
|
||||
@@ -1634,15 +1648,15 @@ export const sendWorkspaceMessage = async (
|
||||
: 'Codex completed without producing an assistant response.',
|
||||
);
|
||||
}
|
||||
await updateMessage({
|
||||
messageId: assistantMessageId,
|
||||
await updateMessage({
|
||||
messageId: assistantMessageId,
|
||||
status: 'completed',
|
||||
content: assistantContent.value,
|
||||
});
|
||||
workspace.agentTurnActive = false;
|
||||
}
|
||||
workspace.agentTurnActive = false;
|
||||
if (claim.job.jobType === 'maintenance_review') {
|
||||
});
|
||||
workspace.agentTurnActive = false;
|
||||
}
|
||||
workspace.agentTurnActive = false;
|
||||
if (claim.job.jobType === 'maintenance_review') {
|
||||
const decision = parseMaintenanceDecision(assistantContent.value);
|
||||
if (decision) {
|
||||
await addArtifact({
|
||||
@@ -1660,7 +1674,7 @@ export const sendWorkspaceMessage = async (
|
||||
});
|
||||
}
|
||||
}
|
||||
const diff = await getWorktreeDiff(workspace.repoDir, redact);
|
||||
const diff = await getWorktreeDiff(workspace.repoDir, redact);
|
||||
await addArtifact({
|
||||
jobId: claim.job._id,
|
||||
kind: 'diff',
|
||||
@@ -1669,11 +1683,11 @@ export const sendWorkspaceMessage = async (
|
||||
contentType: 'text/x-diff',
|
||||
});
|
||||
await recordChangedFiles(workspace, 'agent', diff.output);
|
||||
} catch (error) {
|
||||
workspace.agentTurnActive = false;
|
||||
workspace.resolveTurn?.();
|
||||
workspace.resolveTurn = undefined;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
} catch (error) {
|
||||
workspace.agentTurnActive = false;
|
||||
workspace.resolveTurn?.();
|
||||
workspace.resolveTurn = undefined;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Agent turn failed for job ${claim.job._id}: ${message}`);
|
||||
await appendEvent(
|
||||
claim.job._id,
|
||||
|
||||
@@ -271,7 +271,8 @@ describe('agent event normalization', () => {
|
||||
externalRequestId: 'perm-1',
|
||||
title: 'Permission requested',
|
||||
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(
|
||||
|
||||
@@ -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 path from 'node:path';
|
||||
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 path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
type TestWorkspace = {
|
||||
@@ -53,7 +52,9 @@ const writeConfig = async (
|
||||
config: Record<string, unknown> | string,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user