feat: add durable AgentOS slice execution
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./agent-os": "./src/agent-os.ts",
|
||||
"./execution-runtime": "./src/execution-runtime.ts",
|
||||
"./git": "./src/git.ts",
|
||||
"./git-local-runtime": "./src/git-local-runtime.ts",
|
||||
"./git-remote-runtime": "./src/git-remote-runtime.ts",
|
||||
|
||||
38
packages/primitives/src/execution-runtime.test.ts
Normal file
38
packages/primitives/src/execution-runtime.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Effect } from "effect";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
decodeGitConnectionInput,
|
||||
decodeWorkAttemptExecutionResult,
|
||||
} from "./execution-runtime";
|
||||
|
||||
describe("execution runtime contracts", () => {
|
||||
test("accepts one authenticated Gitea connection", async () => {
|
||||
const decoded = await Effect.runPromise(
|
||||
decodeGitConnectionInput({
|
||||
credential: "secret",
|
||||
credentialKind: "token",
|
||||
provider: "gitea",
|
||||
serverUrl: "https://git.example.com",
|
||||
username: "zopu",
|
||||
})
|
||||
);
|
||||
expect(decoded.provider).toBe("gitea");
|
||||
});
|
||||
|
||||
test("requires exact base and candidate revisions", async () => {
|
||||
await expect(
|
||||
Effect.runPromise(
|
||||
decodeWorkAttemptExecutionResult({
|
||||
baseRevision: "",
|
||||
candidateRevision: "abc123",
|
||||
changedFiles: [],
|
||||
diff: "",
|
||||
environmentId: "workspace-1",
|
||||
events: [],
|
||||
summary: "done",
|
||||
})
|
||||
)
|
||||
).rejects.toMatchObject({ reason: "InvalidInput" });
|
||||
});
|
||||
});
|
||||
155
packages/primitives/src/execution-runtime.ts
Normal file
155
packages/primitives/src/execution-runtime.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/* eslint-disable max-classes-per-file -- execution boundary errors live with their schemas. */
|
||||
import { Effect, Schema } from "effect";
|
||||
|
||||
const Text = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
})
|
||||
);
|
||||
|
||||
const HttpUrl = Schema.String.check(
|
||||
Schema.makeFilter(
|
||||
(value) => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ expected: "an HTTP URL" }
|
||||
)
|
||||
);
|
||||
|
||||
export const GitProvider = Schema.Literals(["github", "gitea"]);
|
||||
export type GitProvider = typeof GitProvider.Type;
|
||||
|
||||
export const GitCredentialKind = Schema.Literals(["oauth", "token"]);
|
||||
export type GitCredentialKind = typeof GitCredentialKind.Type;
|
||||
|
||||
export const GitConnectionInput = Schema.Struct({
|
||||
credential: Text,
|
||||
credentialKind: GitCredentialKind,
|
||||
provider: GitProvider,
|
||||
serverUrl: HttpUrl,
|
||||
username: Schema.optional(Text),
|
||||
});
|
||||
export type GitConnectionInput = typeof GitConnectionInput.Type;
|
||||
|
||||
export const GitConnectionView = Schema.Struct({
|
||||
connectedAt: Schema.Number,
|
||||
credentialKind: GitCredentialKind,
|
||||
id: Text,
|
||||
provider: GitProvider,
|
||||
serverUrl: HttpUrl,
|
||||
username: Schema.optional(Text),
|
||||
});
|
||||
export type GitConnectionView = typeof GitConnectionView.Type;
|
||||
|
||||
export const RepositoryExecutionAuth = Schema.Struct({
|
||||
credential: Text,
|
||||
provider: GitProvider,
|
||||
serverUrl: HttpUrl,
|
||||
username: Schema.optional(Text),
|
||||
});
|
||||
export type RepositoryExecutionAuth = typeof RepositoryExecutionAuth.Type;
|
||||
|
||||
export const ExecutionEventKind = Schema.Literals([
|
||||
"runtime.preparing",
|
||||
"repository.cloning",
|
||||
"repository.ready",
|
||||
"harness.started",
|
||||
"harness.progress",
|
||||
"harness.log",
|
||||
"repository.changed",
|
||||
"runtime.completed",
|
||||
"runtime.failed",
|
||||
"runtime.cancelled",
|
||||
]);
|
||||
export type ExecutionEventKind = typeof ExecutionEventKind.Type;
|
||||
|
||||
export const ExecutionEvent = Schema.Struct({
|
||||
kind: ExecutionEventKind,
|
||||
message: Text,
|
||||
metadata: Schema.Record(Schema.String, Schema.String),
|
||||
occurredAt: Schema.Number,
|
||||
sequence: Schema.Int,
|
||||
});
|
||||
export type ExecutionEvent = typeof ExecutionEvent.Type;
|
||||
|
||||
export const WorkAttemptExecutionInput = Schema.Struct({
|
||||
attemptId: Text,
|
||||
auth: RepositoryExecutionAuth,
|
||||
baseBranch: Text,
|
||||
prompt: Text,
|
||||
repositoryUrl: HttpUrl,
|
||||
runId: Text,
|
||||
workId: Text,
|
||||
workspaceKey: Text,
|
||||
});
|
||||
export type WorkAttemptExecutionInput = typeof WorkAttemptExecutionInput.Type;
|
||||
|
||||
export const WorkAttemptExecutionResult = Schema.Struct({
|
||||
baseRevision: Text,
|
||||
candidateRevision: Text,
|
||||
changedFiles: Schema.Array(Text),
|
||||
diff: Schema.String,
|
||||
environmentId: Text,
|
||||
events: Schema.Array(ExecutionEvent),
|
||||
summary: Text,
|
||||
});
|
||||
export type WorkAttemptExecutionResult = typeof WorkAttemptExecutionResult.Type;
|
||||
|
||||
export const WorkAttemptExecutionErrorReason = Schema.Literals([
|
||||
"Authentication",
|
||||
"Cancelled",
|
||||
"HarnessFailed",
|
||||
"InvalidInput",
|
||||
"ProviderUnavailable",
|
||||
"RepositoryFailed",
|
||||
"Timeout",
|
||||
]);
|
||||
export type WorkAttemptExecutionErrorReason =
|
||||
typeof WorkAttemptExecutionErrorReason.Type;
|
||||
|
||||
export class WorkAttemptExecutionError extends Schema.TaggedErrorClass<WorkAttemptExecutionError>()(
|
||||
"WorkAttemptExecutionError",
|
||||
{
|
||||
message: Schema.String,
|
||||
reason: WorkAttemptExecutionErrorReason,
|
||||
retryable: Schema.Boolean,
|
||||
}
|
||||
) {}
|
||||
|
||||
const invalidInput = (message: string) =>
|
||||
new WorkAttemptExecutionError({
|
||||
message,
|
||||
reason: "InvalidInput",
|
||||
retryable: false,
|
||||
});
|
||||
|
||||
export const decodeGitConnectionInput = (input: unknown) =>
|
||||
Schema.decodeUnknownEffect(GitConnectionInput)(input).pipe(
|
||||
Effect.mapError((cause) => invalidInput(cause.message))
|
||||
);
|
||||
|
||||
export const decodeWorkAttemptExecutionInput = (input: unknown) =>
|
||||
Schema.decodeUnknownEffect(WorkAttemptExecutionInput)(input).pipe(
|
||||
Effect.mapError((cause) => invalidInput(cause.message))
|
||||
);
|
||||
|
||||
export const decodeWorkAttemptExecutionResult = (input: unknown) =>
|
||||
Schema.decodeUnknownEffect(WorkAttemptExecutionResult)(input).pipe(
|
||||
Effect.mapError((cause) => invalidInput(cause.message))
|
||||
);
|
||||
|
||||
export interface SandboxRuntime {
|
||||
readonly name: string;
|
||||
readonly execute: (
|
||||
input: WorkAttemptExecutionInput,
|
||||
signal?: AbortSignal
|
||||
) => Effect.Effect<WorkAttemptExecutionResult, WorkAttemptExecutionError>;
|
||||
readonly cancel: (
|
||||
workspaceKey: string
|
||||
) => Effect.Effect<void, WorkAttemptExecutionError>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// oxlint-disable-next-line no-barrel-file -- The package root intentionally exposes its public modules.
|
||||
export * from "./agent-os";
|
||||
export * from "./execution-runtime";
|
||||
export * from "./git";
|
||||
export * from "./git-local-runtime";
|
||||
export * from "./git-remote-runtime";
|
||||
|
||||
Reference in New Issue
Block a user