feat: add agent runtime skeleton
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
|
||||
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
|
||||
|
||||
export const AGENT_MGR_SERVICE_ID = "hwlab-agent-mgr";
|
||||
export const AGENT_WORKER_SERVICE_ID = "hwlab-agent-worker";
|
||||
export const AGENT_SKILLS_SERVICE_ID = "hwlab-agent-skills";
|
||||
|
||||
export const AGENT_SESSION_STATUSES = Object.freeze([
|
||||
"create",
|
||||
"start",
|
||||
"trace",
|
||||
"finish",
|
||||
"cleanup"
|
||||
]);
|
||||
|
||||
export const WORKER_SESSION_STATUSES = Object.freeze([
|
||||
"create",
|
||||
"start",
|
||||
"trace",
|
||||
"finish",
|
||||
"cleanup"
|
||||
]);
|
||||
|
||||
export function createAgentSession({
|
||||
agentSessionId,
|
||||
projectId,
|
||||
requestedBy = "usr_local",
|
||||
goal = "",
|
||||
environment = ENVIRONMENT_DEV,
|
||||
now = () => new Date().toISOString(),
|
||||
metadata = {}
|
||||
}) {
|
||||
const timestamp = now();
|
||||
return {
|
||||
agentSessionId,
|
||||
projectId,
|
||||
serviceId: AGENT_MGR_SERVICE_ID,
|
||||
status: "create",
|
||||
environment,
|
||||
requestedBy,
|
||||
goal,
|
||||
startedAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
metadata: {
|
||||
...metadata,
|
||||
lifecycle: AGENT_SESSION_STATUSES.slice(),
|
||||
currentState: "create"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function advanceAgentSession(session, nextStatus, { now = () => new Date().toISOString(), extra = {} } = {}) {
|
||||
assertAgentStatus(nextStatus);
|
||||
const timestamp = now();
|
||||
const completedAt = nextStatus === "finish" ? timestamp : session.completedAt;
|
||||
return {
|
||||
...session,
|
||||
status: nextStatus,
|
||||
completedAt,
|
||||
updatedAt: timestamp,
|
||||
metadata: {
|
||||
...session.metadata,
|
||||
...extra,
|
||||
currentState: nextStatus
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createWorkspaceVolume({ agentSessionId, projectId, now = () => new Date().toISOString() }) {
|
||||
return {
|
||||
workspaceVolumeId: `vol_${agentSessionId}`,
|
||||
agentSessionId,
|
||||
projectId,
|
||||
serviceId: AGENT_WORKER_SERVICE_ID,
|
||||
mountPath: path.posix.join("/workspace", projectId, agentSessionId),
|
||||
lifecycle: "create",
|
||||
createdAt: now(),
|
||||
updatedAt: now()
|
||||
};
|
||||
}
|
||||
|
||||
export function createWorkerSession({
|
||||
workerSessionId,
|
||||
agentSessionId,
|
||||
projectId,
|
||||
gatewaySessionId = null,
|
||||
now = () => new Date().toISOString(),
|
||||
metadata = {}
|
||||
}) {
|
||||
const timestamp = now();
|
||||
return {
|
||||
workerSessionId,
|
||||
agentSessionId,
|
||||
projectId,
|
||||
serviceId: AGENT_WORKER_SERVICE_ID,
|
||||
gatewaySessionId,
|
||||
status: "create",
|
||||
environment: ENVIRONMENT_DEV,
|
||||
startedAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
metadata: {
|
||||
...metadata,
|
||||
lifecycle: WORKER_SESSION_STATUSES.slice(),
|
||||
workspaceVolumeId: `vol_${agentSessionId}`,
|
||||
currentState: "create"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function advanceWorkerSession(session, nextStatus, { now = () => new Date().toISOString(), extra = {} } = {}) {
|
||||
assertWorkerStatus(nextStatus);
|
||||
const timestamp = now();
|
||||
const completedAt = nextStatus === "finish" ? timestamp : session.completedAt;
|
||||
return {
|
||||
...session,
|
||||
status: nextStatus,
|
||||
completedAt,
|
||||
updatedAt: timestamp,
|
||||
metadata: {
|
||||
...session.metadata,
|
||||
...extra,
|
||||
currentState: nextStatus
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createTraceEvent({
|
||||
traceEventId,
|
||||
traceId,
|
||||
projectId,
|
||||
serviceId = AGENT_MGR_SERVICE_ID,
|
||||
level = "info",
|
||||
message,
|
||||
environment = ENVIRONMENT_DEV,
|
||||
now = () => new Date().toISOString(),
|
||||
agentSessionId = null,
|
||||
workerSessionId = null,
|
||||
operationId = null,
|
||||
metadata = {}
|
||||
}) {
|
||||
return {
|
||||
traceEventId,
|
||||
traceId,
|
||||
projectId,
|
||||
serviceId,
|
||||
level,
|
||||
message,
|
||||
environment,
|
||||
occurredAt: now(),
|
||||
agentSessionId,
|
||||
workerSessionId,
|
||||
operationId,
|
||||
metadata: {
|
||||
...metadata
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createEvidenceRecord({
|
||||
evidenceId,
|
||||
projectId,
|
||||
operationId,
|
||||
uri,
|
||||
sha256 = digestHex(uri),
|
||||
serviceId = AGENT_WORKER_SERVICE_ID,
|
||||
environment = ENVIRONMENT_DEV,
|
||||
kind = "artifact",
|
||||
now = () => new Date().toISOString(),
|
||||
agentSessionId = null,
|
||||
workerSessionId = null,
|
||||
mimeType = "text/plain",
|
||||
sizeBytes = 0,
|
||||
metadata = {}
|
||||
}) {
|
||||
return {
|
||||
evidenceId,
|
||||
projectId,
|
||||
operationId,
|
||||
agentSessionId,
|
||||
workerSessionId,
|
||||
kind,
|
||||
uri,
|
||||
mimeType,
|
||||
sha256,
|
||||
sizeBytes,
|
||||
serviceId,
|
||||
environment,
|
||||
metadata: {
|
||||
...metadata
|
||||
},
|
||||
createdAt: now()
|
||||
};
|
||||
}
|
||||
|
||||
export function createAgentRunPlan({
|
||||
agentSessionId,
|
||||
projectId,
|
||||
goal = "",
|
||||
prompt = "",
|
||||
skillCommitId = null,
|
||||
now = () => new Date().toISOString()
|
||||
}) {
|
||||
const traceId = `trc_${agentSessionId}`;
|
||||
const workerSessionId = `wkr_${agentSessionId}`;
|
||||
const operationId = `op_${agentSessionId}`;
|
||||
const evidenceId = `evi_${agentSessionId}`;
|
||||
const traceEventId = `tev_${agentSessionId}`;
|
||||
const workspaceVolume = createWorkspaceVolume({ agentSessionId, projectId, now });
|
||||
const agentSession = createAgentSession({
|
||||
agentSessionId,
|
||||
projectId,
|
||||
goal,
|
||||
now
|
||||
});
|
||||
const workerSession = createWorkerSession({
|
||||
workerSessionId,
|
||||
agentSessionId,
|
||||
projectId,
|
||||
now
|
||||
});
|
||||
const traceEvent = createTraceEvent({
|
||||
traceEventId,
|
||||
traceId,
|
||||
projectId,
|
||||
message: prompt || goal || "agent runtime dry run",
|
||||
agentSessionId,
|
||||
workerSessionId,
|
||||
operationId,
|
||||
now
|
||||
});
|
||||
const evidenceRecord = createEvidenceRecord({
|
||||
evidenceId,
|
||||
projectId,
|
||||
operationId,
|
||||
uri: `file://${path.posix.join(workspaceVolume.mountPath, "evidence.txt")}`,
|
||||
agentSessionId,
|
||||
workerSessionId,
|
||||
metadata: {
|
||||
skillCommitId,
|
||||
prompt,
|
||||
goal,
|
||||
workspaceVolumeId: workspaceVolume.workspaceVolumeId
|
||||
},
|
||||
now
|
||||
});
|
||||
|
||||
return {
|
||||
agentSession: advanceAgentSession(agentSession, "start", {
|
||||
now,
|
||||
extra: {
|
||||
workerSessionId,
|
||||
workspaceVolumeId: workspaceVolume.workspaceVolumeId
|
||||
}
|
||||
}),
|
||||
workerSession: advanceWorkerSession(workerSession, "start", {
|
||||
now,
|
||||
extra: {
|
||||
workspaceVolumeId: workspaceVolume.workspaceVolumeId
|
||||
}
|
||||
}),
|
||||
workspaceVolume,
|
||||
events: [
|
||||
{ state: "create", actor: AGENT_MGR_SERVICE_ID, agentSessionId },
|
||||
{ state: "start", actor: AGENT_WORKER_SERVICE_ID, workerSessionId },
|
||||
{ state: "trace", actor: AGENT_MGR_SERVICE_ID, traceEvent },
|
||||
{ state: "finish", actor: AGENT_WORKER_SERVICE_ID, evidenceRecord },
|
||||
{ state: "cleanup", actor: AGENT_WORKER_SERVICE_ID, workspaceVolumeId: workspaceVolume.workspaceVolumeId }
|
||||
],
|
||||
traceEvent,
|
||||
evidenceRecord,
|
||||
skillCommitId
|
||||
};
|
||||
}
|
||||
|
||||
export function finalizeAgentRun(plan, { now = () => new Date().toISOString() } = {}) {
|
||||
const finishedAgentSession = advanceAgentSession(plan.agentSession, "finish", { now });
|
||||
const finishedWorkerSession = advanceWorkerSession(plan.workerSession, "finish", { now });
|
||||
return {
|
||||
...plan,
|
||||
agentSession: advanceAgentSession(finishedAgentSession, "cleanup", {
|
||||
now,
|
||||
extra: { cleanedUp: true }
|
||||
}),
|
||||
workerSession: advanceWorkerSession(finishedWorkerSession, "cleanup", {
|
||||
now,
|
||||
extra: { cleanedUp: true }
|
||||
}),
|
||||
cleanup: {
|
||||
workspaceVolumeId: plan.workspaceVolume.workspaceVolumeId,
|
||||
removed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createSkillsManifest({
|
||||
commitId,
|
||||
skillName = "hwlab-agent-runtime",
|
||||
serviceId = AGENT_SKILLS_SERVICE_ID,
|
||||
environment = ENVIRONMENT_DEV,
|
||||
skills = []
|
||||
}) {
|
||||
if (!commitId) {
|
||||
throw new Error("skills manifest requires explicit commitId");
|
||||
}
|
||||
return {
|
||||
manifestVersion: "v1",
|
||||
environment,
|
||||
commitId,
|
||||
namespace: "hwlab",
|
||||
endpoint: "http://74.48.78.17:6667",
|
||||
profiles: {
|
||||
dev: {
|
||||
name: "dev",
|
||||
enabled: true,
|
||||
namespace: "hwlab-dev",
|
||||
endpoint: "http://74.48.78.17:6667",
|
||||
notes: "explicit repo-local skills artifact"
|
||||
}
|
||||
},
|
||||
services: [
|
||||
{
|
||||
serviceId,
|
||||
image: `${skillName}:${commitId.slice(0, 7)}`,
|
||||
namespace: "hwlab-dev",
|
||||
healthPath: "/health",
|
||||
profile: "dev",
|
||||
env: {
|
||||
HWLAB_SKILLS_COMMIT_ID: commitId
|
||||
}
|
||||
}
|
||||
],
|
||||
skills
|
||||
};
|
||||
}
|
||||
|
||||
function assertAgentStatus(status) {
|
||||
if (!AGENT_SESSION_STATUSES.includes(status)) {
|
||||
throw new Error(`unknown agent session status ${JSON.stringify(status)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertWorkerStatus(status) {
|
||||
if (!WORKER_SESSION_STATUSES.includes(status)) {
|
||||
throw new Error(`unknown worker session status ${JSON.stringify(status)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function digestHex(value) {
|
||||
return crypto.createHash("sha256").update(String(value)).digest("hex");
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
AGENT_SESSION_STATUSES,
|
||||
WORKER_SESSION_STATUSES,
|
||||
advanceAgentSession,
|
||||
advanceWorkerSession,
|
||||
createAgentRunPlan,
|
||||
createSkillsManifest,
|
||||
finalizeAgentRun
|
||||
} from "./index.mjs";
|
||||
|
||||
test("agent runtime run plan exposes create/start/trace/finish/cleanup", () => {
|
||||
const plan = createAgentRunPlan({
|
||||
agentSessionId: "agt_01J00000000000000000000000",
|
||||
projectId: "prj_01J00000000000000000000000",
|
||||
goal: "verify runtime skeleton",
|
||||
prompt: "dry run"
|
||||
});
|
||||
|
||||
assert.deepEqual(AGENT_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]);
|
||||
assert.deepEqual(WORKER_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]);
|
||||
assert.equal(plan.agentSession.status, "start");
|
||||
assert.equal(plan.workerSession.status, "start");
|
||||
assert.equal(plan.events.map((event) => event.state).join(","), "create,start,trace,finish,cleanup");
|
||||
assert.equal(plan.traceEvent.message, "dry run");
|
||||
assert.equal(plan.evidenceRecord.projectId, "prj_01J00000000000000000000000");
|
||||
assert.equal(plan.evidenceRecord.metadata.workspaceVolumeId, "vol_agt_01J00000000000000000000000");
|
||||
});
|
||||
|
||||
test("finalizeAgentRun marks cleanup and keeps evidence visible", () => {
|
||||
const plan = createAgentRunPlan({
|
||||
agentSessionId: "agt_01J00000000000000000000001",
|
||||
projectId: "prj_01J00000000000000000000001",
|
||||
goal: "cleanup coverage"
|
||||
});
|
||||
const finished = finalizeAgentRun(plan);
|
||||
|
||||
assert.equal(finished.agentSession.status, "cleanup");
|
||||
assert.equal(finished.workerSession.status, "cleanup");
|
||||
assert.equal(finished.cleanup.removed, true);
|
||||
assert.ok(finished.evidenceRecord.sha256);
|
||||
});
|
||||
|
||||
test("skills manifest requires explicit commitId", () => {
|
||||
assert.throws(() => createSkillsManifest({}), /explicit commitId/);
|
||||
|
||||
const manifest = createSkillsManifest({ commitId: "6509a35" });
|
||||
assert.equal(manifest.commitId, "6509a35");
|
||||
assert.equal(manifest.services[0].serviceId, "hwlab-agent-skills");
|
||||
});
|
||||
|
||||
test("advance helpers preserve completedAt on non-finish states", () => {
|
||||
const session = {
|
||||
status: "start",
|
||||
completedAt: undefined,
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const agent = advanceAgentSession(session, "trace");
|
||||
const worker = advanceWorkerSession(session, "trace");
|
||||
|
||||
assert.equal(agent.status, "trace");
|
||||
assert.equal(worker.status, "trace");
|
||||
assert.equal(agent.completedAt, undefined);
|
||||
assert.equal(worker.completedAt, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user