Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/execution-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Execution boundaries

Factorize separates an incoming event from the system that executes its task:

```text
provider webhook -> SourceAdapter -> WorkItem -> prompt renderer
|
v
Durable Object orchestrator -> ExecutionBackend -> external execution system
```

`SourceAdapter` owns provider payload normalization. It must not know about VMs,
Herdr, or a coding harness. `ExecutionBackend` owns launch, prompt delivery,
inspection, output, and stopping. The orchestrator owns durable queue, claim,
concurrency, and delivery state, but does not construct backend commands.

The current `ExeHerdrBackend` is one implementation. exe.dev supplies command
transport and VM wake-up, Herdr supplies workspace and agent supervision, and
the configured harness supplies Codex, Claude, Pi, or another Herdr-supported
agent. A future Amp implementation should implement `ExecutionBackend` without
adding Amp-specific state or commands to the orchestrator.

Launch and prompt delivery are deliberately separate operations. An existing
harness proves only that launch reconciliation succeeded. It never proves that
the run's prompt was accepted. Prompt delivery therefore has its own persisted
state and request/response receipt.
26 changes: 26 additions & 0 deletions src/exe-herdr-backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ExecutionBackend, LaunchRequest, PromptDeliveryReceipt, RunHandle } from "./execution";
import { agentOutputCommand, agentStatusCommand, exec, launchAgentCommand, promptAgentCommand, stopAgentCommand, type ExeConnection } from "./exe";

/** Execution adapter for the current exe.dev transport, Herdr supervisor, and configured harness. */
export class ExeHerdrBackend implements ExecutionBackend {
readonly kind = "exe-herdr";

constructor(private readonly connection: ExeConnection) {}

async launch(request: LaunchRequest) {
const command = await exec(this.connection, launchAgentCommand(request.agentName, this.connection, request.workspaceName, request.runPath, request.lease));
return { handle: { backend: this.kind, agentName: request.agentName }, command };
}

async deliverPrompt(handle: RunHandle, prompt: string): Promise<PromptDeliveryReceipt> {
const command = await exec(this.connection, promptAgentCommand(this.connection, handle.agentName, prompt));
const state = command.ok && (command.exitCode === null || command.exitCode === 0)
? "accepted"
: command.status >= 500 || command.exitCode === null ? "ambiguous" : "failed";
return { state, command };
}

inspect(handle: RunHandle) { return exec(this.connection, agentStatusCommand(this.connection, handle.agentName)); }
readOutput(handle: RunHandle) { return exec(this.connection, agentOutputCommand(this.connection, handle.agentName)); }
stop(handle: RunHandle) { return exec(this.connection, stopAgentCommand(this.connection, handle.agentName)); }
}
15 changes: 12 additions & 3 deletions src/exe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ export function shellAtom(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}

