feat: add agent runtime skeleton
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { createAgentRunPlan, createSkillsManifest, finalizeAgentRun } from "../../internal/agent/index.mjs";
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {
|
||||||
|
agentSessionId: "agt_local",
|
||||||
|
projectId: "prj_local",
|
||||||
|
goal: "dry-run agent runtime skeleton",
|
||||||
|
prompt: "dry-run",
|
||||||
|
skillCommitId: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const arg = argv[i];
|
||||||
|
if (arg === "--agent-session-id") args.agentSessionId = argv[++i] || args.agentSessionId;
|
||||||
|
else if (arg === "--project-id") args.projectId = argv[++i] || args.projectId;
|
||||||
|
else if (arg === "--goal") args.goal = argv[++i] || args.goal;
|
||||||
|
else if (arg === "--prompt") args.prompt = argv[++i] || args.prompt;
|
||||||
|
else if (arg === "--skill-commit-id") args.skillCommitId = argv[++i] || args.skillCommitId;
|
||||||
|
else if (arg === "--help" || arg === "-h") args.help = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log("usage: node cmd/hwlab-agent-mgr/main.mjs [--agent-session-id ID] [--project-id ID] [--goal TEXT] [--prompt TEXT] [--skill-commit-id COMMIT]");
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
|
||||||
|
if (args.help) {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = createAgentRunPlan({
|
||||||
|
agentSessionId: args.agentSessionId,
|
||||||
|
projectId: args.projectId,
|
||||||
|
goal: args.goal,
|
||||||
|
prompt: args.prompt,
|
||||||
|
skillCommitId: args.skillCommitId || null
|
||||||
|
});
|
||||||
|
const finished = finalizeAgentRun(plan);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
serviceId: "hwlab-agent-mgr",
|
||||||
|
dryRun: true,
|
||||||
|
agentSession: finished.agentSession,
|
||||||
|
workerSession: finished.workerSession,
|
||||||
|
workspaceVolume: finished.workspaceVolume,
|
||||||
|
traceEvent: finished.traceEvent,
|
||||||
|
evidenceRecord: finished.evidenceRecord,
|
||||||
|
cleanup: finished.cleanup,
|
||||||
|
skillsManifest: args.skillCommitId ? createSkillsManifest({ commitId: args.skillCommitId }) : null
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { createAgentRunPlan, finalizeAgentRun } from "../../internal/agent/index.mjs";
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {
|
||||||
|
agentSessionId: process.env.HWLAB_AGENT_SESSION_ID || "agt_local",
|
||||||
|
projectId: process.env.HWLAB_PROJECT_ID || "prj_local",
|
||||||
|
prompt: "worker dry-run",
|
||||||
|
goal: "agent worker session skeleton",
|
||||||
|
skillCommitId: process.env.HWLAB_SKILL_COMMIT_ID || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const arg = argv[i];
|
||||||
|
if (arg === "--agent-session-id") args.agentSessionId = argv[++i] || args.agentSessionId;
|
||||||
|
else if (arg === "--project-id") args.projectId = argv[++i] || args.projectId;
|
||||||
|
else if (arg === "--prompt") args.prompt = argv[++i] || args.prompt;
|
||||||
|
else if (arg === "--goal") args.goal = argv[++i] || args.goal;
|
||||||
|
else if (arg === "--skill-commit-id") args.skillCommitId = argv[++i] || args.skillCommitId;
|
||||||
|
else if (arg === "--help" || arg === "-h") args.help = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log("usage: node cmd/hwlab-agent-worker/main.mjs [--agent-session-id ID] [--project-id ID] [--goal TEXT] [--prompt TEXT] [--skill-commit-id COMMIT]");
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
|
||||||
|
if (args.help) {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = finalizeAgentRun(
|
||||||
|
createAgentRunPlan({
|
||||||
|
agentSessionId: args.agentSessionId,
|
||||||
|
projectId: args.projectId,
|
||||||
|
goal: args.goal,
|
||||||
|
prompt: args.prompt,
|
||||||
|
skillCommitId: args.skillCommitId || null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
serviceId: "hwlab-agent-worker",
|
||||||
|
dryRun: true,
|
||||||
|
agentSession: plan.agentSession,
|
||||||
|
workerSession: plan.workerSession,
|
||||||
|
workspaceVolume: plan.workspaceVolume,
|
||||||
|
traceEvent: plan.traceEvent,
|
||||||
|
evidenceRecord: plan.evidenceRecord,
|
||||||
|
cleanup: plan.cleanup
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"manifestVersion": "v1",
|
||||||
|
"environment": "dev",
|
||||||
|
"commitId": "6509a35",
|
||||||
|
"namespace": "hwlab-dev",
|
||||||
|
"endpoint": "http://74.48.78.17:6667",
|
||||||
|
"profiles": {
|
||||||
|
"dev": {
|
||||||
|
"name": "dev",
|
||||||
|
"enabled": true,
|
||||||
|
"namespace": "hwlab-dev",
|
||||||
|
"endpoint": "http://74.48.78.17:6667",
|
||||||
|
"notes": "HWLAB dev deploy manifest with explicit commitId for repo-local skills artifact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"serviceId": "hwlab-agent-mgr",
|
||||||
|
"image": "hwlab-agent-mgr:6509a35",
|
||||||
|
"namespace": "hwlab-dev",
|
||||||
|
"healthPath": "/health",
|
||||||
|
"profile": "dev",
|
||||||
|
"replicas": 1,
|
||||||
|
"env": {
|
||||||
|
"HWLAB_AGENT_SESSION_MODE": "control-plane"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serviceId": "hwlab-agent-worker",
|
||||||
|
"image": "hwlab-agent-worker:6509a35",
|
||||||
|
"namespace": "hwlab-dev",
|
||||||
|
"healthPath": "/health",
|
||||||
|
"profile": "dev",
|
||||||
|
"replicas": 0,
|
||||||
|
"env": {
|
||||||
|
"HWLAB_AGENT_SESSION_MODE": "session-scoped"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serviceId": "hwlab-agent-skills",
|
||||||
|
"image": "hwlab-agent-skills:6509a35",
|
||||||
|
"namespace": "hwlab-dev",
|
||||||
|
"healthPath": "/health",
|
||||||
|
"profile": "dev",
|
||||||
|
"replicas": 1,
|
||||||
|
"env": {
|
||||||
|
"HWLAB_SKILLS_COMMIT_ID": "6509a35"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"validate": "node scripts/validate-contract.mjs",
|
"validate": "node scripts/validate-contract.mjs",
|
||||||
"check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs",
|
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs && node --test internal/agent/index.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
|
||||||
"cli:health": "node tools/hwlab-cli/bin/hwlab-cli.mjs health",
|
"cli:health": "node tools/hwlab-cli/bin/hwlab-cli.mjs health",
|
||||||
"cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
|
"cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
|
||||||
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
|
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
|
||||||
|
|||||||
@@ -2,6 +2,40 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
mkdir -p "$repo_root/skills"
|
commit_id="${1:-}"
|
||||||
|
|
||||||
echo "HWLAB skills directory ready: $repo_root/skills"
|
mkdir -p "$repo_root/skills/hwlab-agent-runtime/references"
|
||||||
|
mkdir -p "$repo_root/skills/hwlab-agent-runtime/assets"
|
||||||
|
|
||||||
|
cat > "$repo_root/skills/hwlab-agent-runtime/SKILL.md" <<EOF
|
||||||
|
---
|
||||||
|
name: hwlab-agent-runtime
|
||||||
|
description: Build and validate the HWLAB agent-mgr and agent-worker runtime skeleton, including session lifecycle, workspace volume bookkeeping, trace events, evidence records, and explicit skills commitId handling.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HWLAB Agent Runtime
|
||||||
|
|
||||||
|
Use this skill when working on HWLAB agent runtime skeletons.
|
||||||
|
|
||||||
|
- Keep the control plane long-lived and worker execution session-scoped.
|
||||||
|
- Model the lifecycle as create, start, trace, finish, cleanup.
|
||||||
|
- Always carry explicit evidence record fields through the run plan.
|
||||||
|
- Use repo-local skills artifacts only when a commitId is supplied explicitly.
|
||||||
|
- Do not infer or fall back to external skill sources.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$repo_root/skills/hwlab-agent-runtime/references/runtime.md" <<EOF
|
||||||
|
# Runtime Notes
|
||||||
|
|
||||||
|
- agent-mgr owns session creation and trace emission.
|
||||||
|
- agent-worker owns worker session execution and cleanup.
|
||||||
|
- Each agent session gets its own workspace volume identity.
|
||||||
|
- Evidence records must expose projectId, operationId, kind, uri, sha256, serviceId, environment, and createdAt.
|
||||||
|
- Skills deployment artifacts require an explicit commitId.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -n "$commit_id" ]; then
|
||||||
|
printf '%s\n' "$commit_id" > "$repo_root/skills/hwlab-agent-runtime/.commit-id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "HWLAB skills directory ready: $repo_root/skills/hwlab-agent-runtime"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
async function parseJSONFiles(dir) {
|
async function parseJSONFiles(dir, { requireSchema = true } = {}) {
|
||||||
const entries = await readdir(path.join(repoRoot, dir), { withFileTypes: true });
|
const entries = await readdir(path.join(repoRoot, dir), { withFileTypes: true });
|
||||||
const parsed = [];
|
const parsed = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -23,13 +23,36 @@ async function parseJSONFiles(dir) {
|
|||||||
const relativePath = path.join(dir, entry.name);
|
const relativePath = path.join(dir, entry.name);
|
||||||
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
||||||
const doc = JSON.parse(raw);
|
const doc = JSON.parse(raw);
|
||||||
assert.ok(doc.$schema, `${relativePath} missing $schema`);
|
if (requireSchema) {
|
||||||
assert.ok(doc.$id, `${relativePath} missing $id`);
|
assert.ok(doc.$schema, `${relativePath} missing $schema`);
|
||||||
|
assert.ok(doc.$id, `${relativePath} missing $id`);
|
||||||
|
}
|
||||||
parsed.push({ relativePath, doc });
|
parsed.push({ relativePath, doc });
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function parseJSONSchemaFiles(dir) {
|
||||||
|
const parsed = [];
|
||||||
|
for (const item of await parseJSONFiles(dir, { requireSchema: false })) {
|
||||||
|
if (!item.doc.$schema || !item.doc.$id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parsed.push(item);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseDeployManifest(relativePath) {
|
||||||
|
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
||||||
|
const doc = JSON.parse(raw);
|
||||||
|
assert.equal(doc.manifestVersion, "v1", `${relativePath} manifestVersion`);
|
||||||
|
assert.equal(doc.environment, ENVIRONMENT_DEV, `${relativePath} environment`);
|
||||||
|
assert.ok(doc.commitId, `${relativePath} commitId`);
|
||||||
|
assert.ok(Array.isArray(doc.services) && doc.services.length >= 1, `${relativePath} services`);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
function assertUnique(name, values) {
|
function assertUnique(name, values) {
|
||||||
assert.equal(new Set(values).size, values.length, `${name} must be unique`);
|
assert.equal(new Set(values).size, values.length, `${name} must be unique`);
|
||||||
}
|
}
|
||||||
@@ -91,8 +114,9 @@ function assertEnvelopeValidation() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemas = await parseJSONFiles("protocol/schemas");
|
const schemas = await parseJSONSchemaFiles("protocol/schemas");
|
||||||
const deploySchemas = await parseJSONFiles("deploy");
|
const deploySchemas = await parseJSONSchemaFiles("deploy");
|
||||||
|
const deployManifest = await parseDeployManifest("deploy/deploy.json");
|
||||||
const docsByName = new Map([...schemas, ...deploySchemas].map((item) => [path.basename(item.relativePath), item.doc]));
|
const docsByName = new Map([...schemas, ...deploySchemas].map((item) => [path.basename(item.relativePath), item.doc]));
|
||||||
|
|
||||||
assert.ok(schemas.length >= 13, "expected protocol schemas");
|
assert.ok(schemas.length >= 13, "expected protocol schemas");
|
||||||
@@ -102,5 +126,8 @@ assertUnique("tables", TABLES);
|
|||||||
assertCommonSchema(docsByName.get("common.json"));
|
assertCommonSchema(docsByName.get("common.json"));
|
||||||
assertDeploySchema(docsByName.get("deploy.schema.json"));
|
assertDeploySchema(docsByName.get("deploy.schema.json"));
|
||||||
assertEnvelopeValidation();
|
assertEnvelopeValidation();
|
||||||
|
assert.equal(deployManifest.services.some((service) => service.serviceId === "hwlab-agent-mgr"), true);
|
||||||
|
assert.equal(deployManifest.services.some((service) => service.serviceId === "hwlab-agent-worker"), true);
|
||||||
|
assert.equal(deployManifest.services.some((service) => service.serviceId === "hwlab-agent-skills"), true);
|
||||||
|
|
||||||
console.log(`validated ${schemas.length + deploySchemas.length} JSON files and ${SERVICE_IDS.length} service ids`);
|
console.log(`validated ${schemas.length + deploySchemas.length} JSON files and ${SERVICE_IDS.length} service ids`);
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
echo "HWLAB worker entrypoint placeholder"
|
repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
workspace_root="${HWLAB_WORKSPACE_ROOT:-/workspace}"
|
||||||
|
agent_session_id="${HWLAB_AGENT_SESSION_ID:-agent_local}"
|
||||||
|
project_id="${HWLAB_PROJECT_ID:-prj_local}"
|
||||||
|
skill_commit_id="${HWLAB_SKILL_COMMIT_ID:-}"
|
||||||
|
|
||||||
|
mkdir -p "$workspace_root"
|
||||||
|
|
||||||
|
echo "HWLAB worker starting"
|
||||||
|
echo "repo=$repo_root"
|
||||||
|
echo "workspace=$workspace_root"
|
||||||
|
echo "agentSessionId=$agent_session_id"
|
||||||
|
echo "projectId=$project_id"
|
||||||
|
if [ -n "$skill_commit_id" ]; then
|
||||||
|
echo "skillsCommitId=$skill_commit_id"
|
||||||
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: hwlab-agent-runtime
|
||||||
|
description: Build and validate the HWLAB agent-mgr and agent-worker runtime skeleton, including session lifecycle, workspace volume bookkeeping, trace events, evidence records, and explicit skills commitId handling.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HWLAB Agent Runtime
|
||||||
|
|
||||||
|
Use this skill when working on HWLAB agent runtime skeletons.
|
||||||
|
|
||||||
|
- Keep the control plane long-lived and worker execution session-scoped.
|
||||||
|
- Model the lifecycle as create, start, trace, finish, cleanup.
|
||||||
|
- Always carry explicit evidence record fields through the run plan.
|
||||||
|
- Use repo-local skills artifacts only when a commitId is supplied explicitly.
|
||||||
|
- Do not infer or fall back to external skill sources.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Runtime Notes
|
||||||
|
|
||||||
|
- agent-mgr owns session creation and trace emission.
|
||||||
|
- agent-worker owns worker session execution and cleanup.
|
||||||
|
- Each agent session gets its own workspace volume identity.
|
||||||
|
- Evidence records must expose projectId, operationId, kind, uri, sha256, serviceId, environment, and createdAt.
|
||||||
|
- Skills deployment artifacts require an explicit commitId.
|
||||||
Reference in New Issue
Block a user