@elie-laloum/outpost 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.fr.md +251 -0
- package/README.md +226 -0
- package/ROADMAP.md +34 -0
- package/SECURITY.md +15 -0
- package/dist/application/campaign.d.ts +33 -0
- package/dist/application/campaign.js +195 -0
- package/dist/application/campaign.js.map +1 -0
- package/dist/application/execution.d.ts +48 -0
- package/dist/application/execution.js +322 -0
- package/dist/application/execution.js.map +1 -0
- package/dist/application/interactive-brief.d.ts +3 -0
- package/dist/application/interactive-brief.js +35 -0
- package/dist/application/interactive-brief.js.map +1 -0
- package/dist/application/outpost.d.ts +91 -0
- package/dist/application/outpost.js +629 -0
- package/dist/application/outpost.js.map +1 -0
- package/dist/application/remote-workspace.d.ts +11 -0
- package/dist/application/remote-workspace.js +264 -0
- package/dist/application/remote-workspace.js.map +1 -0
- package/dist/application/tasks.d.ts +17 -0
- package/dist/application/tasks.js +36 -0
- package/dist/application/tasks.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +94 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/scaffold.d.ts +28 -0
- package/dist/cli/scaffold.js +169 -0
- package/dist/cli/scaffold.js.map +1 -0
- package/dist/cli/starters.d.ts +3 -0
- package/dist/cli/starters.js +34 -0
- package/dist/cli/starters.js.map +1 -0
- package/dist/domain/backlog.d.ts +16 -0
- package/dist/domain/backlog.js +23 -0
- package/dist/domain/backlog.js.map +1 -0
- package/dist/domain/errors.d.ts +11 -0
- package/dist/domain/errors.js +37 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/ports.d.ts +183 -0
- package/dist/domain/ports.js +2 -0
- package/dist/domain/ports.js.map +1 -0
- package/dist/domain/prompts.d.ts +22 -0
- package/dist/domain/prompts.js +45 -0
- package/dist/domain/prompts.js.map +1 -0
- package/dist/domain/response.d.ts +37 -0
- package/dist/domain/response.js +50 -0
- package/dist/domain/response.js.map +1 -0
- package/dist/domain/workflow.d.ts +68 -0
- package/dist/domain/workflow.js +246 -0
- package/dist/domain/workflow.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/abort.d.ts +1 -0
- package/dist/infrastructure/abort.js +16 -0
- package/dist/infrastructure/abort.js.map +1 -0
- package/dist/infrastructure/conversations.d.ts +30 -0
- package/dist/infrastructure/conversations.js +222 -0
- package/dist/infrastructure/conversations.js.map +1 -0
- package/dist/infrastructure/files.d.ts +5 -0
- package/dist/infrastructure/files.js +85 -0
- package/dist/infrastructure/files.js.map +1 -0
- package/dist/infrastructure/git.d.ts +14 -0
- package/dist/infrastructure/git.js +304 -0
- package/dist/infrastructure/git.js.map +1 -0
- package/dist/infrastructure/journal.d.ts +10 -0
- package/dist/infrastructure/journal.js +53 -0
- package/dist/infrastructure/journal.js.map +1 -0
- package/dist/infrastructure/process.d.ts +6 -0
- package/dist/infrastructure/process.js +133 -0
- package/dist/infrastructure/process.js.map +1 -0
- package/dist/infrastructure/reporter.d.ts +10 -0
- package/dist/infrastructure/reporter.js +47 -0
- package/dist/infrastructure/reporter.js.map +1 -0
- package/dist/infrastructure/settings.d.ts +3 -0
- package/dist/infrastructure/settings.js +41 -0
- package/dist/infrastructure/settings.js.map +1 -0
- package/dist/infrastructure/shutdown.d.ts +1 -0
- package/dist/infrastructure/shutdown.js +67 -0
- package/dist/infrastructure/shutdown.js.map +1 -0
- package/dist/infrastructure/terminal.d.ts +11 -0
- package/dist/infrastructure/terminal.js +19 -0
- package/dist/infrastructure/terminal.js.map +1 -0
- package/dist/infrastructure/transfer.d.ts +3 -0
- package/dist/infrastructure/transfer.js +29 -0
- package/dist/infrastructure/transfer.js.map +1 -0
- package/dist/providers/agents.d.ts +17 -0
- package/dist/providers/agents.js +204 -0
- package/dist/providers/agents.js.map +1 -0
- package/dist/providers/backlogs.d.ts +9 -0
- package/dist/providers/backlogs.js +82 -0
- package/dist/providers/backlogs.js.map +1 -0
- package/dist/providers/cloud-files.d.ts +4 -0
- package/dist/providers/cloud-files.js +66 -0
- package/dist/providers/cloud-files.js.map +1 -0
- package/dist/providers/container.d.ts +21 -0
- package/dist/providers/container.js +310 -0
- package/dist/providers/container.js.map +1 -0
- package/dist/providers/daytona.d.ts +10 -0
- package/dist/providers/daytona.js +153 -0
- package/dist/providers/daytona.js.map +1 -0
- package/dist/providers/docker.d.ts +3 -0
- package/dist/providers/docker.js +3 -0
- package/dist/providers/docker.js.map +1 -0
- package/dist/providers/factories.d.ts +9 -0
- package/dist/providers/factories.js +12 -0
- package/dist/providers/factories.js.map +1 -0
- package/dist/providers/local.d.ts +4 -0
- package/dist/providers/local.js +64 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/providers/podman.d.ts +3 -0
- package/dist/providers/podman.js +3 -0
- package/dist/providers/podman.js.map +1 -0
- package/dist/providers/vercel.d.ts +9 -0
- package/dist/providers/vercel.js +158 -0
- package/dist/providers/vercel.js.map +1 -0
- package/dist/providers/versions.d.ts +4 -0
- package/dist/providers/versions.js +5 -0
- package/dist/providers/versions.js.map +1 -0
- package/docs/api.md +147 -0
- package/docs/architecture.md +21 -0
- package/docs/migration-1.1.fr.md +44 -0
- package/docs/migration-1.1.md +38 -0
- package/docs/operations.md +101 -0
- package/docs/providers.md +119 -0
- package/docs/workflows.md +101 -0
- package/package.json +95 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type PromptVariables = Readonly<Record<string, string | number | boolean>>;
|
|
2
|
+
export type Brief = {
|
|
3
|
+
readonly text: string;
|
|
4
|
+
readonly file?: never;
|
|
5
|
+
readonly values?: never;
|
|
6
|
+
} | {
|
|
7
|
+
readonly file: string;
|
|
8
|
+
readonly text?: never;
|
|
9
|
+
readonly values?: PromptVariables;
|
|
10
|
+
};
|
|
11
|
+
export interface PromptFragment {
|
|
12
|
+
readonly kind: "literal" | "command";
|
|
13
|
+
readonly value: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function prepareBrief(source: string, values: PromptVariables | undefined, builtins: {
|
|
16
|
+
WORK_BRANCH: string;
|
|
17
|
+
BASE_BRANCH: string;
|
|
18
|
+
}): {
|
|
19
|
+
fragments: readonly PromptFragment[];
|
|
20
|
+
unused: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
export declare function validateBrief(brief: Brief | undefined, optional?: boolean): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { OutpostError, invariant } from "./errors.js";
|
|
2
|
+
export function prepareBrief(source, values = {}, builtins) {
|
|
3
|
+
for (const key of Object.keys(builtins))
|
|
4
|
+
invariant(!(key in values), `Reserved prompt variable: ${key}`);
|
|
5
|
+
const available = { ...values, ...builtins };
|
|
6
|
+
invariant(Object.values(values).every((value) => typeof value === "string" ||
|
|
7
|
+
typeof value === "boolean" ||
|
|
8
|
+
(typeof value === "number" && Number.isFinite(value))), "Prompt variables must be finite numbers, strings or booleans");
|
|
9
|
+
const used = new Set();
|
|
10
|
+
const substitute = (text) => text.replace(/\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g, (_, key) => {
|
|
11
|
+
if (!Object.hasOwn(available, key))
|
|
12
|
+
throw new OutpostError("prompt", `Missing prompt variable: ${key}`, {
|
|
13
|
+
key,
|
|
14
|
+
});
|
|
15
|
+
used.add(key);
|
|
16
|
+
return String(available[key]);
|
|
17
|
+
});
|
|
18
|
+
const fragments = [];
|
|
19
|
+
let previous = 0;
|
|
20
|
+
for (const match of source.matchAll(/!`([^`]+)`/g)) {
|
|
21
|
+
fragments.push({
|
|
22
|
+
kind: "literal",
|
|
23
|
+
value: substitute(source.slice(previous, match.index)),
|
|
24
|
+
});
|
|
25
|
+
fragments.push({ kind: "command", value: substitute(match[1]) });
|
|
26
|
+
previous = match.index + match[0].length;
|
|
27
|
+
}
|
|
28
|
+
fragments.push({
|
|
29
|
+
kind: "literal",
|
|
30
|
+
value: substitute(source.slice(previous)),
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
fragments,
|
|
34
|
+
unused: Object.keys(values).filter((key) => !used.has(key)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function validateBrief(brief, optional = false) {
|
|
38
|
+
if (!brief && optional)
|
|
39
|
+
return;
|
|
40
|
+
invariant(brief &&
|
|
41
|
+
(typeof brief.text === "string") !== (typeof brief.file === "string"), "Provide exactly one brief: text or file");
|
|
42
|
+
if (typeof brief.text === "string")
|
|
43
|
+
invariant(brief.values === undefined, "Literal briefs cannot use template variables");
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/domain/prompts.ts"],"names":[],"mappings":"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","sourcesContent":["import { OutpostError, invariant } from \"./errors.ts\";\n\nexport type PromptVariables = Readonly<\n Record<string, string | number | boolean>\n>;\nexport type Brief =\n | { readonly text: string; readonly file?: never; readonly values?: never }\n | {\n readonly file: string;\n readonly text?: never;\n readonly values?: PromptVariables;\n };\n\nexport interface PromptFragment {\n readonly kind: \"literal\" | \"command\";\n readonly value: string;\n}\n\nexport function prepareBrief(\n source: string,\n values: PromptVariables = {},\n builtins: { WORK_BRANCH: string; BASE_BRANCH: string },\n): { fragments: readonly PromptFragment[]; unused: readonly string[] } {\n for (const key of Object.keys(builtins))\n invariant(!(key in values), `Reserved prompt variable: ${key}`);\n const available = { ...values, ...builtins };\n invariant(\n Object.values(values).every(\n (value) =>\n typeof value === \"string\" ||\n typeof value === \"boolean\" ||\n (typeof value === \"number\" && Number.isFinite(value)),\n ),\n \"Prompt variables must be finite numbers, strings or booleans\",\n );\n const used = new Set<string>();\n const substitute = (text: string) =>\n text.replace(\n /\\{\\{\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\}\\}/g,\n (_, key: string) => {\n if (!Object.hasOwn(available, key))\n throw new OutpostError(\"prompt\", `Missing prompt variable: ${key}`, {\n key,\n });\n used.add(key);\n return String(available[key as keyof typeof available]);\n },\n );\n const fragments: PromptFragment[] = [];\n let previous = 0;\n for (const match of source.matchAll(/!`([^`]+)`/g)) {\n fragments.push({\n kind: \"literal\",\n value: substitute(source.slice(previous, match.index)),\n });\n fragments.push({ kind: \"command\", value: substitute(match[1]!) });\n previous = match.index + match[0].length;\n }\n fragments.push({\n kind: \"literal\",\n value: substitute(source.slice(previous)),\n });\n return {\n fragments,\n unused: Object.keys(values).filter((key) => !used.has(key)),\n };\n}\n\nexport function validateBrief(\n brief: Brief | undefined,\n optional = false,\n): void {\n if (!brief && optional) return;\n invariant(\n brief &&\n (typeof brief.text === \"string\") !== (typeof brief.file === \"string\"),\n \"Provide exactly one brief: text or file\",\n );\n if (typeof brief.text === \"string\")\n invariant(\n brief.values === undefined,\n \"Literal briefs cannot use template variables\",\n );\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { OutpostError } from "./errors.ts";
|
|
2
|
+
export interface StandardValidator<T> {
|
|
3
|
+
readonly "~standard": {
|
|
4
|
+
readonly validate: (input: unknown) => {
|
|
5
|
+
readonly value: T;
|
|
6
|
+
readonly issues?: undefined;
|
|
7
|
+
} | {
|
|
8
|
+
readonly issues: readonly unknown[];
|
|
9
|
+
} | Promise<{
|
|
10
|
+
readonly value: T;
|
|
11
|
+
readonly issues?: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
readonly issues: readonly unknown[];
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface ResponseSpec<T> {
|
|
18
|
+
readonly tag: string;
|
|
19
|
+
readonly repairs: number;
|
|
20
|
+
read(text: string): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
export declare class ResponseError extends OutpostError {
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
readonly raw: string | undefined;
|
|
25
|
+
constructor(tag: string, message: string, raw?: string, cause?: unknown);
|
|
26
|
+
}
|
|
27
|
+
export declare const response: {
|
|
28
|
+
text: (options: {
|
|
29
|
+
tag: string;
|
|
30
|
+
repairs?: number;
|
|
31
|
+
}) => ResponseSpec<string>;
|
|
32
|
+
json: <T>(options: {
|
|
33
|
+
tag: string;
|
|
34
|
+
schema: StandardValidator<T> | ((input: unknown) => T | Promise<T>);
|
|
35
|
+
repairs?: number;
|
|
36
|
+
}) => ResponseSpec<T>;
|
|
37
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { OutpostError, invariant } from "./errors.js";
|
|
2
|
+
export class ResponseError extends OutpostError {
|
|
3
|
+
tag;
|
|
4
|
+
raw;
|
|
5
|
+
constructor(tag, message, raw, cause) {
|
|
6
|
+
super("response", message, { tag, raw }, cause);
|
|
7
|
+
this.name = "ResponseError";
|
|
8
|
+
this.tag = tag;
|
|
9
|
+
this.raw = raw;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function content(text, tag) {
|
|
13
|
+
const matches = [
|
|
14
|
+
...text.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "g")),
|
|
15
|
+
];
|
|
16
|
+
if (!matches.length)
|
|
17
|
+
throw new ResponseError(tag, `No complete <${tag}> response was found`);
|
|
18
|
+
return matches.at(-1)[1].trim();
|
|
19
|
+
}
|
|
20
|
+
function spec(tag, repairs, parse) {
|
|
21
|
+
invariant(/^[A-Za-z][A-Za-z0-9_-]*$/.test(tag), "Response tags must be XML-style identifiers");
|
|
22
|
+
invariant(Number.isSafeInteger(repairs) && repairs >= 0, "Response repairs must be a nonnegative integer");
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
tag,
|
|
25
|
+
repairs,
|
|
26
|
+
async read(text) {
|
|
27
|
+
const raw = content(text, tag);
|
|
28
|
+
try {
|
|
29
|
+
return await parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
throw new ResponseError(tag, `Invalid <${tag}> response: ${cause instanceof Error ? cause.message : String(cause)}`, raw, cause);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export const response = {
|
|
38
|
+
text: (options) => spec(options.tag, options.repairs ?? 0, async (text) => text),
|
|
39
|
+
json: (options) => spec(options.tag, options.repairs ?? 0, async (text) => {
|
|
40
|
+
const fenced = text.match(/^```(?:json)?\s*\r?\n([\s\S]*?)\r?\n```$/i);
|
|
41
|
+
const input = JSON.parse(fenced ? fenced[1] : text);
|
|
42
|
+
if (typeof options.schema === "function")
|
|
43
|
+
return options.schema(input);
|
|
44
|
+
const result = await options.schema["~standard"].validate(input);
|
|
45
|
+
if (result.issues)
|
|
46
|
+
throw new Error(JSON.stringify(result.issues));
|
|
47
|
+
return result.value;
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../src/domain/response.ts"],"names":[],"mappings":"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","sourcesContent":["import { OutpostError, invariant } from \"./errors.ts\";\n\nexport interface StandardValidator<T> {\n readonly \"~standard\": {\n readonly validate: (\n input: unknown,\n ) =>\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly unknown[] }\n | Promise<\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly unknown[] }\n >;\n };\n}\n\nexport interface ResponseSpec<T> {\n readonly tag: string;\n readonly repairs: number;\n read(text: string): Promise<T>;\n}\n\nexport class ResponseError extends OutpostError {\n readonly tag: string;\n readonly raw: string | undefined;\n constructor(tag: string, message: string, raw?: string, cause?: unknown) {\n super(\"response\", message, { tag, raw }, cause);\n this.name = \"ResponseError\";\n this.tag = tag;\n this.raw = raw;\n }\n}\n\nfunction content(text: string, tag: string): string {\n const matches = [\n ...text.matchAll(new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`, \"g\")),\n ];\n if (!matches.length)\n throw new ResponseError(tag, `No complete <${tag}> response was found`);\n return matches.at(-1)![1]!.trim();\n}\n\nfunction spec<T>(\n tag: string,\n repairs: number,\n parse: (text: string) => Promise<T>,\n): ResponseSpec<T> {\n invariant(\n /^[A-Za-z][A-Za-z0-9_-]*$/.test(tag),\n \"Response tags must be XML-style identifiers\",\n );\n invariant(\n Number.isSafeInteger(repairs) && repairs >= 0,\n \"Response repairs must be a nonnegative integer\",\n );\n return Object.freeze({\n tag,\n repairs,\n async read(text: string) {\n const raw = content(text, tag);\n try {\n return await parse(raw);\n } catch (cause) {\n throw new ResponseError(\n tag,\n `Invalid <${tag}> response: ${cause instanceof Error ? cause.message : String(cause)}`,\n raw,\n cause,\n );\n }\n },\n });\n}\n\nexport const response = {\n text: (options: { tag: string; repairs?: number }): ResponseSpec<string> =>\n spec(options.tag, options.repairs ?? 0, async (text) => text),\n json: <T>(options: {\n tag: string;\n schema: StandardValidator<T> | ((input: unknown) => T | Promise<T>);\n repairs?: number;\n }): ResponseSpec<T> =>\n spec(options.tag, options.repairs ?? 0, async (text) => {\n const fenced = text.match(/^```(?:json)?\\s*\\r?\\n([\\s\\S]*?)\\r?\\n```$/i);\n const input: unknown = JSON.parse(fenced ? fenced[1]! : text);\n if (typeof options.schema === \"function\") return options.schema(input);\n const result = await options.schema[\"~standard\"].validate(input);\n if (result.issues) throw new Error(JSON.stringify(result.issues));\n return result.value;\n }),\n};\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface TaskContext {
|
|
2
|
+
readonly signal: AbortSignal;
|
|
3
|
+
readonly attempt: number;
|
|
4
|
+
readonly executionId: string;
|
|
5
|
+
value<T>(dependency: Task<T>): T;
|
|
6
|
+
}
|
|
7
|
+
export interface Retry {
|
|
8
|
+
readonly attempts: number;
|
|
9
|
+
readonly delayMs?: number;
|
|
10
|
+
readonly accepts?: (error: unknown, attempt: number) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface Task<T = unknown> {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly after: readonly Task[];
|
|
15
|
+
readonly perform: (context: TaskContext) => T | Promise<T>;
|
|
16
|
+
readonly condition?: (context: TaskContext) => boolean | Promise<boolean>;
|
|
17
|
+
readonly retry?: Retry;
|
|
18
|
+
readonly timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export type TaskOptions<T> = Omit<Task<T>, "after"> & {
|
|
21
|
+
readonly after?: readonly Task[];
|
|
22
|
+
};
|
|
23
|
+
export declare function task<T>(options: TaskOptions<T>): Task<T>;
|
|
24
|
+
export type TaskStatus = "waiting" | "active" | "done" | "failed" | "skipped" | "cancelled";
|
|
25
|
+
export interface TaskRecord {
|
|
26
|
+
readonly key: string;
|
|
27
|
+
status: TaskStatus;
|
|
28
|
+
attempts: number;
|
|
29
|
+
startedAt?: string;
|
|
30
|
+
finishedAt?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface WorkflowEvent {
|
|
34
|
+
readonly executionId: string;
|
|
35
|
+
readonly workflow: string;
|
|
36
|
+
readonly timestamp: string;
|
|
37
|
+
readonly type: "start" | "task" | "retry" | "finish";
|
|
38
|
+
readonly key?: string;
|
|
39
|
+
readonly status?: TaskStatus;
|
|
40
|
+
readonly attempt?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface WorkflowOptions {
|
|
43
|
+
readonly signal?: AbortSignal;
|
|
44
|
+
readonly concurrency?: number;
|
|
45
|
+
readonly stopOnError?: boolean;
|
|
46
|
+
readonly observe?: (event: WorkflowEvent) => void;
|
|
47
|
+
}
|
|
48
|
+
export interface WorkflowResult {
|
|
49
|
+
readonly executionId: string;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly status: "done" | "failed" | "cancelled";
|
|
52
|
+
readonly tasks: readonly Readonly<TaskRecord>[];
|
|
53
|
+
readonly errors: readonly unknown[];
|
|
54
|
+
readonly observerErrors: readonly unknown[];
|
|
55
|
+
value<T>(task: Task<T>): T;
|
|
56
|
+
unwrap(): void;
|
|
57
|
+
}
|
|
58
|
+
export declare class WorkflowFailure extends Error {
|
|
59
|
+
readonly result: WorkflowResult;
|
|
60
|
+
constructor(result: WorkflowResult);
|
|
61
|
+
}
|
|
62
|
+
export interface Workflow {
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly tasks: readonly Task[];
|
|
65
|
+
start(options?: WorkflowOptions): Promise<WorkflowResult>;
|
|
66
|
+
diagram(): string;
|
|
67
|
+
}
|
|
68
|
+
export declare function workflow(name: string, tasks: readonly Task[]): Workflow;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
function positive(value, label) {
|
|
4
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
5
|
+
throw new Error(`${label} must be a positive integer`);
|
|
6
|
+
}
|
|
7
|
+
export function task(options) {
|
|
8
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(options.key))
|
|
9
|
+
throw new Error(`Invalid task key: ${options.key}`);
|
|
10
|
+
if (options.timeoutMs !== undefined)
|
|
11
|
+
positive(options.timeoutMs, "timeoutMs");
|
|
12
|
+
if (options.retry) {
|
|
13
|
+
positive(options.retry.attempts, "retry.attempts");
|
|
14
|
+
if (!Number.isFinite(options.retry.delayMs ?? 0) ||
|
|
15
|
+
(options.retry.delayMs ?? 0) < 0)
|
|
16
|
+
throw new Error("retry.delayMs must be nonnegative");
|
|
17
|
+
}
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
...options,
|
|
20
|
+
...(options.retry ? { retry: Object.freeze({ ...options.retry }) } : {}),
|
|
21
|
+
after: Object.freeze([...(options.after ?? [])]),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export class WorkflowFailure extends Error {
|
|
25
|
+
result;
|
|
26
|
+
constructor(result) {
|
|
27
|
+
super(`Workflow ${result.name} ${result.status}`, {
|
|
28
|
+
cause: result.errors[0],
|
|
29
|
+
});
|
|
30
|
+
this.name = "WorkflowFailure";
|
|
31
|
+
this.result = result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function workflow(name, tasks) {
|
|
35
|
+
if (!name.trim())
|
|
36
|
+
throw new Error("Workflow name cannot be empty");
|
|
37
|
+
const graph = Object.freeze([...tasks]);
|
|
38
|
+
validate(graph);
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
name,
|
|
41
|
+
tasks: graph,
|
|
42
|
+
start: (options = {}) => schedule(name, graph, options),
|
|
43
|
+
diagram() {
|
|
44
|
+
const ids = new Map(graph.map((item, index) => [item, `n${index}`]));
|
|
45
|
+
return [
|
|
46
|
+
"flowchart LR",
|
|
47
|
+
...graph.flatMap((item) => [
|
|
48
|
+
` ${ids.get(item)}["${item.key}"]`,
|
|
49
|
+
...item.after.map((dependency) => ` ${ids.get(dependency)} --> ${ids.get(item)}`),
|
|
50
|
+
]),
|
|
51
|
+
].join("\n");
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function validate(tasks) {
|
|
56
|
+
const keys = new Set();
|
|
57
|
+
const members = new Set(tasks);
|
|
58
|
+
const visiting = new Set();
|
|
59
|
+
const visited = new Set();
|
|
60
|
+
for (const item of tasks) {
|
|
61
|
+
if (keys.has(item.key))
|
|
62
|
+
throw new Error(`Duplicate task: ${item.key}`);
|
|
63
|
+
keys.add(item.key);
|
|
64
|
+
for (const dependency of item.after)
|
|
65
|
+
if (!members.has(dependency))
|
|
66
|
+
throw new Error(`${item.key}: missing dependency ${dependency.key}`);
|
|
67
|
+
}
|
|
68
|
+
function visit(item) {
|
|
69
|
+
if (visiting.has(item))
|
|
70
|
+
throw new Error(`Dependency cycle at ${item.key}`);
|
|
71
|
+
if (visited.has(item))
|
|
72
|
+
return;
|
|
73
|
+
visiting.add(item);
|
|
74
|
+
item.after.forEach(visit);
|
|
75
|
+
visiting.delete(item);
|
|
76
|
+
visited.add(item);
|
|
77
|
+
}
|
|
78
|
+
tasks.forEach(visit);
|
|
79
|
+
}
|
|
80
|
+
async function schedule(name, tasks, options) {
|
|
81
|
+
const concurrency = options.concurrency ?? 1;
|
|
82
|
+
positive(concurrency, "concurrency");
|
|
83
|
+
const executionId = randomUUID();
|
|
84
|
+
const stop = new AbortController();
|
|
85
|
+
const signal = options.signal
|
|
86
|
+
? AbortSignal.any([options.signal, stop.signal])
|
|
87
|
+
: stop.signal;
|
|
88
|
+
const values = new Map();
|
|
89
|
+
const records = new Map(tasks.map((item) => [
|
|
90
|
+
item,
|
|
91
|
+
{ key: item.key, status: "waiting", attempts: 0 },
|
|
92
|
+
]));
|
|
93
|
+
const errors = [], observerErrors = [];
|
|
94
|
+
const active = new Map();
|
|
95
|
+
const record = (item) => records.get(item);
|
|
96
|
+
function emit(event) {
|
|
97
|
+
try {
|
|
98
|
+
options.observe?.({
|
|
99
|
+
...event,
|
|
100
|
+
executionId,
|
|
101
|
+
workflow: name,
|
|
102
|
+
timestamp: new Date().toISOString(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
observerErrors.push(error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function finish(item, status) {
|
|
110
|
+
Object.assign(record(item), {
|
|
111
|
+
status,
|
|
112
|
+
finishedAt: new Date().toISOString(),
|
|
113
|
+
});
|
|
114
|
+
emit({
|
|
115
|
+
type: "task",
|
|
116
|
+
key: item.key,
|
|
117
|
+
status,
|
|
118
|
+
attempt: record(item).attempts,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function context(item, attempt, taskSignal) {
|
|
122
|
+
return {
|
|
123
|
+
signal: taskSignal,
|
|
124
|
+
attempt,
|
|
125
|
+
executionId,
|
|
126
|
+
value(dependency) {
|
|
127
|
+
if (!item.after.includes(dependency))
|
|
128
|
+
throw new Error(`${item.key}: undeclared dependency ${dependency.key}`);
|
|
129
|
+
if (!values.has(dependency))
|
|
130
|
+
throw new Error(`${dependency.key} has no successful value`);
|
|
131
|
+
return values.get(dependency);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function launch(item) {
|
|
136
|
+
const state = record(item);
|
|
137
|
+
state.status = "active";
|
|
138
|
+
state.startedAt = new Date().toISOString();
|
|
139
|
+
emit({ type: "task", key: item.key, status: "active", attempt: 0 });
|
|
140
|
+
try {
|
|
141
|
+
signal.throwIfAborted();
|
|
142
|
+
if (item.condition && !(await item.condition(context(item, 0, signal)))) {
|
|
143
|
+
signal.throwIfAborted();
|
|
144
|
+
finish(item, "skipped");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const limit = item.retry?.attempts ?? 1;
|
|
148
|
+
for (let attempt = 1; attempt <= limit; attempt++) {
|
|
149
|
+
signal.throwIfAborted();
|
|
150
|
+
state.attempts = attempt;
|
|
151
|
+
const deadline = new AbortController();
|
|
152
|
+
const timer = item.timeoutMs === undefined
|
|
153
|
+
? undefined
|
|
154
|
+
: setTimeout(() => deadline.abort(new Error(`${item.key} timed out`)), item.timeoutMs);
|
|
155
|
+
const taskSignal = AbortSignal.any([signal, deadline.signal]);
|
|
156
|
+
try {
|
|
157
|
+
const value = await item.perform(context(item, attempt, taskSignal));
|
|
158
|
+
taskSignal.throwIfAborted();
|
|
159
|
+
values.set(item, value);
|
|
160
|
+
finish(item, "done");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (signal.aborted ||
|
|
165
|
+
attempt === limit ||
|
|
166
|
+
item.retry?.accepts?.(error, attempt) === false)
|
|
167
|
+
throw error;
|
|
168
|
+
emit({ type: "retry", key: item.key, attempt });
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
}
|
|
173
|
+
await delay(item.retry?.delayMs ?? 0, undefined, { signal });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
178
|
+
if (signal.aborted)
|
|
179
|
+
finish(item, "cancelled");
|
|
180
|
+
else {
|
|
181
|
+
errors.push(error);
|
|
182
|
+
finish(item, "failed");
|
|
183
|
+
if (options.stopOnError !== false)
|
|
184
|
+
stop.abort(error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
emit({ type: "start" });
|
|
189
|
+
while (true) {
|
|
190
|
+
let changed = false;
|
|
191
|
+
for (const item of tasks) {
|
|
192
|
+
if (record(item).status !== "waiting")
|
|
193
|
+
continue;
|
|
194
|
+
if (signal.aborted) {
|
|
195
|
+
finish(item, "cancelled");
|
|
196
|
+
changed = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const dependencies = item.after.map((dependency) => record(dependency).status);
|
|
200
|
+
if (dependencies.some((status) => ["failed", "skipped", "cancelled"].includes(status))) {
|
|
201
|
+
finish(item, "skipped");
|
|
202
|
+
changed = true;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (active.size >= concurrency ||
|
|
206
|
+
!dependencies.every((status) => status === "done"))
|
|
207
|
+
continue;
|
|
208
|
+
const running = launch(item).finally(() => {
|
|
209
|
+
active.delete(item);
|
|
210
|
+
});
|
|
211
|
+
active.set(item, running);
|
|
212
|
+
changed = true;
|
|
213
|
+
}
|
|
214
|
+
if (active.size)
|
|
215
|
+
await Promise.race(active.values());
|
|
216
|
+
else if (!changed)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
if (options.signal?.aborted)
|
|
220
|
+
errors.push(options.signal.reason);
|
|
221
|
+
const status = options.signal?.aborted
|
|
222
|
+
? "cancelled"
|
|
223
|
+
: errors.length
|
|
224
|
+
? "failed"
|
|
225
|
+
: "done";
|
|
226
|
+
emit({ type: "finish" });
|
|
227
|
+
const result = Object.freeze({
|
|
228
|
+
executionId,
|
|
229
|
+
name,
|
|
230
|
+
status,
|
|
231
|
+
tasks: Object.freeze([...records.values()].map((value) => Object.freeze({ ...value }))),
|
|
232
|
+
errors: Object.freeze(errors),
|
|
233
|
+
observerErrors: Object.freeze(observerErrors),
|
|
234
|
+
value(item) {
|
|
235
|
+
if (!values.has(item))
|
|
236
|
+
throw new Error(`${item.key} has no successful value in this execution`);
|
|
237
|
+
return values.get(item);
|
|
238
|
+
},
|
|
239
|
+
unwrap() {
|
|
240
|
+
if (status !== "done")
|
|
241
|
+
throw new WorkflowFailure(result);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=workflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.js","sourceRoot":"","sources":["../../src/domain/workflow.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { setTimeout as delay } from \"node:timers/promises\";\n\nexport interface TaskContext {\n readonly signal: AbortSignal;\n readonly attempt: number;\n readonly executionId: string;\n value<T>(dependency: Task<T>): T;\n}\n\nexport interface Retry {\n readonly attempts: number;\n readonly delayMs?: number;\n readonly accepts?: (error: unknown, attempt: number) => boolean;\n}\n\nexport interface Task<T = unknown> {\n readonly key: string;\n readonly after: readonly Task[];\n readonly perform: (context: TaskContext) => T | Promise<T>;\n readonly condition?: (context: TaskContext) => boolean | Promise<boolean>;\n readonly retry?: Retry;\n readonly timeoutMs?: number;\n}\n\nexport type TaskOptions<T> = Omit<Task<T>, \"after\"> & {\n readonly after?: readonly Task[];\n};\n\nfunction positive(value: number, label: string): void {\n if (!Number.isSafeInteger(value) || value < 1)\n throw new Error(`${label} must be a positive integer`);\n}\n\nexport function task<T>(options: TaskOptions<T>): Task<T> {\n if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(options.key))\n throw new Error(`Invalid task key: ${options.key}`);\n if (options.timeoutMs !== undefined) positive(options.timeoutMs, \"timeoutMs\");\n if (options.retry) {\n positive(options.retry.attempts, \"retry.attempts\");\n if (\n !Number.isFinite(options.retry.delayMs ?? 0) ||\n (options.retry.delayMs ?? 0) < 0\n )\n throw new Error(\"retry.delayMs must be nonnegative\");\n }\n return Object.freeze({\n ...options,\n ...(options.retry ? { retry: Object.freeze({ ...options.retry }) } : {}),\n after: Object.freeze([...(options.after ?? [])]),\n });\n}\n\nexport type TaskStatus =\n \"waiting\" | \"active\" | \"done\" | \"failed\" | \"skipped\" | \"cancelled\";\nexport interface TaskRecord {\n readonly key: string;\n status: TaskStatus;\n attempts: number;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nexport interface WorkflowEvent {\n readonly executionId: string;\n readonly workflow: string;\n readonly timestamp: string;\n readonly type: \"start\" | \"task\" | \"retry\" | \"finish\";\n readonly key?: string;\n readonly status?: TaskStatus;\n readonly attempt?: number;\n}\n\nexport interface WorkflowOptions {\n readonly signal?: AbortSignal;\n readonly concurrency?: number;\n readonly stopOnError?: boolean;\n readonly observe?: (event: WorkflowEvent) => void;\n}\n\nexport interface WorkflowResult {\n readonly executionId: string;\n readonly name: string;\n readonly status: \"done\" | \"failed\" | \"cancelled\";\n readonly tasks: readonly Readonly<TaskRecord>[];\n readonly errors: readonly unknown[];\n readonly observerErrors: readonly unknown[];\n value<T>(task: Task<T>): T;\n unwrap(): void;\n}\n\nexport class WorkflowFailure extends Error {\n readonly result: WorkflowResult;\n constructor(result: WorkflowResult) {\n super(`Workflow ${result.name} ${result.status}`, {\n cause: result.errors[0],\n });\n this.name = \"WorkflowFailure\";\n this.result = result;\n }\n}\n\nexport interface Workflow {\n readonly name: string;\n readonly tasks: readonly Task[];\n start(options?: WorkflowOptions): Promise<WorkflowResult>;\n diagram(): string;\n}\n\nexport function workflow(name: string, tasks: readonly Task[]): Workflow {\n if (!name.trim()) throw new Error(\"Workflow name cannot be empty\");\n const graph = Object.freeze([...tasks]);\n validate(graph);\n return Object.freeze({\n name,\n tasks: graph,\n start: (options: WorkflowOptions = {}) => schedule(name, graph, options),\n diagram() {\n const ids = new Map(graph.map((item, index) => [item, `n${index}`]));\n return [\n \"flowchart LR\",\n ...graph.flatMap((item) => [\n ` ${ids.get(item)}[\"${item.key}\"]`,\n ...item.after.map(\n (dependency) => ` ${ids.get(dependency)} --> ${ids.get(item)}`,\n ),\n ]),\n ].join(\"\\n\");\n },\n });\n}\n\nfunction validate(tasks: readonly Task[]): void {\n const keys = new Set<string>();\n const members = new Set(tasks);\n const visiting = new Set<Task>();\n const visited = new Set<Task>();\n for (const item of tasks) {\n if (keys.has(item.key)) throw new Error(`Duplicate task: ${item.key}`);\n keys.add(item.key);\n for (const dependency of item.after)\n if (!members.has(dependency))\n throw new Error(`${item.key}: missing dependency ${dependency.key}`);\n }\n function visit(item: Task): void {\n if (visiting.has(item)) throw new Error(`Dependency cycle at ${item.key}`);\n if (visited.has(item)) return;\n visiting.add(item);\n item.after.forEach(visit);\n visiting.delete(item);\n visited.add(item);\n }\n tasks.forEach(visit);\n}\n\nasync function schedule(\n name: string,\n tasks: readonly Task[],\n options: WorkflowOptions,\n): Promise<WorkflowResult> {\n const concurrency = options.concurrency ?? 1;\n positive(concurrency, \"concurrency\");\n const executionId = randomUUID();\n const stop = new AbortController();\n const signal = options.signal\n ? AbortSignal.any([options.signal, stop.signal])\n : stop.signal;\n const values = new Map<Task, unknown>();\n const records = new Map<Task, TaskRecord>(\n tasks.map((item) => [\n item,\n { key: item.key, status: \"waiting\", attempts: 0 },\n ]),\n );\n const errors: unknown[] = [],\n observerErrors: unknown[] = [];\n const active = new Map<Task, Promise<void>>();\n const record = (item: Task) => records.get(item)!;\n function emit(\n event: Omit<WorkflowEvent, \"executionId\" | \"workflow\" | \"timestamp\">,\n ): void {\n try {\n options.observe?.({\n ...event,\n executionId,\n workflow: name,\n timestamp: new Date().toISOString(),\n });\n } catch (error) {\n observerErrors.push(error);\n }\n }\n function finish(item: Task, status: TaskStatus): void {\n Object.assign(record(item), {\n status,\n finishedAt: new Date().toISOString(),\n });\n emit({\n type: \"task\",\n key: item.key,\n status,\n attempt: record(item).attempts,\n });\n }\n function context(\n item: Task,\n attempt: number,\n taskSignal: AbortSignal,\n ): TaskContext {\n return {\n signal: taskSignal,\n attempt,\n executionId,\n value<T>(dependency: Task<T>): T {\n if (!item.after.includes(dependency))\n throw new Error(\n `${item.key}: undeclared dependency ${dependency.key}`,\n );\n if (!values.has(dependency))\n throw new Error(`${dependency.key} has no successful value`);\n return values.get(dependency) as T;\n },\n };\n }\n async function launch(item: Task): Promise<void> {\n const state = record(item);\n state.status = \"active\";\n state.startedAt = new Date().toISOString();\n emit({ type: \"task\", key: item.key, status: \"active\", attempt: 0 });\n try {\n signal.throwIfAborted();\n if (item.condition && !(await item.condition(context(item, 0, signal)))) {\n signal.throwIfAborted();\n finish(item, \"skipped\");\n return;\n }\n const limit = item.retry?.attempts ?? 1;\n for (let attempt = 1; attempt <= limit; attempt++) {\n signal.throwIfAborted();\n state.attempts = attempt;\n const deadline = new AbortController();\n const timer =\n item.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () => deadline.abort(new Error(`${item.key} timed out`)),\n item.timeoutMs,\n );\n const taskSignal = AbortSignal.any([signal, deadline.signal]);\n try {\n const value = await item.perform(context(item, attempt, taskSignal));\n taskSignal.throwIfAborted();\n values.set(item, value);\n finish(item, \"done\");\n return;\n } catch (error) {\n if (\n signal.aborted ||\n attempt === limit ||\n item.retry?.accepts?.(error, attempt) === false\n )\n throw error;\n emit({ type: \"retry\", key: item.key, attempt });\n } finally {\n clearTimeout(timer);\n }\n await delay(item.retry?.delayMs ?? 0, undefined, { signal });\n }\n } catch (error) {\n state.error = error instanceof Error ? error.message : String(error);\n if (signal.aborted) finish(item, \"cancelled\");\n else {\n errors.push(error);\n finish(item, \"failed\");\n if (options.stopOnError !== false) stop.abort(error);\n }\n }\n }\n emit({ type: \"start\" });\n while (true) {\n let changed = false;\n for (const item of tasks) {\n if (record(item).status !== \"waiting\") continue;\n if (signal.aborted) {\n finish(item, \"cancelled\");\n changed = true;\n continue;\n }\n const dependencies = item.after.map(\n (dependency) => record(dependency).status,\n );\n if (\n dependencies.some((status) =>\n [\"failed\", \"skipped\", \"cancelled\"].includes(status),\n )\n ) {\n finish(item, \"skipped\");\n changed = true;\n continue;\n }\n if (\n active.size >= concurrency ||\n !dependencies.every((status) => status === \"done\")\n )\n continue;\n const running = launch(item).finally(() => {\n active.delete(item);\n });\n active.set(item, running);\n changed = true;\n }\n if (active.size) await Promise.race(active.values());\n else if (!changed) break;\n }\n if (options.signal?.aborted) errors.push(options.signal.reason);\n const status = options.signal?.aborted\n ? \"cancelled\"\n : errors.length\n ? \"failed\"\n : \"done\";\n emit({ type: \"finish\" });\n const result: WorkflowResult = Object.freeze({\n executionId,\n name,\n status,\n tasks: Object.freeze(\n [...records.values()].map((value) => Object.freeze({ ...value })),\n ),\n errors: Object.freeze(errors),\n observerErrors: Object.freeze(observerErrors),\n value<T>(item: Task<T>): T {\n if (!values.has(item))\n throw new Error(\n `${item.key} has no successful value in this execution`,\n );\n return values.get(item) as T;\n },\n unwrap() {\n if (status !== \"done\") throw new WorkflowFailure(result);\n },\n });\n return result;\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { dispatch, attach, createSandbox, openWorkspace, } from "./application/outpost.ts";
|
|
2
|
+
export type { AttachOptions, AttachResult, DispatchResult, WarmDispatchResult, ContinuationOptions, Sandbox, SandboxOptions, Workspace, WorkspaceOptions, } from "./application/outpost.ts";
|
|
3
|
+
export type { DispatchOptions, Execution, Turn, } from "./application/execution.ts";
|
|
4
|
+
export { task, workflow, WorkflowFailure } from "./domain/workflow.ts";
|
|
5
|
+
export type { Task, TaskContext, TaskOptions, TaskRecord, TaskStatus, Retry, Workflow, WorkflowEvent, WorkflowOptions, WorkflowResult, } from "./domain/workflow.ts";
|
|
6
|
+
export { agentTask, isolatedTask, commandTask } from "./application/tasks.ts";
|
|
7
|
+
export { campaign } from "./application/campaign.ts";
|
|
8
|
+
export type { CampaignOptions, CampaignResult, CampaignEvent, IssueOutcome, } from "./application/campaign.ts";
|
|
9
|
+
export type { Issue, Backlog, Assignment } from "./domain/backlog.ts";
|
|
10
|
+
export { githubBacklog, beadsBacklog } from "./providers/backlogs.ts";
|
|
11
|
+
export type { BacklogSettings } from "./providers/backlogs.ts";
|
|
12
|
+
export { claude, codex } from "./providers/agents.ts";
|
|
13
|
+
export { agentVersions } from "./providers/versions.ts";
|
|
14
|
+
export { conversations } from "./infrastructure/conversations.ts";
|
|
15
|
+
export type { ConversationFormat, ConversationLocation, } from "./infrastructure/conversations.ts";
|
|
16
|
+
export type { ClaudeSettings, CodexSettings } from "./providers/agents.ts";
|
|
17
|
+
export { mountedProvider, remoteProvider } from "./providers/factories.ts";
|
|
18
|
+
export { response, ResponseError } from "./domain/response.ts";
|
|
19
|
+
export type { ResponseSpec, StandardValidator } from "./domain/response.ts";
|
|
20
|
+
export { OutpostError, recoveryDetails } from "./domain/errors.ts";
|
|
21
|
+
export type { FaultCode } from "./domain/errors.ts";
|
|
22
|
+
export type { AgentAdapter, AgentEvent, AgentObservation, AgentInput, BranchPolicy, Channel, Command, CommandResult, Commit, ConversationRecord, ConversationContext, ConversationStore, Disposal, LifecycleHooks, SandboxContext, SandboxLease, SandboxProvider, StageLimits, TransferOptions, Usage, Variables, Volume, WorkspaceRecord, } from "./domain/ports.ts";
|
|
23
|
+
export type { Brief, PromptVariables } from "./domain/prompts.ts";
|
|
24
|
+
export type { Logging } from "./infrastructure/journal.ts";
|
|
25
|
+
export { reporter } from "./infrastructure/reporter.ts";
|
|
26
|
+
export type { ReporterOptions } from "./infrastructure/reporter.ts";
|
|
27
|
+
export type { VariableQuestion } from "./application/interactive-brief.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { dispatch, attach, createSandbox, openWorkspace, } from "./application/outpost.js";
|
|
2
|
+
export { task, workflow, WorkflowFailure } from "./domain/workflow.js";
|
|
3
|
+
export { agentTask, isolatedTask, commandTask } from "./application/tasks.js";
|
|
4
|
+
export { campaign } from "./application/campaign.js";
|
|
5
|
+
export { githubBacklog, beadsBacklog } from "./providers/backlogs.js";
|
|
6
|
+
export { claude, codex } from "./providers/agents.js";
|
|
7
|
+
export { agentVersions } from "./providers/versions.js";
|
|
8
|
+
export { conversations } from "./infrastructure/conversations.js";
|
|
9
|
+
export { mountedProvider, remoteProvider } from "./providers/factories.js";
|
|
10
|
+
export { response, ResponseError } from "./domain/response.js";
|
|
11
|
+
export { OutpostError, recoveryDetails } from "./domain/errors.js";
|
|
12
|
+
export { reporter } from "./infrastructure/reporter.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,MAAM,EACN,aAAa,EACb,aAAa,GACd,MAAM,0BAA0B,CAAC;AAiBlC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAavE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAQrD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEtE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAMlE,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA6BnE,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC","sourcesContent":["export {\n dispatch,\n attach,\n createSandbox,\n openWorkspace,\n} from \"./application/outpost.ts\";\nexport type {\n AttachOptions,\n AttachResult,\n DispatchResult,\n WarmDispatchResult,\n ContinuationOptions,\n Sandbox,\n SandboxOptions,\n Workspace,\n WorkspaceOptions,\n} from \"./application/outpost.ts\";\nexport type {\n DispatchOptions,\n Execution,\n Turn,\n} from \"./application/execution.ts\";\nexport { task, workflow, WorkflowFailure } from \"./domain/workflow.ts\";\nexport type {\n Task,\n TaskContext,\n TaskOptions,\n TaskRecord,\n TaskStatus,\n Retry,\n Workflow,\n WorkflowEvent,\n WorkflowOptions,\n WorkflowResult,\n} from \"./domain/workflow.ts\";\nexport { agentTask, isolatedTask, commandTask } from \"./application/tasks.ts\";\nexport { campaign } from \"./application/campaign.ts\";\nexport type {\n CampaignOptions,\n CampaignResult,\n CampaignEvent,\n IssueOutcome,\n} from \"./application/campaign.ts\";\nexport type { Issue, Backlog, Assignment } from \"./domain/backlog.ts\";\nexport { githubBacklog, beadsBacklog } from \"./providers/backlogs.ts\";\nexport type { BacklogSettings } from \"./providers/backlogs.ts\";\nexport { claude, codex } from \"./providers/agents.ts\";\nexport { agentVersions } from \"./providers/versions.ts\";\nexport { conversations } from \"./infrastructure/conversations.ts\";\nexport type {\n ConversationFormat,\n ConversationLocation,\n} from \"./infrastructure/conversations.ts\";\nexport type { ClaudeSettings, CodexSettings } from \"./providers/agents.ts\";\nexport { mountedProvider, remoteProvider } from \"./providers/factories.ts\";\nexport { response, ResponseError } from \"./domain/response.ts\";\nexport type { ResponseSpec, StandardValidator } from \"./domain/response.ts\";\nexport { OutpostError, recoveryDetails } from \"./domain/errors.ts\";\nexport type { FaultCode } from \"./domain/errors.ts\";\nexport type {\n AgentAdapter,\n AgentEvent,\n AgentObservation,\n AgentInput,\n BranchPolicy,\n Channel,\n Command,\n CommandResult,\n Commit,\n ConversationRecord,\n ConversationContext,\n ConversationStore,\n Disposal,\n LifecycleHooks,\n SandboxContext,\n SandboxLease,\n SandboxProvider,\n StageLimits,\n TransferOptions,\n Usage,\n Variables,\n Volume,\n WorkspaceRecord,\n} from \"./domain/ports.ts\";\nexport type { Brief, PromptVariables } from \"./domain/prompts.ts\";\nexport type { Logging } from \"./infrastructure/journal.ts\";\nexport { reporter } from \"./infrastructure/reporter.ts\";\nexport type { ReporterOptions } from \"./infrastructure/reporter.ts\";\nexport type { VariableQuestion } from \"./application/interactive-brief.ts\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function interruptible<T>(pending: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export async function interruptible(pending, signal) {
|
|
2
|
+
let abort = () => { };
|
|
3
|
+
const interrupted = new Promise((_, reject) => {
|
|
4
|
+
abort = () => reject(signal.reason);
|
|
5
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
6
|
+
if (signal.aborted)
|
|
7
|
+
abort();
|
|
8
|
+
});
|
|
9
|
+
try {
|
|
10
|
+
return await Promise.race([pending, interrupted]);
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
signal.removeEventListener("abort", abort);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=abort.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abort.js","sourceRoot":"","sources":["../../src/infrastructure/abort.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAmB,EACnB,MAAmB;IAEnB,IAAI,KAAK,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;IACrB,MAAM,WAAW,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QACnD,KAAK,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,IAAI,MAAM,CAAC,OAAO;YAAE,KAAK,EAAE,CAAC;IAC9B,CAAC,CAAC,CAAC;IACH,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IACpD,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC","sourcesContent":["export async function interruptible<T>(\n pending: Promise<T>,\n signal: AbortSignal,\n): Promise<T> {\n let abort = () => {};\n const interrupted = new Promise<never>((_, reject) => {\n abort = () => reject(signal.reason);\n signal.addEventListener(\"abort\", abort, { once: true });\n if (signal.aborted) abort();\n });\n try {\n return await Promise.race([pending, interrupted]);\n } finally {\n signal.removeEventListener(\"abort\", abort);\n }\n}\n"]}
|