@twelvehart/orcats 0.0.0-seed.0 → 0.2.3
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/LICENSE +48 -0
- package/NOTICE +5 -0
- package/README.md +399 -0
- package/bin/orcats +4 -0
- package/dist/backends/acp-client.d.ts +74 -0
- package/dist/backends/acp-client.d.ts.map +1 -0
- package/dist/backends/acp-run.d.ts +31 -0
- package/dist/backends/acp-run.d.ts.map +1 -0
- package/dist/backends/claude-run.d.ts +35 -0
- package/dist/backends/claude-run.d.ts.map +1 -0
- package/dist/backends/claude-stream-json.d.ts +15 -0
- package/dist/backends/claude-stream-json.d.ts.map +1 -0
- package/dist/backends/codex-jsonl.d.ts +34 -0
- package/dist/backends/codex-jsonl.d.ts.map +1 -0
- package/dist/backends/codex-run.d.ts +40 -0
- package/dist/backends/codex-run.d.ts.map +1 -0
- package/dist/backends/codex.d.ts +5 -0
- package/dist/backends/codex.d.ts.map +1 -0
- package/dist/backends/conversation-config.d.ts +12 -0
- package/dist/backends/conversation-config.d.ts.map +1 -0
- package/dist/backends/index.d.ts +14 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/opencode-run.d.ts +48 -0
- package/dist/backends/opencode-run.d.ts.map +1 -0
- package/dist/backends/opencode-sse.d.ts +35 -0
- package/dist/backends/opencode-sse.d.ts.map +1 -0
- package/dist/backends/pi-rpc.d.ts +21 -0
- package/dist/backends/pi-rpc.d.ts.map +1 -0
- package/dist/backends/pi-run.d.ts +21 -0
- package/dist/backends/pi-run.d.ts.map +1 -0
- package/dist/backends/select.d.ts +17 -0
- package/dist/backends/select.d.ts.map +1 -0
- package/dist/backends/subprocess-run.d.ts +68 -0
- package/dist/backends/subprocess-run.d.ts.map +1 -0
- package/dist/backends/types.d.ts +17 -0
- package/dist/backends/types.d.ts.map +1 -0
- package/dist/backends/unsupported.d.ts +9 -0
- package/dist/backends/unsupported.d.ts.map +1 -0
- package/dist/baseline/index.d.ts +56 -0
- package/dist/baseline/index.d.ts.map +1 -0
- package/dist/cli/args.d.ts +22 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/embedded.d.ts +7 -0
- package/dist/cli/embedded.d.ts.map +1 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/conversation/ask-user.d.ts +17 -0
- package/dist/conversation/ask-user.d.ts.map +1 -0
- package/dist/conversation/conversation.d.ts +46 -0
- package/dist/conversation/conversation.d.ts.map +1 -0
- package/dist/conversation/harness.d.ts +14 -0
- package/dist/conversation/harness.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +5 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/queue.d.ts +15 -0
- package/dist/conversation/queue.d.ts.map +1 -0
- package/dist/flow/accessors.d.ts +13 -0
- package/dist/flow/accessors.d.ts.map +1 -0
- package/dist/flow/approvals.d.ts +6 -0
- package/dist/flow/approvals.d.ts.map +1 -0
- package/dist/flow/args.d.ts +4 -0
- package/dist/flow/args.d.ts.map +1 -0
- package/dist/flow/context.d.ts +36 -0
- package/dist/flow/context.d.ts.map +1 -0
- package/dist/flow/index.d.ts +5 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/loop/builder/index.d.ts +17 -0
- package/dist/loop/builder/index.d.ts.map +1 -0
- package/dist/loop/builder/types.d.ts +97 -0
- package/dist/loop/builder/types.d.ts.map +1 -0
- package/dist/loop/context/compaction.d.ts +38 -0
- package/dist/loop/context/compaction.d.ts.map +1 -0
- package/dist/loop/context/index.d.ts +4 -0
- package/dist/loop/context/index.d.ts.map +1 -0
- package/dist/loop/context/offload.d.ts +34 -0
- package/dist/loop/context/offload.d.ts.map +1 -0
- package/dist/loop/context/types.d.ts +13 -0
- package/dist/loop/context/types.d.ts.map +1 -0
- package/dist/loop/engine/bridge.d.ts +21 -0
- package/dist/loop/engine/bridge.d.ts.map +1 -0
- package/dist/loop/engine/index.d.ts +73 -0
- package/dist/loop/engine/index.d.ts.map +1 -0
- package/dist/loop/execution.d.ts +68 -0
- package/dist/loop/execution.d.ts.map +1 -0
- package/dist/loop/fanout.d.ts +148 -0
- package/dist/loop/fanout.d.ts.map +1 -0
- package/dist/loop/firing.d.ts +54 -0
- package/dist/loop/firing.d.ts.map +1 -0
- package/dist/loop/graph/analyze.d.ts +8 -0
- package/dist/loop/graph/analyze.d.ts.map +1 -0
- package/dist/loop/graph/index.d.ts +3 -0
- package/dist/loop/graph/index.d.ts.map +1 -0
- package/dist/loop/graph/types.d.ts +35 -0
- package/dist/loop/graph/types.d.ts.map +1 -0
- package/dist/loop/index.d.ts +7 -0
- package/dist/loop/index.d.ts.map +1 -0
- package/dist/loop/io/index.d.ts +4 -0
- package/dist/loop/io/index.d.ts.map +1 -0
- package/dist/loop/io/linear.d.ts +142 -0
- package/dist/loop/io/linear.d.ts.map +1 -0
- package/dist/loop/io/sink.d.ts +47 -0
- package/dist/loop/io/sink.d.ts.map +1 -0
- package/dist/loop/io/source.d.ts +64 -0
- package/dist/loop/io/source.d.ts.map +1 -0
- package/dist/loop/serve.d.ts +97 -0
- package/dist/loop/serve.d.ts.map +1 -0
- package/dist/loop/state/index.d.ts +5 -0
- package/dist/loop/state/index.d.ts.map +1 -0
- package/dist/loop/state/manifest.d.ts +36 -0
- package/dist/loop/state/manifest.d.ts.map +1 -0
- package/dist/loop/state/port.d.ts +33 -0
- package/dist/loop/state/port.d.ts.map +1 -0
- package/dist/loop/state/snapshot.d.ts +15 -0
- package/dist/loop/state/snapshot.d.ts.map +1 -0
- package/dist/loop/state/sqlite.d.ts +31 -0
- package/dist/loop/state/sqlite.d.ts.map +1 -0
- package/dist/loop/termination-contract.d.ts +28 -0
- package/dist/loop/termination-contract.d.ts.map +1 -0
- package/dist/loop/termination.d.ts +33 -0
- package/dist/loop/termination.d.ts.map +1 -0
- package/dist/model/backend-config.d.ts +30 -0
- package/dist/model/backend-config.d.ts.map +1 -0
- package/dist/model/brand.d.ts +9 -0
- package/dist/model/brand.d.ts.map +1 -0
- package/dist/model/errors.d.ts +11 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/index.d.ts +8 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/json-schema.d.ts +4 -0
- package/dist/model/json-schema.d.ts.map +1 -0
- package/dist/model/result.d.ts +3 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/schemas.d.ts +435 -0
- package/dist/model/schemas.d.ts.map +1 -0
- package/dist/model/structured.d.ts +9 -0
- package/dist/model/structured.d.ts.map +1 -0
- package/dist/monitor/index.d.ts +139 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/plan/index.d.ts +3 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/persistent.d.ts +23 -0
- package/dist/plan/persistent.d.ts.map +1 -0
- package/dist/plan/verdict.d.ts +9 -0
- package/dist/plan/verdict.d.ts.map +1 -0
- package/dist/review/index.d.ts +4 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/loop.d.ts +117 -0
- package/dist/review/loop.d.ts.map +1 -0
- package/dist/review/reviewers.d.ts +13 -0
- package/dist/review/reviewers.d.ts.map +1 -0
- package/dist/review/tool.d.ts +20 -0
- package/dist/review/tool.d.ts.map +1 -0
- package/dist/run-output/index.d.ts +112 -0
- package/dist/run-output/index.d.ts.map +1 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/typecheck.d.ts +23 -0
- package/dist/runner/typecheck.d.ts.map +1 -0
- package/dist/test-utils/fakes.d.ts +76 -0
- package/dist/test-utils/fakes.d.ts.map +1 -0
- package/dist/test-utils/fixtures.d.ts +4 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +4 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/tier2.d.ts +8 -0
- package/dist/test-utils/tier2.d.ts.map +1 -0
- package/dist/tools/fs.d.ts +11 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/git.d.ts +10 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/github.d.ts +13 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/linear.d.ts +117 -0
- package/dist/tools/linear.d.ts.map +1 -0
- package/dist/tools/process.d.ts +40 -0
- package/dist/tools/process.d.ts.map +1 -0
- package/dist/tools/terminal.d.ts +23 -0
- package/dist/tools/terminal.d.ts.map +1 -0
- package/package.json +89 -9
- package/src/backends/acp-client.ts +324 -0
- package/src/backends/acp-run.ts +636 -0
- package/src/backends/claude-run.ts +226 -0
- package/src/backends/claude-stream-json.ts +273 -0
- package/src/backends/codex-jsonl.ts +339 -0
- package/src/backends/codex-run.ts +177 -0
- package/src/backends/codex.ts +84 -0
- package/src/backends/conversation-config.ts +26 -0
- package/src/backends/index.ts +13 -0
- package/src/backends/opencode-run.ts +432 -0
- package/src/backends/opencode-sse.ts +360 -0
- package/src/backends/pi-rpc.ts +242 -0
- package/src/backends/pi-run.ts +154 -0
- package/src/backends/select.ts +77 -0
- package/src/backends/subprocess-run.ts +305 -0
- package/src/backends/types.ts +29 -0
- package/src/backends/unsupported.ts +48 -0
- package/src/baseline/index.ts +437 -0
- package/src/cli/args.ts +109 -0
- package/src/cli/embedded.ts +206 -0
- package/src/cli/main.ts +332 -0
- package/src/cli/version.ts +1 -0
- package/src/conversation/ask-user.ts +105 -0
- package/src/conversation/conversation.ts +109 -0
- package/src/conversation/harness.ts +39 -0
- package/src/conversation/index.ts +4 -0
- package/src/conversation/queue.ts +86 -0
- package/src/flow/accessors.ts +43 -0
- package/src/flow/approvals.ts +10 -0
- package/src/flow/args.ts +26 -0
- package/src/flow/context.ts +106 -0
- package/src/flow/index.ts +4 -0
- package/src/index.ts +17 -0
- package/src/loop/builder/index.ts +346 -0
- package/src/loop/builder/types.ts +131 -0
- package/src/loop/context/compaction.ts +175 -0
- package/src/loop/context/index.ts +3 -0
- package/src/loop/context/offload.ts +79 -0
- package/src/loop/context/types.ts +17 -0
- package/src/loop/engine/bridge.ts +61 -0
- package/src/loop/engine/index.ts +166 -0
- package/src/loop/execution.ts +453 -0
- package/src/loop/fanout.ts +444 -0
- package/src/loop/firing.ts +188 -0
- package/src/loop/graph/analyze.ts +128 -0
- package/src/loop/graph/index.ts +2 -0
- package/src/loop/graph/types.ts +45 -0
- package/src/loop/index.ts +11 -0
- package/src/loop/io/index.ts +3 -0
- package/src/loop/io/linear.ts +714 -0
- package/src/loop/io/sink.ts +170 -0
- package/src/loop/io/source.ts +255 -0
- package/src/loop/serve.ts +307 -0
- package/src/loop/state/index.ts +4 -0
- package/src/loop/state/manifest.ts +56 -0
- package/src/loop/state/port.ts +43 -0
- package/src/loop/state/snapshot.ts +129 -0
- package/src/loop/state/sqlite.ts +265 -0
- package/src/loop/termination-contract.ts +89 -0
- package/src/loop/termination.ts +156 -0
- package/src/model/backend-config.ts +34 -0
- package/src/model/brand.ts +11 -0
- package/src/model/errors.ts +28 -0
- package/src/model/index.ts +7 -0
- package/src/model/json-schema.ts +14 -0
- package/src/model/result.ts +28 -0
- package/src/model/schemas.ts +218 -0
- package/src/model/structured.ts +26 -0
- package/src/monitor/index.ts +367 -0
- package/src/plan/index.ts +2 -0
- package/src/plan/persistent.ts +98 -0
- package/src/plan/verdict.ts +3 -0
- package/src/review/index.ts +3 -0
- package/src/review/loop.ts +365 -0
- package/src/review/reviewers.ts +92 -0
- package/src/review/tool.ts +41 -0
- package/src/run-output/index.ts +319 -0
- package/src/runner/index.ts +1 -0
- package/src/runner/typecheck.ts +72 -0
- package/src/test-utils/fakes.ts +260 -0
- package/src/test-utils/fixtures.ts +33 -0
- package/src/test-utils/index.ts +3 -0
- package/src/test-utils/tier2.ts +12 -0
- package/src/tools/fs.ts +44 -0
- package/src/tools/git.ts +34 -0
- package/src/tools/github.ts +25 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/linear.ts +428 -0
- package/src/tools/process.ts +156 -0
- package/src/tools/terminal.ts +116 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { err, ok, type Result } from "neverthrow";
|
|
4
|
+
|
|
5
|
+
import type { RuntimeError } from "../../model/index.ts";
|
|
6
|
+
import { createFsTool, type FsTool } from "../../tools/index.ts";
|
|
7
|
+
import { DEFAULT_COMPACTION_CONFIG } from "./compaction.ts";
|
|
8
|
+
import type { OffloadPointer } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
// Large-output offload (design D10; task 7.2). A tool/step output larger than the configured size is
|
|
11
|
+
// written in FULL to a scratch file under `<root>/.orca/scratch/`, and only a short pointer reference
|
|
12
|
+
// is injected into context in its place — keeping one big payload from blowing the working window.
|
|
13
|
+
// A later step resolves the pointer back to the full payload. Scratch files are content-addressed
|
|
14
|
+
// (sha256 of the payload), so identical outputs dedupe to one file and the path is deterministic.
|
|
15
|
+
|
|
16
|
+
export interface OffloadOptions {
|
|
17
|
+
/** Working root; scratch payloads land under `<root>/.orca/scratch/`. */
|
|
18
|
+
readonly root: string;
|
|
19
|
+
/** Char length above which an output is offloaded; defaults to the aggressive D10 default. */
|
|
20
|
+
readonly thresholdChars?: number;
|
|
21
|
+
/** Injectable filesystem for tests; defaults to the real adapter. */
|
|
22
|
+
readonly fsTool?: FsTool;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What interception did to one output: either it stayed inline (under threshold), or its body was
|
|
27
|
+
* offloaded and replaced by a short `ref` string plus the structured `pointer` to resolve later.
|
|
28
|
+
*/
|
|
29
|
+
export type OffloadOutcome =
|
|
30
|
+
| { readonly offloaded: false; readonly content: string }
|
|
31
|
+
| { readonly offloaded: true; readonly ref: string; readonly pointer: OffloadPointer };
|
|
32
|
+
|
|
33
|
+
export interface OffloadStore {
|
|
34
|
+
/** Route one output: offload + return a pointer ref when oversized, otherwise pass it through. */
|
|
35
|
+
intercept(output: string): Promise<Result<OffloadOutcome, RuntimeError>>;
|
|
36
|
+
/** Resolve a pointer back to the full payload written at offload time. */
|
|
37
|
+
resolve(pointer: OffloadPointer): Promise<Result<string, RuntimeError>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The short reference injected into context in place of an offloaded payload. */
|
|
41
|
+
export function offloadRef(pointer: OffloadPointer): string {
|
|
42
|
+
return `⟦offloaded ${String(pointer.bytes)}B → ${offloadDisplayPath(pointer.path)}⟧`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createOffloadStore(options: OffloadOptions): OffloadStore {
|
|
46
|
+
const { root } = options;
|
|
47
|
+
const threshold = options.thresholdChars ?? DEFAULT_COMPACTION_CONFIG.offloadThresholdChars;
|
|
48
|
+
const fsTool = options.fsTool ?? createFsTool();
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
async intercept(output) {
|
|
52
|
+
if (output.length <= threshold) {
|
|
53
|
+
return ok({ offloaded: false, content: output }); // small enough — leave it in context
|
|
54
|
+
}
|
|
55
|
+
const hash = createHash("sha256").update(output).digest("hex").slice(0, 16);
|
|
56
|
+
const path = join(root, ".orca", "scratch", `offload-${hash}.txt`);
|
|
57
|
+
const written = await fsTool.writeText(path, output, { mode: 0o600 });
|
|
58
|
+
if (written.isErr()) {
|
|
59
|
+
return err(written.error);
|
|
60
|
+
}
|
|
61
|
+
const pointer: OffloadPointer = { path, bytes: Buffer.byteLength(output, "utf8") };
|
|
62
|
+
return ok({ offloaded: true, ref: offloadRef(pointer), pointer });
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async resolve(pointer) {
|
|
66
|
+
return fsTool.readText(pointer.path);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function offloadDisplayPath(path: string): string {
|
|
72
|
+
const normalized = path.replaceAll("\\", "/");
|
|
73
|
+
const marker = "/.orca/scratch/";
|
|
74
|
+
const index = normalized.lastIndexOf(marker);
|
|
75
|
+
if (index >= 0) {
|
|
76
|
+
return `.orca/scratch/${normalized.slice(index + marker.length)}`;
|
|
77
|
+
}
|
|
78
|
+
return normalized.split("/").at(-1) ?? "offload";
|
|
79
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Opt-in managed context (design D10): once enabled, staged compaction
|
|
2
|
+
// (mask -> prune -> summarize) by token pressure plus large-output offload to a
|
|
3
|
+
// scratch file with an injected pointer run automatically during the cycle.
|
|
4
|
+
|
|
5
|
+
/** Tunable thresholds; aggressive defaults apply once managed context is enabled (design D10). */
|
|
6
|
+
export interface CompactionConfig {
|
|
7
|
+
/** Small working-memory window: token pressure above which staged compaction begins. */
|
|
8
|
+
readonly workingWindowTokens: number;
|
|
9
|
+
/** Char length above which an output is offloaded to a scratch file + pointer. */
|
|
10
|
+
readonly offloadThresholdChars: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Pointer to an offloaded oversized output; resolves back to the full payload (see ./offload.ts). */
|
|
14
|
+
export interface OffloadPointer {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly bytes: number;
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Boundary bridge (design D2). Effect lives only inside src/loop/engine/**; everything that
|
|
2
|
+
// crosses to the public API or the flow-authoring surface is a neverthrow `Result` or a plain
|
|
3
|
+
// value. This module is the only seam between the two worlds:
|
|
4
|
+
// - inward : Result -> Effect via result.match(Effect.succeed, Effect.fail)
|
|
5
|
+
// - outward : Effect -> Effect<Result> via Effect.either + Either.match
|
|
6
|
+
// - boundary : Effect -> Promise<Result> (the single place an engine Effect is *run*)
|
|
7
|
+
// The facade gate (scripts/check-facade-gate.ts) skips src/loop/engine/**, so the Effect
|
|
8
|
+
// types named here never reach a scanned declaration.
|
|
9
|
+
import { Cause, Effect, Either, Exit } from "effect";
|
|
10
|
+
import { err, ok, type Result } from "neverthrow";
|
|
11
|
+
|
|
12
|
+
/** Inward bridge: lift a neverthrow `Result` into the Effect world. */
|
|
13
|
+
export function fromResult<A, E>(result: Result<A, E>): Effect.Effect<A, E> {
|
|
14
|
+
return result.match(
|
|
15
|
+
(value): Effect.Effect<A, E> => Effect.succeed(value),
|
|
16
|
+
(error): Effect.Effect<A, E> => Effect.fail(error),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Outward bridge: collapse an Effect's typed failure channel into a `Result`, still inside Effect. */
|
|
21
|
+
export function toResult<A, E>(effect: Effect.Effect<A, E>): Effect.Effect<Result<A, E>> {
|
|
22
|
+
return Effect.either(effect).pipe(
|
|
23
|
+
Effect.map((either) => Either.match(either, { onLeft: (e) => err(e), onRight: (a) => ok(a) })),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RunOptions {
|
|
28
|
+
/** When provided, aborting the signal interrupts the run; the resulting interruption becomes `err`. */
|
|
29
|
+
readonly signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Boundary runner — the single place an engine Effect is executed. Returns a `Result` and never
|
|
34
|
+
* throws an Effect, `Cause`, or interruption: typed failures become `err`, and interruption /
|
|
35
|
+
* defects are squashed into `err`, so no Effect type or `Cause` escapes the facade.
|
|
36
|
+
*
|
|
37
|
+
* Note on cancellation: engine work that wants a graceful `cancelled` outcome wires its own
|
|
38
|
+
* `AbortSignal` through `runCancellable` (which converts cancellation into a success value).
|
|
39
|
+
* The `signal` here is the blunt alternative — hard interruption surfaced as `err`.
|
|
40
|
+
*/
|
|
41
|
+
export async function runToResult<A, E>(
|
|
42
|
+
effect: Effect.Effect<A, E>,
|
|
43
|
+
options: RunOptions = {},
|
|
44
|
+
): Promise<Result<A, E | Error>> {
|
|
45
|
+
const exit = await Effect.runPromiseExit(effect, options.signal ? { signal: options.signal } : undefined);
|
|
46
|
+
return Exit.match(exit, {
|
|
47
|
+
onSuccess: (value): Result<A, E | Error> => ok(value),
|
|
48
|
+
onFailure: (cause): Result<A, E | Error> => {
|
|
49
|
+
const failure = Cause.failureOption(cause);
|
|
50
|
+
return failure._tag === "Some" ? err(failure.value) : err(causeToError(cause));
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function causeToError(cause: Cause.Cause<unknown>): Error {
|
|
56
|
+
if (Cause.isInterruptedOnly(cause)) {
|
|
57
|
+
return new Error("loop engine run was interrupted");
|
|
58
|
+
}
|
|
59
|
+
const squashed = Cause.squash(cause);
|
|
60
|
+
return squashed instanceof Error ? squashed : new Error(String(squashed));
|
|
61
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// INTERNAL loop engine — Effect lives here and ONLY here (design D2 facade gate).
|
|
2
|
+
//
|
|
3
|
+
// Public/authoring code never imports this module: src/loop/index.ts does NOT re-export it,
|
|
4
|
+
// and the facade gate (scripts/check-facade-gate.ts) skips src/loop/engine/**. Everything
|
|
5
|
+
// that crosses back out goes through ./bridge.ts as a neverthrow `Result`/plain value.
|
|
6
|
+
//
|
|
7
|
+
// This provides the three primitives the builder (L06) and fan-out / fan-in (L07) lower onto:
|
|
8
|
+
// 1. recurrence with variant-stop — runRecurrence (variant floor ∩ ceiling)
|
|
9
|
+
// 2. bounded structured concurrency — runBranches (Effect.all { concurrency })
|
|
10
|
+
// 3. structured cancellation — runCancellable (raceFirst → interrupt in-flight work)
|
|
11
|
+
// plus internal Layer-based DI that resolves the plain FlowContext snapshot captured at the
|
|
12
|
+
// boundary, without exposing any Effect type to the authoring accessors (design D2).
|
|
13
|
+
import { Context, Effect, Layer } from "effect";
|
|
14
|
+
import type { LoopStopReason } from "../builder/types.ts";
|
|
15
|
+
import type { Result } from "neverthrow";
|
|
16
|
+
import { currentFlowContext, type FlowContext } from "../../flow/context.ts";
|
|
17
|
+
import { runToResult } from "./bridge.ts";
|
|
18
|
+
|
|
19
|
+
export * from "./bridge.ts";
|
|
20
|
+
|
|
21
|
+
/** Engine-internal outcome; the builder (L06) maps this to the public Effect-free `LoopOutcome`. */
|
|
22
|
+
export interface EngineOutcome<S> {
|
|
23
|
+
readonly state: S;
|
|
24
|
+
readonly stopReason: LoopStopReason;
|
|
25
|
+
readonly iterations: number;
|
|
26
|
+
readonly measure: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One loop iteration as an internal Effect; the builder bridges plain step bodies inward via `fromResult`. */
|
|
30
|
+
export type IterationStep<S, R = never> = (state: S, iteration: number) => Effect.Effect<S, Error, R>;
|
|
31
|
+
|
|
32
|
+
export interface RecurrenceSpec<S, R = never> {
|
|
33
|
+
readonly initial: S;
|
|
34
|
+
readonly iterate: IterationStep<S, R>;
|
|
35
|
+
/** Loop variant (design D3): a measure bounded below that the loop drives toward `floor`. */
|
|
36
|
+
readonly measure: (state: S) => number;
|
|
37
|
+
/** Convergence floor; default 0. */
|
|
38
|
+
readonly floor?: number;
|
|
39
|
+
/** Iteration-ceiling seatbelt; default unbounded (termination then rests on the variant). */
|
|
40
|
+
readonly maxIterations?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Drive a stateful loop to convergence. The body runs at least once; the variant measure is
|
|
45
|
+
* checked after each iteration against the convergence floor and iteration ceiling, so whichever
|
|
46
|
+
* limit trips first stops the loop — `converged` when the measure won, `ceiling` otherwise.
|
|
47
|
+
*/
|
|
48
|
+
export function runRecurrence<S, R = never>(spec: RecurrenceSpec<S, R>): Effect.Effect<EngineOutcome<S>, Error, R> {
|
|
49
|
+
const floor = spec.floor ?? 0;
|
|
50
|
+
const maxIterations =
|
|
51
|
+
spec.maxIterations === undefined || !Number.isFinite(spec.maxIterations)
|
|
52
|
+
? Number.POSITIVE_INFINITY
|
|
53
|
+
: Math.max(0, Math.floor(spec.maxIterations));
|
|
54
|
+
|
|
55
|
+
const recur = (state: S, iterations: number): Effect.Effect<EngineOutcome<S>, Error, R> =>
|
|
56
|
+
Effect.suspend(() => {
|
|
57
|
+
const measure = spec.measure(state);
|
|
58
|
+
if (iterations > 0 && measure <= floor) {
|
|
59
|
+
return Effect.succeed({ state, stopReason: "converged", iterations, measure });
|
|
60
|
+
}
|
|
61
|
+
if (iterations > 0 && iterations >= maxIterations) {
|
|
62
|
+
return Effect.succeed({ state, stopReason: "ceiling", iterations, measure });
|
|
63
|
+
}
|
|
64
|
+
return spec.iterate(state, iterations).pipe(Effect.flatMap((next) => recur(next, iterations + 1)));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return recur(spec.initial, 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface BranchesSpec<A> {
|
|
71
|
+
readonly branches: readonly Effect.Effect<A, Error>[];
|
|
72
|
+
/** Bounded fan-out width (design D2/D8: in-process, ~3-10). */
|
|
73
|
+
readonly concurrency: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run branches with bounded structured concurrency. This is the single concurrency primitive
|
|
78
|
+
* L07's `fanOut`/`fanIn` build on: interrupting the parent fiber interrupts every in-flight
|
|
79
|
+
* branch automatically (Effect structured concurrency), which is what `runCancellable` exploits.
|
|
80
|
+
*/
|
|
81
|
+
export function runBranches<A>(spec: BranchesSpec<A>): Effect.Effect<readonly A[], Error> {
|
|
82
|
+
return Effect.all(spec.branches, { concurrency: spec.concurrency });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Plain-boundary bounded fan-out — the Effect-free seam L07's `fanOut` builds on (design D2). Each
|
|
87
|
+
* thunk is a self-contained unit of branch work that resolves to its own outcome value and MUST
|
|
88
|
+
* NOT reject: the caller captures branch success/failure AS DATA so a partial-failure policy can
|
|
89
|
+
* weigh it, and a thunk that throws anyway is squashed into the outer `err`. Branches run with the
|
|
90
|
+
* given concurrency cap (`runBranches` ⇒ `Effect.all { concurrency }`); an aborted `signal` hard-
|
|
91
|
+
* interrupts every in-flight branch via structured concurrency and surfaces as `err`. Returning a
|
|
92
|
+
* `Result` here means `src/loop/fanout.ts` never names an Effect type — Effect stays in the engine.
|
|
93
|
+
*/
|
|
94
|
+
export async function runBoundedBranches<A>(
|
|
95
|
+
thunks: readonly (() => Promise<A>)[],
|
|
96
|
+
concurrency: number,
|
|
97
|
+
signal?: AbortSignal,
|
|
98
|
+
): Promise<Result<readonly A[], Error>> {
|
|
99
|
+
const branches = thunks.map((thunk) =>
|
|
100
|
+
Effect.tryPromise({
|
|
101
|
+
try: () => thunk(),
|
|
102
|
+
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
return runToResult(runBranches({ branches, concurrency }), signal === undefined ? {} : { signal });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Make a unit of engine work cancellable by an `AbortSignal`. When the signal fires, the cancel
|
|
110
|
+
* branch wins the race and the `work` fiber — together with any in-flight fan-out children under
|
|
111
|
+
* it — is interrupted via structured concurrency; `onCancel()` then produces a `cancelled`
|
|
112
|
+
* outcome (it is a thunk so the caller can fold in last-known/partial state). With no signal the
|
|
113
|
+
* work runs unchanged.
|
|
114
|
+
*/
|
|
115
|
+
export function runCancellable<S>(
|
|
116
|
+
work: Effect.Effect<EngineOutcome<S>, Error>,
|
|
117
|
+
onCancel: () => EngineOutcome<S>,
|
|
118
|
+
signal?: AbortSignal,
|
|
119
|
+
): Effect.Effect<EngineOutcome<S>, Error> {
|
|
120
|
+
if (signal === undefined) {
|
|
121
|
+
return work;
|
|
122
|
+
}
|
|
123
|
+
const cancelled = abortEffect(signal).pipe(Effect.map(onCancel));
|
|
124
|
+
return Effect.raceFirst(work, cancelled);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function abortEffect(signal: AbortSignal): Effect.Effect<undefined> {
|
|
128
|
+
return Effect.async<undefined>((resume) => {
|
|
129
|
+
if (signal.aborted) {
|
|
130
|
+
resume(Effect.succeed(undefined));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const onAbort = (): void => {
|
|
134
|
+
resume(Effect.succeed(undefined));
|
|
135
|
+
};
|
|
136
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
137
|
+
return Effect.sync(() => {
|
|
138
|
+
signal.removeEventListener("abort", onAbort);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Internal Layer-based DI (design D2): Layer DI inside the engine, plain accessors outside. ---
|
|
144
|
+
|
|
145
|
+
/** Engine-internal DI tag carrying the plain `FlowContext` snapshot. */
|
|
146
|
+
export const FlowContextTag = Context.GenericTag<FlowContext>("orca/loop/FlowContext");
|
|
147
|
+
|
|
148
|
+
/** Build a Layer providing a captured `FlowContext` snapshot to engine effects. */
|
|
149
|
+
export function flowContextLayer(context: FlowContext): Layer.Layer<FlowContext> {
|
|
150
|
+
return Layer.succeed(FlowContextTag, context);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Provide the ambient flow context to an engine effect. The snapshot is read with the plain,
|
|
155
|
+
* Effect-free `currentFlowContext()` accessor at the boundary and handed to the engine as a
|
|
156
|
+
* Layer — so DI is Effect `Layer` internally while the authoring accessors stay plain.
|
|
157
|
+
*/
|
|
158
|
+
export function withAmbientFlowContext<A, E>(effect: Effect.Effect<A, E, FlowContext>): Effect.Effect<A, E> {
|
|
159
|
+
return Effect.provide(effect, flowContextLayer(currentFlowContext()));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// In-engine equivalents of the plain fs()/git()/llm() authoring accessors, resolved from the
|
|
163
|
+
// Layer. The public accessors stay Effect-free; these never leave src/loop/engine/**.
|
|
164
|
+
export const engineFs = Effect.map(FlowContextTag, (context) => context.fs);
|
|
165
|
+
export const engineGit = Effect.map(FlowContextTag, (context) => context.git);
|
|
166
|
+
export const engineLlm = Effect.map(FlowContextTag, (context) => context.llm);
|