export function startAgentCommand(agentName: string, connection: ExeConnection, prompt: string, workspaceName: string, runPath: string, lease: string): string {
const encodedPrompt = base64(prompt);
export function launchAgentCommand(agentName: string, connection: ExeConnection, workspaceName: string, runPath: string, lease: string): string {
const name = agentName;
const herdr = herdrBinary(connection);
return [
Expand All @@ -56,10 +55,20 @@ export function startAgentCommand(agentName: string, connection: ExeConnection,
`workspace_id=$(printf '%s' "$workspaces" | jq -r --arg label ${shellAtom(workspaceName)} '.result.workspaces[]? | select(.label == $label) | .workspace_id' | head -n1)`,
`if [ -z "$workspace_id" ]; then created=$(${herdr} workspace create --cwd ${shellAtom(runPath)} --label ${shellAtom(workspaceName)} --no-focus) && workspace_id=$(printf '%s' "$created" | jq -er '.result.workspace.workspace_id') && tab_id=$(printf '%s' "$created" | jq -er '.result.tab.tab_id') && pane=$(printf '%s' "$created" | jq -er '.result.root_pane.pane_id') && ${herdr} tab rename "$tab_id" ${shellAtom(name)} >/dev/null; else tabs=$(${herdr} tab list --workspace "$workspace_id") && tab_id=$(printf '%s' "$tabs" | jq -r --arg label ${shellAtom(name)} '.result.tabs[]? | select(.label == $label) | .tab_id' | head -n1); if [ -z "$tab_id" ]; then created=$(${herdr} tab create --workspace "$workspace_id" --cwd ${shellAtom(runPath)} --label ${shellAtom(name)} --no-focus) && tab_id=$(printf '%s' "$created" | jq -er '.result.tab.tab_id') && pane=$(printf '%s' "$created" | jq -er '.result.root_pane.pane_id'); else panes=$(${herdr} pane list --workspace "$workspace_id") && pane=$(printf '%s' "$panes" | jq -r --arg tab "$tab_id" '.result.panes[]? | select(.tab_id == $tab) | .pane_id' | head -n1); test -n "$pane"; extras=$(printf '%s' "$panes" | jq -r --arg tab "$tab_id" --arg keep "$pane" '.result.panes[]? | select(.tab_id == $tab and .pane_id != $keep) | .pane_id'); for extra in $extras; do ${herdr} pane close "$extra" >/dev/null; done; fi; fi`,
`existing=$(${herdr} agent get ${shellAtom(name)} 2>/dev/null || true)`,
`if [ -n "$existing" ]; then printf '%s\\n' "$existing"; else ${herdr} agent start ${shellAtom(name)} --kind ${shellAtom(connection.agentKind)} --pane "$pane"${agentCommand(connection)} && prompt=$(printf '%s' ${shellAtom(encodedPrompt)} | base64 -d) && ${herdr} agent prompt ${shellAtom(name)} "$prompt" && ${herdr} agent get ${shellAtom(name)}; fi`,
`if [ -n "$existing" ]; then printf '%s\\n' "$existing"; else ${herdr} agent start ${shellAtom(name)} --kind ${shellAtom(connection.agentKind)} --pane "$pane"${agentCommand(connection)} && ${herdr} agent get ${shellAtom(name)}; fi`,
].join(" && ");
}

export function promptAgentCommand(connection: ExeConnection, agentName: string, prompt: string): string {
const encodedPrompt = base64(prompt), herdr = herdrBinary(connection);
return `${herdrPrefix(connection)} && prompt=$(printf '%s' ${shellAtom(encodedPrompt)} | base64 -d) && ${herdr} agent prompt ${shellAtom(agentName)} "$prompt"`;
}

/** Compatibility helper used by recovery paths; prompt delivery is never skipped. */
export function startAgentCommand(agentName: string, connection: ExeConnection, prompt: string, workspaceName: string, runPath: string, lease: string): string {
return `${launchAgentCommand(agentName, connection, workspaceName, runPath, lease)} && ${promptAgentCommand(connection, agentName, prompt)}`;
}

export function agentListCommand(connection: ExeConnection): string { return `${herdrPrefix(connection)} && ${herdrBinary(connection)} agent list`; }
export function paneGetCommand(connection: ExeConnection, paneId: string): string { return `${herdrPrefix(connection)} && ${herdrBinary(connection)} pane get ${shellAtom(paneId)}`; }
export function paneProcessInfoCommand(connection: ExeConnection, paneId: string): string { return `${herdrPrefix(connection)} && ${herdrBinary(connection)} pane process-info --pane ${shellAtom(paneId)}`; }
Expand Down
42 changes: 42 additions & 0 deletions src/execution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export type PromptDeliveryState = "pending" | "submitting" | "accepted" | "ambiguous" | "failed";

export interface BackendCommandResult {
ok: boolean;
status: number;
exitCode: number | null;
body: string;
requestBody: string;
}

export interface LaunchRequest {
runId: string;
agentName: string;
workspaceName: string;
runPath: string;
lease: string;
}

export interface RunHandle {
backend: string;
agentName: string;
}

export interface LaunchReceipt {
handle: RunHandle;
command: BackendCommandResult;
}

export interface PromptDeliveryReceipt {
state: Exclude<PromptDeliveryState, "pending" | "submitting">;
command: BackendCommandResult;
}

/** Boundary implemented by exe.dev + Herdr today and by alternative runners later. */
export interface ExecutionBackend {
readonly kind: string;
launch(request: LaunchRequest): Promise<LaunchReceipt>;
deliverPrompt(handle: RunHandle, prompt: string): Promise<PromptDeliveryReceipt>;
inspect(handle: RunHandle): Promise<BackendCommandResult>;
readOutput(handle: RunHandle): Promise<BackendCommandResult>;
stop(handle: RunHandle): Promise<BackendCommandResult>;
}
60 changes: 60 additions & 0 deletions src/linear-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Mustache from "mustache";
import type { WorkItem } from "./types";

const object = (value: unknown): Record<string, any> => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
const text = (value: unknown): string => typeof value === "string" ? value : "";

export const DEFAULT_CONTEXT_TEMPLATE = `---
pipe: "{{{flow.name}}}"
issue: "{{{ticket.identifier}}}"
url: "{{{ticket.url}}}"
{{#ticket.project.name}}project: "{{{ticket.project.name}}}"
{{/ticket.project.name}}{{#ticket.labels.length}}labels: [{{#ticket.labels}}"{{{name}}}"{{^last}}, {{/last}}{{/ticket.labels}}]
{{/ticket.labels.length}}{{#ticket.assignee.name}}owner: "{{{ticket.assignee.name}}}"
{{/ticket.assignee.name}}{{#ticket.state.name}}status: "{{{ticket.state.name}}}"
{{/ticket.state.name}}---

# {{{ticket.title}}}

{{{ticket.description}}}`;

export interface SourceAdapter<TPayload> {
toWorkItem(payload: TPayload, claimKey: string, event: Record<string, unknown>): WorkItem;
renderPrompt(template: string, payload: TPayload, flowName: string): string;
}

export class LinearSourceAdapter implements SourceAdapter<Record<string, any>> {
toWorkItem(payload: Record<string, any>, claimKey: string, event: Record<string, unknown>): WorkItem {
return {
provider: "linear", claimKey, identifier: claimKey,
title: String(payload.title ?? payload.issue?.title ?? ""),
description: String(payload.description ?? payload.issue?.description ?? ""),
url: linearIssueUrl(payload, claimKey), event,
};
}

renderPrompt(template: string, payload: Record<string, any>, flowName: string): string {
return renderContextTemplate(template, payload, flowName);
}
}

export function linearIssueUrl(data: Record<string, any>, issueId: string): string {
const candidate = typeof data.url === "string" ? data.url : typeof data.issue?.url === "string" ? data.issue.url : "";
return candidate.startsWith("https://linear.app/") ? candidate : `https://linear.app/issue/${encodeURIComponent(issueId)}`;
}

export function renderContextTemplate(template: string, payload: Record<string, unknown>, flowName: string): string {
const issue = object(payload.issue);
const ticket = text(issue.title) || text(issue.description) ? issue : payload;
const project = object(ticket.project), assignee = object(ticket.assignee), state = object(ticket.state), labelsValue = object(ticket.labels);
const rawLabels = Array.isArray(ticket.labels) ? ticket.labels : Array.isArray(labelsValue.nodes) ? labelsValue.nodes : [];
const labels = rawLabels.map((label) => text(object(label).name)).filter(Boolean);
const normalizedTicket = {
...ticket, id: text(ticket.id), identifier: text(ticket.identifier) || text(ticket.id), url: text(ticket.url),
title: text(ticket.title) || "Untitled Linear issue", description: text(ticket.description).trim() || "No description provided.",
project, assignee, state, labels: labels.map((name, index) => ({ name, last: index === labels.length - 1 })),
};
return Mustache.render(template || DEFAULT_CONTEXT_TEMPLATE, { ...payload, ticket: normalizedTicket, flow: { name: flowName } });
}

export const linearTicketPrompt = (payload: Record<string, unknown>, flowName: string) => renderContextTemplate(DEFAULT_CONTEXT_TEMPLATE, payload, flowName);
Loading