Files
zopu-code/packages/agents/src/orb/project-events.ts
-Puter 50ccf4e418 feat(agents/orb): wire project-manager orchestration over merged Orb runtime
Add a thin orchestration layer that drives the existing Orb runtime to
progress issue-scoped work. OrbProjectManager creates/resumes Orb runs,
assembles context packs, projects OrbEvents into durable project events,
detects needs-input and work-complete conditions, forwards follow-up
messages, and runs the Git/Gitea lifecycle on completion.

Port interfaces (OrbAdapter, OrbRunPort, GitLifecyclePort) make the
orchestrator testable with fakes. The real adapters wrap OrbRuntime and
runPostRunGiteaLifecycle without reimplementing Docker, AgentOS, or
OpenCode.

Idempotency: duplicate starts reuse active runs, duplicate completion
returns cached results, cancel is idempotent. Never auto-merges.

88 tests pass (19 orchestration + 12 event-mapping + orb suite), 2 live
tests skipped; lint and type checks clean.
2026-07-25 01:53:16 +05:30

172 lines
4.8 KiB
TypeScript

import { Schema } from "effect";
import type { OrbEvent } from "./events";
// ---------------------------------------------------------------------------
// Durable project events — human-meaningful projections of Orb execution
//
// These are NOT raw OpenCode events. Each variant maps to a product-level
// concept the UI and work-graph understand. Raw OrbEvents are preserved for
// audit; this is the durable projection layer.
// ---------------------------------------------------------------------------
export const ProjectRunEventVariant = Schema.Literals([
"run.started",
"run.repository_prepared",
"run.session_opened",
"run.agent_message",
"run.agent_progress",
"run.command_executed",
"run.needs_input",
"run.permission_requested",
"run.completed",
"run.failed",
"run.cancelled",
"run.session_closed",
]);
export type ProjectRunEventVariant = typeof ProjectRunEventVariant.Type;
export const ProjectRunEvent = Schema.Struct({
exitCode: Schema.UndefinedOr(Schema.Int),
issueId: Schema.String,
runId: Schema.String,
sequence: Schema.Number,
text: Schema.UndefinedOr(Schema.String),
timestamp: Schema.String,
toolName: Schema.UndefinedOr(Schema.String),
type: ProjectRunEventVariant,
});
export type ProjectRunEvent = typeof ProjectRunEvent.Type;
// ---------------------------------------------------------------------------
// Marker detection — OpenCode signals product-level conditions via markers
// ---------------------------------------------------------------------------
/** Prefix the agent emits when it cannot proceed without human input. */
export const NEEDS_INPUT_MARKER = "NEEDS_INPUT:";
/**
* Prefix the agent emits when implementation and verification are complete
* and the orchestrator should proceed to the Git publish lifecycle.
*/
export const WORK_COMPLETE_MARKER = "WORK_COMPLETE:";
export const extractNeedsInputQuestion = (text: string): string | undefined => {
const index = text.indexOf(NEEDS_INPUT_MARKER);
if (index === -1) {
return undefined;
}
const after = text.slice(index + NEEDS_INPUT_MARKER.length).trim();
return after.length > 0 ? after.slice(0, 4000) : "Agent requires input";
};
export const isWorkComplete = (text: string): boolean =>
text.includes(WORK_COMPLETE_MARKER);
// ---------------------------------------------------------------------------
// OrbEvent → ProjectRunEvent translation
// ---------------------------------------------------------------------------
/**
* Translate one normalized OrbEvent into zero or one durable ProjectRunEvent.
* Returns undefined for OrbEvents that have no product-meaningful projection
* (e.g. chunked intermediate output that is only useful in the raw audit log).
*/
export const mapOrbEvent = (
orbEvent: OrbEvent,
issueId: string,
runId: string
): ProjectRunEvent | undefined => {
const base: {
exitCode: number | undefined;
issueId: string;
runId: string;
sequence: number;
text: string | undefined;
timestamp: string;
toolName: string | undefined;
} = {
exitCode: undefined,
issueId,
runId,
sequence: orbEvent.sequence,
text: undefined,
timestamp: orbEvent.timestamp,
toolName: undefined,
};
switch (orbEvent.type) {
case "session_opened": {
return {
...base,
text: orbEvent.text ?? "Session opened",
type: "run.session_opened",
};
}
case "vm_booted": {
return undefined;
}
case "agent_message_completed": {
const text = orbEvent.text ?? "";
if (extractNeedsInputQuestion(text) !== undefined) {
return {
...base,
text: extractNeedsInputQuestion(text),
type: "run.needs_input",
};
}
return {
...base,
text,
type: "run.agent_message",
};
}
case "agent_message_chunk":
case "agent_thought_chunk": {
return undefined;
}
case "tool_call_started":
case "tool_call_completed": {
return {
...base,
text: undefined,
toolName: orbEvent.toolName,
type: "run.agent_progress",
};
}
case "command_executed": {
return {
...base,
exitCode: orbEvent.exitCode,
text: orbEvent.command,
type: "run.command_executed",
};
}
case "permission_requested":
case "permission_denied": {
return {
...base,
text: orbEvent.text ?? "Permission requested",
type: "run.permission_requested",
};
}
case "session_failed": {
return {
...base,
text: orbEvent.text ?? "Session failed",
type: "run.failed",
};
}
case "session_closed": {
return {
...base,
text: orbEvent.text ?? "Session closed",
type: "run.session_closed",
};
}
default: {
return undefined;
}
}
};