@orc-brain/core 1.0.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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/dist/autoLoop.d.ts +70 -0
  3. package/dist/autoLoop.d.ts.map +1 -0
  4. package/dist/autoLoop.js +178 -0
  5. package/dist/autoLoop.js.map +1 -0
  6. package/dist/backpressure.d.ts +38 -0
  7. package/dist/backpressure.d.ts.map +1 -0
  8. package/dist/backpressure.js +103 -0
  9. package/dist/backpressure.js.map +1 -0
  10. package/dist/budgetTracker.d.ts +53 -0
  11. package/dist/budgetTracker.d.ts.map +1 -0
  12. package/dist/budgetTracker.js +93 -0
  13. package/dist/budgetTracker.js.map +1 -0
  14. package/dist/config.d.ts +9 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +107 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/escalation.d.ts +43 -0
  19. package/dist/escalation.d.ts.map +1 -0
  20. package/dist/escalation.js +81 -0
  21. package/dist/escalation.js.map +1 -0
  22. package/dist/eventBus.d.ts +30 -0
  23. package/dist/eventBus.d.ts.map +1 -0
  24. package/dist/eventBus.js +42 -0
  25. package/dist/eventBus.js.map +1 -0
  26. package/dist/goalJudge.d.ts +92 -0
  27. package/dist/goalJudge.d.ts.map +1 -0
  28. package/dist/goalJudge.js +254 -0
  29. package/dist/goalJudge.js.map +1 -0
  30. package/dist/index.d.ts +30 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +30 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/modelRouter.d.ts +40 -0
  35. package/dist/modelRouter.d.ts.map +1 -0
  36. package/dist/modelRouter.js +114 -0
  37. package/dist/modelRouter.js.map +1 -0
  38. package/dist/orchestrator.d.ts +242 -0
  39. package/dist/orchestrator.d.ts.map +1 -0
  40. package/dist/orchestrator.js +975 -0
  41. package/dist/orchestrator.js.map +1 -0
  42. package/dist/pacing.d.ts +34 -0
  43. package/dist/pacing.d.ts.map +1 -0
  44. package/dist/pacing.js +49 -0
  45. package/dist/pacing.js.map +1 -0
  46. package/dist/planValidation.d.ts +24 -0
  47. package/dist/planValidation.d.ts.map +1 -0
  48. package/dist/planValidation.js +134 -0
  49. package/dist/planValidation.js.map +1 -0
  50. package/dist/planner.d.ts +70 -0
  51. package/dist/planner.d.ts.map +1 -0
  52. package/dist/planner.js +210 -0
  53. package/dist/planner.js.map +1 -0
  54. package/dist/plugins/host.d.ts +39 -0
  55. package/dist/plugins/host.d.ts.map +1 -0
  56. package/dist/plugins/host.js +109 -0
  57. package/dist/plugins/host.js.map +1 -0
  58. package/dist/plugins/registry.d.ts +71 -0
  59. package/dist/plugins/registry.d.ts.map +1 -0
  60. package/dist/plugins/registry.js +269 -0
  61. package/dist/plugins/registry.js.map +1 -0
  62. package/dist/plugins/secrets.d.ts +30 -0
  63. package/dist/plugins/secrets.d.ts.map +1 -0
  64. package/dist/plugins/secrets.js +99 -0
  65. package/dist/plugins/secrets.js.map +1 -0
  66. package/dist/preflight.d.ts +38 -0
  67. package/dist/preflight.d.ts.map +1 -0
  68. package/dist/preflight.js +108 -0
  69. package/dist/preflight.js.map +1 -0
  70. package/dist/reporting.d.ts +41 -0
  71. package/dist/reporting.d.ts.map +1 -0
  72. package/dist/reporting.js +229 -0
  73. package/dist/reporting.js.map +1 -0
  74. package/dist/safety/denyRules.d.ts +57 -0
  75. package/dist/safety/denyRules.d.ts.map +1 -0
  76. package/dist/safety/denyRules.js +498 -0
  77. package/dist/safety/denyRules.js.map +1 -0
  78. package/dist/safety/envClassifier.d.ts +45 -0
  79. package/dist/safety/envClassifier.d.ts.map +1 -0
  80. package/dist/safety/envClassifier.js +99 -0
  81. package/dist/safety/envClassifier.js.map +1 -0
  82. package/dist/safety/index.d.ts +83 -0
  83. package/dist/safety/index.d.ts.map +1 -0
  84. package/dist/safety/index.js +184 -0
  85. package/dist/safety/index.js.map +1 -0
  86. package/dist/safety/limitSignals.d.ts +36 -0
  87. package/dist/safety/limitSignals.d.ts.map +1 -0
  88. package/dist/safety/limitSignals.js +79 -0
  89. package/dist/safety/limitSignals.js.map +1 -0
  90. package/dist/safety/paths.d.ts +32 -0
  91. package/dist/safety/paths.d.ts.map +1 -0
  92. package/dist/safety/paths.js +102 -0
  93. package/dist/safety/paths.js.map +1 -0
  94. package/dist/safety/redact.d.ts +17 -0
  95. package/dist/safety/redact.d.ts.map +1 -0
  96. package/dist/safety/redact.js +85 -0
  97. package/dist/safety/redact.js.map +1 -0
  98. package/dist/spawnEnv.d.ts +18 -0
  99. package/dist/spawnEnv.d.ts.map +1 -0
  100. package/dist/spawnEnv.js +43 -0
  101. package/dist/spawnEnv.js.map +1 -0
  102. package/dist/store/auditLog.d.ts +28 -0
  103. package/dist/store/auditLog.d.ts.map +1 -0
  104. package/dist/store/auditLog.js +48 -0
  105. package/dist/store/auditLog.js.map +1 -0
  106. package/dist/store/index.d.ts +140 -0
  107. package/dist/store/index.d.ts.map +1 -0
  108. package/dist/store/index.js +648 -0
  109. package/dist/store/index.js.map +1 -0
  110. package/dist/store/schema.d.ts +17 -0
  111. package/dist/store/schema.d.ts.map +1 -0
  112. package/dist/store/schema.js +197 -0
  113. package/dist/store/schema.js.map +1 -0
  114. package/dist/system.d.ts +69 -0
  115. package/dist/system.d.ts.map +1 -0
  116. package/dist/system.js +135 -0
  117. package/dist/system.js.map +1 -0
  118. package/dist/workerManager.d.ts +80 -0
  119. package/dist/workerManager.d.ts.map +1 -0
  120. package/dist/workerManager.js +280 -0
  121. package/dist/workerManager.js.map +1 -0
  122. package/dist/worktrees.d.ts +74 -0
  123. package/dist/worktrees.d.ts.map +1 -0
  124. package/dist/worktrees.js +210 -0
  125. package/dist/worktrees.js.map +1 -0
  126. package/package.json +48 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paulo Lima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Autonomous outer-loop controller (.specs/active/autonomous-loop.md §3.1, G4).
3
+ * Sits above the event-driven dispatch `tick`: when the orchestrator reports a
4
+ * run has quiesced (all tasks terminal), this decides whether the goal is
5
+ * satisfied (finish), needs more work (re-plan + grow the DAG), or has hit a
6
+ * guard (pause). It never widens a boundary on its own — safety escalations,
7
+ * scope boundaries, and the budget still bind (§17 design goal #3, Golden Rules).
8
+ *
9
+ * Deterministic control flow; the only LLM calls are the injected Planner
10
+ * (`replan`) and the {@link GoalJudge}. Runaway is bounded three ways (G5): the
11
+ * budget hard-stop (existing), a `max_replan_cycles` ceiling, and no-progress
12
+ * detection (a cycle that appends zero tasks pauses the run).
13
+ */
14
+ import type { Environment, OrchestratorConfig, Plan } from "@orc-brain/shared";
15
+ import type { Store } from "./store/index.js";
16
+ import type { EventBus } from "./eventBus.js";
17
+ import type { Planner } from "./planner.js";
18
+ import type { GoalJudge } from "./goalJudge.js";
19
+ import type { AutoLoopHook } from "./orchestrator.js";
20
+ /** The slice of the orchestrator the controller drives (avoids a hard cycle). */
21
+ export interface AutoLoopOrchestrator {
22
+ applyReplan(goalId: string, plan: Plan): {
23
+ scopes: {
24
+ id: string;
25
+ }[];
26
+ tasks: {
27
+ id: string;
28
+ }[];
29
+ };
30
+ approveScope(scopeId: string): void;
31
+ finalizeRun(runId: string, state: "done" | "failed", reason?: string): void;
32
+ parkRun(runId: string, reason: string): void;
33
+ tick(runId: string): Promise<void>;
34
+ }
35
+ /** Collaborators for the controller. */
36
+ export interface AutoLoopDeps {
37
+ store: Store;
38
+ bus: EventBus;
39
+ config: OrchestratorConfig;
40
+ planner: Planner;
41
+ judge: GoalJudge;
42
+ orchestrator: AutoLoopOrchestrator;
43
+ /** Environment classifier for the judge's deterministic pass (injectable). */
44
+ classifyEnv?: (cwd: string) => Environment;
45
+ }
46
+ /** The autonomous controller (autonomous-loop.md §3.1). */
47
+ export declare class AutoLoop implements AutoLoopHook {
48
+ private readonly deps;
49
+ /** Runs mid-cycle, so a re-entrant quiesce notification is ignored. */
50
+ private readonly busy;
51
+ /** Per-run mode override (POST /runs/:id/mode); falls back to config (G6). */
52
+ private readonly modeOverride;
53
+ constructor(deps: AutoLoopDeps);
54
+ /** Sets the approval mode for a specific run at runtime (G6, Phase F). */
55
+ setMode(runId: string, mode: "supervised" | "unattended"): void;
56
+ private modeFor;
57
+ private classifyEnv;
58
+ /** Orchestrator hook: a run reached quiescence. Fire-and-forget one cycle. */
59
+ onRunQuiesced(runId: string): void;
60
+ /**
61
+ * One evaluate → replan-or-finish cycle (autonomous-loop.md §3.1). Assumes the
62
+ * DAG is quiesced (the orchestrator only calls `onRunQuiesced` then).
63
+ */
64
+ private runCycle;
65
+ /** Goal + scope success-criteria descriptions, aggregated for the judge. */
66
+ private aggregateCriteria;
67
+ /** Digest of completed scopes' task results for the re-plan prompt (§3.3). */
68
+ private buildDigest;
69
+ }
70
+ //# sourceMappingURL=autoLoop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoLoop.d.ts","sourceRoot":"","sources":["../src/autoLoop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,WAAW,CACT,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,GACT;QAAE,MAAM,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;IACzD,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,QAAQ,CAAC;IACd,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,YAAY,EAAE,oBAAoB,CAAC;IACnC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,WAAW,CAAC;CAC5C;AAsBD,2DAA2D;AAC3D,qBAAa,QAAS,YAAW,YAAY;IAS/B,OAAO,CAAC,QAAQ,CAAC,IAAI;IARjC,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAGzB;gBAEyB,IAAI,EAAE,YAAY;IAE/C,0EAA0E;IAC1E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,YAAY,GAAG,IAAI;IAI/D,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,WAAW;IAUnB,8EAA8E;IAC9E,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAMlC;;;OAGG;YACW,QAAQ;IA8EtB,4EAA4E;IAC5E,OAAO,CAAC,iBAAiB;IASzB,8EAA8E;IAC9E,OAAO,CAAC,WAAW;CAapB"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Autonomous outer-loop controller (.specs/active/autonomous-loop.md §3.1, G4).
3
+ * Sits above the event-driven dispatch `tick`: when the orchestrator reports a
4
+ * run has quiesced (all tasks terminal), this decides whether the goal is
5
+ * satisfied (finish), needs more work (re-plan + grow the DAG), or has hit a
6
+ * guard (pause). It never widens a boundary on its own — safety escalations,
7
+ * scope boundaries, and the budget still bind (§17 design goal #3, Golden Rules).
8
+ *
9
+ * Deterministic control flow; the only LLM calls are the injected Planner
10
+ * (`replan`) and the {@link GoalJudge}. Runaway is bounded three ways (G5): the
11
+ * budget hard-stop (existing), a `max_replan_cycles` ceiling, and no-progress
12
+ * detection (a cycle that appends zero tasks pauses the run).
13
+ */
14
+ import { execFileSync } from "node:child_process";
15
+ import { classifyEnvironment } from "./safety/envClassifier.js";
16
+ /** Reads the current git branch of a directory, or undefined if not a repo. */
17
+ function gitBranch(cwd) {
18
+ try {
19
+ return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
20
+ cwd,
21
+ encoding: "utf8",
22
+ stdio: ["ignore", "pipe", "ignore"],
23
+ }).trim();
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /** Truncates a value to a short single-line digest string (Open Decision 3). */
30
+ function digestLine(value, cap = 500) {
31
+ const s = typeof value === "string" ? value : JSON.stringify(value ?? "");
32
+ const oneLine = s.replace(/\s+/g, " ").trim();
33
+ return oneLine.length > cap ? oneLine.slice(0, cap) + "…" : oneLine;
34
+ }
35
+ /** The autonomous controller (autonomous-loop.md §3.1). */
36
+ export class AutoLoop {
37
+ deps;
38
+ /** Runs mid-cycle, so a re-entrant quiesce notification is ignored. */
39
+ busy = new Set();
40
+ /** Per-run mode override (POST /runs/:id/mode); falls back to config (G6). */
41
+ modeOverride = new Map();
42
+ constructor(deps) {
43
+ this.deps = deps;
44
+ }
45
+ /** Sets the approval mode for a specific run at runtime (G6, Phase F). */
46
+ setMode(runId, mode) {
47
+ this.modeOverride.set(runId, mode);
48
+ }
49
+ modeFor(runId) {
50
+ const override = this.modeOverride.get(runId);
51
+ if (override)
52
+ return override;
53
+ // Feature-flow runs (spec 002 §R5) are unattended by default: the single
54
+ // human gate was the initial plan approval. Durable across restarts.
55
+ if (this.deps.store.getRun(runId)?.auto_loop)
56
+ return "unattended";
57
+ return this.deps.config.autoLoop.mode;
58
+ }
59
+ classifyEnv(cwd, baseBranch) {
60
+ if (this.deps.classifyEnv)
61
+ return this.deps.classifyEnv(cwd);
62
+ // A run with a recorded base branch classifies against it (spec 002 §R10),
63
+ // so worktree runs never classify as their `orc/…` branches.
64
+ return classifyEnvironment({ declared: "unknown", branch: baseBranch ?? gitBranch(cwd) }, this.deps.config.safety).environment;
65
+ }
66
+ /** Orchestrator hook: a run reached quiescence. Fire-and-forget one cycle. */
67
+ onRunQuiesced(runId) {
68
+ if (this.busy.has(runId))
69
+ return;
70
+ this.busy.add(runId);
71
+ void this.runCycle(runId).finally(() => this.busy.delete(runId));
72
+ }
73
+ /**
74
+ * One evaluate → replan-or-finish cycle (autonomous-loop.md §3.1). Assumes the
75
+ * DAG is quiesced (the orchestrator only calls `onRunQuiesced` then).
76
+ */
77
+ async runCycle(runId) {
78
+ const run = this.deps.store.getRun(runId);
79
+ if (!run || run.state !== "running")
80
+ return;
81
+ const goal = this.deps.store.getGoal(run.goal_id);
82
+ if (!goal)
83
+ return;
84
+ // 1. Evaluate goal satisfaction (hybrid: deterministic + judge).
85
+ const verdict = await this.deps.judge.evaluate({
86
+ run_id: runId,
87
+ goal_id: goal.id,
88
+ title: goal.title,
89
+ objective: goal.objective,
90
+ cwd: goal.repo_root,
91
+ environment: this.classifyEnv(goal.repo_root, run.base_branch),
92
+ criteria: this.aggregateCriteria(goal.id),
93
+ });
94
+ this.deps.bus.publish({
95
+ type: "goal_evaluated",
96
+ run_id: runId,
97
+ payload: {
98
+ satisfied: verdict.satisfied,
99
+ unmet: verdict.unmet,
100
+ rationale: verdict.rationale,
101
+ },
102
+ });
103
+ // 2. Satisfied → finish.
104
+ if (verdict.satisfied) {
105
+ this.deps.orchestrator.finalizeRun(runId, "done");
106
+ return;
107
+ }
108
+ // 3. Guard: re-plan cycle ceiling (G5).
109
+ if (run.replan_cycle >= this.deps.config.autoLoop.max_replan_cycles) {
110
+ this.deps.orchestrator.parkRun(runId, "cycle_cap");
111
+ return;
112
+ }
113
+ // 4. Re-plan to close the gap.
114
+ let plan;
115
+ try {
116
+ plan = await this.deps.planner.replan(goal, {
117
+ completedDigest: this.buildDigest(goal.id),
118
+ unmetCriteria: verdict.unmet,
119
+ });
120
+ }
121
+ catch {
122
+ this.deps.orchestrator.parkRun(runId, "replan_failed");
123
+ return;
124
+ }
125
+ const { scopes, tasks } = this.deps.orchestrator.applyReplan(goal.id, plan);
126
+ const nextCycle = run.replan_cycle + 1;
127
+ this.deps.bus.publish({
128
+ type: "replan_cycle",
129
+ run_id: runId,
130
+ payload: {
131
+ cycle: nextCycle,
132
+ added_scopes: scopes.length,
133
+ added_tasks: tasks.length,
134
+ },
135
+ });
136
+ // 5. Guard: no progress (G5) — a cycle that adds no work never converges.
137
+ if (tasks.length === 0) {
138
+ this.deps.orchestrator.parkRun(runId, "no_progress");
139
+ return;
140
+ }
141
+ this.deps.store.updateRun(runId, { replan_cycle: nextCycle });
142
+ // 6. Approve the new scopes per mode (G6), then resume dispatch.
143
+ if (this.modeFor(runId) === "unattended") {
144
+ for (const s of scopes)
145
+ this.deps.orchestrator.approveScope(s.id);
146
+ void this.deps.orchestrator.tick(runId);
147
+ }
148
+ else {
149
+ // Supervised: leave new scopes `proposed`; pause for operator approval.
150
+ this.deps.orchestrator.parkRun(runId, "awaiting_replan_approval");
151
+ }
152
+ }
153
+ /** Goal + scope success-criteria descriptions, aggregated for the judge. */
154
+ aggregateCriteria(goalId) {
155
+ const goal = this.deps.store.getGoal(goalId);
156
+ const criteria = (goal?.success_criteria ?? []).map((c) => c.description);
157
+ for (const scope of this.deps.store.listScopesByGoal(goalId)) {
158
+ for (const c of scope.success_criteria)
159
+ criteria.push(c.description);
160
+ }
161
+ return criteria;
162
+ }
163
+ /** Digest of completed scopes' task results for the re-plan prompt (§3.3). */
164
+ buildDigest(goalId) {
165
+ const lines = [];
166
+ for (const scope of this.deps.store.listScopesByGoal(goalId)) {
167
+ if (scope.status !== "done" && scope.status !== "failed")
168
+ continue;
169
+ for (const task of this.deps.store.listTasksByScope(scope.id)) {
170
+ if (task.result_summary == null)
171
+ continue;
172
+ lines.push(`${scope.name}/${task.title}: ${digestLine(task.result_summary)}`);
173
+ }
174
+ }
175
+ return lines;
176
+ }
177
+ }
178
+ //# sourceMappingURL=autoLoop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoLoop.js","sourceRoot":"","sources":["../src/autoLoop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Rate-limit backpressure (§7.4). Turns a detected {@link LimitSignal} into a
3
+ * scheduling constraint: a session/weekly/unknown limit pauses *all* dispatch
4
+ * until the reset; a per-model limit only quarantines that model (router R7
5
+ * routes around it). Reset times are used when the CLI text carried one,
6
+ * otherwise an exponential backoff with a cap. Engage/clear are surfaced as
7
+ * `limit.backpressure` events with a countdown for the UI/CLI.
8
+ */
9
+ import type { LimitConfig, ModelName } from "@orc-brain/shared";
10
+ import type { EventBus } from "./eventBus.js";
11
+ import { type LimitSignal } from "./safety/limitSignals.js";
12
+ /** Scheduler-facing rate-limit state for one orchestrator. */
13
+ export declare class Backpressure {
14
+ private readonly bus;
15
+ private readonly config;
16
+ private readonly now;
17
+ private global;
18
+ private readonly models;
19
+ /** Consecutive limit hits, for backoff escalation when no reset is parsed. */
20
+ private consecutive;
21
+ /** Re-kick the dispatch loop when a hold expires. */
22
+ onClear?: (runId: string) => void;
23
+ private readonly timers;
24
+ constructor(bus: EventBus, config: LimitConfig, now?: () => number);
25
+ /** Engages backpressure for a detected signal, scheduling its auto-clear. */
26
+ engage(signal: LimitSignal, runId: string): void;
27
+ /** True when all new dispatch must halt (global hold active). */
28
+ isDispatchBlocked(): boolean;
29
+ /** Models currently quarantined by a per-model limit (router R7 input). */
30
+ quarantinedModels(): ModelName[];
31
+ /** Epoch ms the global hold clears at, if any (for status/countdown). */
32
+ globalResetsAt(): number | undefined;
33
+ /** Cancels all pending timers (shutdown). */
34
+ stopAll(): void;
35
+ private schedule;
36
+ private emit;
37
+ }
38
+ //# sourceMappingURL=backpressure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backpressure.d.ts","sourceRoot":"","sources":["../src/backpressure.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAQ5E,8DAA8D;AAC9D,qBAAa,YAAY;IAUrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAXtB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD,8EAA8E;IAC9E,OAAO,CAAC,WAAW,CAAK;IACxB,qDAAqD;IACrD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;gBAGhD,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,WAAW,EACnB,GAAG,GAAE,MAAM,MAAyB;IAGvD,6EAA6E;IAC7E,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAkChD,iEAAiE;IACjE,iBAAiB,IAAI,OAAO;IAS5B,2EAA2E;IAC3E,iBAAiB,IAAI,SAAS,EAAE;IAUhC,yEAAyE;IACzE,cAAc,IAAI,MAAM,GAAG,SAAS;IAIpC,6CAA6C;IAC7C,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,IAAI;CAab"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Rate-limit backpressure (§7.4). Turns a detected {@link LimitSignal} into a
3
+ * scheduling constraint: a session/weekly/unknown limit pauses *all* dispatch
4
+ * until the reset; a per-model limit only quarantines that model (router R7
5
+ * routes around it). Reset times are used when the CLI text carried one,
6
+ * otherwise an exponential backoff with a cap. Engage/clear are surfaced as
7
+ * `limit.backpressure` events with a countdown for the UI/CLI.
8
+ */
9
+ import { backoffDelayMs } from "./safety/limitSignals.js";
10
+ /** Scheduler-facing rate-limit state for one orchestrator. */
11
+ export class Backpressure {
12
+ bus;
13
+ config;
14
+ now;
15
+ global = null;
16
+ models = new Map();
17
+ /** Consecutive limit hits, for backoff escalation when no reset is parsed. */
18
+ consecutive = 0;
19
+ /** Re-kick the dispatch loop when a hold expires. */
20
+ onClear;
21
+ timers = new Set();
22
+ constructor(bus, config, now = () => Date.now()) {
23
+ this.bus = bus;
24
+ this.config = config;
25
+ this.now = now;
26
+ }
27
+ /** Engages backpressure for a detected signal, scheduling its auto-clear. */
28
+ engage(signal, runId) {
29
+ const t = this.now();
30
+ const until = signal.resets_at && signal.resets_at > t
31
+ ? signal.resets_at
32
+ : t + backoffDelayMs(this.consecutive++, this.config);
33
+ const delay = Math.max(0, until - t);
34
+ if (signal.kind === "model_limit" && signal.model) {
35
+ this.models.set(signal.model, { until, reason: signal.raw });
36
+ this.emit(true, "model", signal.raw, until, signal.model);
37
+ this.schedule(delay, () => {
38
+ this.models.delete(signal.model);
39
+ this.emit(false, "model", "model limit cleared", undefined, signal.model);
40
+ this.onClear?.(runId);
41
+ });
42
+ }
43
+ else {
44
+ this.global = { until, reason: signal.raw };
45
+ this.emit(true, "global", signal.raw, until);
46
+ this.schedule(delay, () => {
47
+ this.global = null;
48
+ this.consecutive = 0;
49
+ this.emit(false, "global", "backpressure cleared");
50
+ this.onClear?.(runId);
51
+ });
52
+ }
53
+ }
54
+ /** True when all new dispatch must halt (global hold active). */
55
+ isDispatchBlocked() {
56
+ if (!this.global)
57
+ return false;
58
+ if (this.global.until <= this.now()) {
59
+ this.global = null;
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ /** Models currently quarantined by a per-model limit (router R7 input). */
65
+ quarantinedModels() {
66
+ const t = this.now();
67
+ const out = [];
68
+ for (const [model, hold] of this.models) {
69
+ if (hold.until > t)
70
+ out.push(model);
71
+ else
72
+ this.models.delete(model);
73
+ }
74
+ return out;
75
+ }
76
+ /** Epoch ms the global hold clears at, if any (for status/countdown). */
77
+ globalResetsAt() {
78
+ return this.global?.until;
79
+ }
80
+ /** Cancels all pending timers (shutdown). */
81
+ stopAll() {
82
+ for (const t of this.timers)
83
+ clearTimeout(t);
84
+ this.timers.clear();
85
+ }
86
+ schedule(delayMs, fn) {
87
+ const timer = setTimeout(() => {
88
+ this.timers.delete(timer);
89
+ fn();
90
+ }, delayMs);
91
+ if (typeof timer.unref === "function")
92
+ timer.unref();
93
+ this.timers.add(timer);
94
+ }
95
+ emit(engaged, scope, reason, resetsAt, model) {
96
+ this.bus.publish({
97
+ type: "limit.backpressure",
98
+ run_id: null,
99
+ payload: { engaged, scope, model, reason, resets_at: resetsAt },
100
+ });
101
+ }
102
+ }
103
+ //# sourceMappingURL=backpressure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backpressure.js","sourceRoot":"","sources":["../src/backpressure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Usage & budget tracker (§7). This is an *estimator plus a backpressure
3
+ * system*, not a meter: under subscription auth `total_cost_usd` is an
4
+ * estimated USD-equivalent, not a billed amount. It provides (a) relative
5
+ * consumption per run/scope/task, (b) a spend-equivalent ceiling, and (c)
6
+ * graceful reaction to real rate-limit signals (handled in limitSignals).
7
+ */
8
+ import type { BudgetConfig, BudgetState } from "@orc-brain/shared";
9
+ import type { Store } from "./store/index.js";
10
+ import type { EventBus } from "./eventBus.js";
11
+ /** The subset of an SDK result message the ledger needs (§7.1). */
12
+ export interface ResultUsageLike {
13
+ total_cost_usd?: number;
14
+ num_turns?: number;
15
+ usage?: {
16
+ input_tokens?: number;
17
+ output_tokens?: number;
18
+ cache_read_input_tokens?: number;
19
+ cache_creation_input_tokens?: number;
20
+ } | null;
21
+ }
22
+ /** Computes the budget state from spend against thresholds (§7.3). */
23
+ export declare function computeBudgetState(spent: number, budget: number, config: BudgetConfig): BudgetState;
24
+ /**
25
+ * Per-task hard bound (§7.2, Open Decision 6): scope budget ÷ task count,
26
+ * clamped to `[per_task_min_usd, per_task_max_usd]`. Passed to the worker as
27
+ * the SDK `maxBudgetUsd` option.
28
+ */
29
+ export declare function perTaskBudgetUsd(scopeBudgetUsd: number, taskCount: number, config: BudgetConfig): number;
30
+ /** Ledger recorder + threshold enforcer for a single run. */
31
+ export declare class BudgetTracker {
32
+ private readonly store;
33
+ private readonly bus;
34
+ private readonly config;
35
+ constructor(store: Store, bus: EventBus, config: BudgetConfig);
36
+ /**
37
+ * Records one result message to the ledger (one row per message — the source
38
+ * of truth for aggregation, §7.1), recomputes the run's spend and budget
39
+ * state, and emits `budget.tick`. Returns the new budget state.
40
+ */
41
+ recordResult(args: {
42
+ run_id: string;
43
+ task_id: string;
44
+ session_id: string | null;
45
+ model: string;
46
+ result: ResultUsageLike;
47
+ }): BudgetState;
48
+ /** Recomputes spend + state for a run from the ledger and emits a tick (§7.3). */
49
+ refresh(runId: string): BudgetState;
50
+ /** True when dispatch must halt because the run hit the hard stop (§7.3). */
51
+ isStopped(runId: string): boolean;
52
+ }
53
+ //# sourceMappingURL=budgetTracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgetTracker.d.ts","sourceRoot":"","sources":["../src/budgetTracker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE;QACN,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;QACjC,2BAA2B,CAAC,EAAE,MAAM,CAAC;KACtC,GAAG,IAAI,CAAC;CACV;AAED,sEAAsE;AACtE,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,GACnB,WAAW,CAMb;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,GACnB,MAAM,CAMR;AAED,6DAA6D;AAC7D,qBAAa,aAAa;IAEtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAFN,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,YAAY;IAGvC;;;;OAIG;IACH,YAAY,CAAC,IAAI,EAAE;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,eAAe,CAAC;KACzB,GAAG,WAAW;IAuBf,kFAAkF;IAClF,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW;IAuBnC,6EAA6E;IAC7E,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;CAIlC"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Usage & budget tracker (§7). This is an *estimator plus a backpressure
3
+ * system*, not a meter: under subscription auth `total_cost_usd` is an
4
+ * estimated USD-equivalent, not a billed amount. It provides (a) relative
5
+ * consumption per run/scope/task, (b) a spend-equivalent ceiling, and (c)
6
+ * graceful reaction to real rate-limit signals (handled in limitSignals).
7
+ */
8
+ /** Computes the budget state from spend against thresholds (§7.3). */
9
+ export function computeBudgetState(spent, budget, config) {
10
+ if (budget <= 0)
11
+ return "ok";
12
+ const frac = spent / budget;
13
+ if (frac >= config.hard_stop_at)
14
+ return "stopped";
15
+ if (frac >= config.warn_at)
16
+ return "warn";
17
+ return "ok";
18
+ }
19
+ /**
20
+ * Per-task hard bound (§7.2, Open Decision 6): scope budget ÷ task count,
21
+ * clamped to `[per_task_min_usd, per_task_max_usd]`. Passed to the worker as
22
+ * the SDK `maxBudgetUsd` option.
23
+ */
24
+ export function perTaskBudgetUsd(scopeBudgetUsd, taskCount, config) {
25
+ const raw = taskCount > 0 ? scopeBudgetUsd / taskCount : scopeBudgetUsd;
26
+ return Math.min(config.per_task_max_usd, Math.max(config.per_task_min_usd, raw));
27
+ }
28
+ /** Ledger recorder + threshold enforcer for a single run. */
29
+ export class BudgetTracker {
30
+ store;
31
+ bus;
32
+ config;
33
+ constructor(store, bus, config) {
34
+ this.store = store;
35
+ this.bus = bus;
36
+ this.config = config;
37
+ }
38
+ /**
39
+ * Records one result message to the ledger (one row per message — the source
40
+ * of truth for aggregation, §7.1), recomputes the run's spend and budget
41
+ * state, and emits `budget.tick`. Returns the new budget state.
42
+ */
43
+ recordResult(args) {
44
+ const u = args.result.usage ?? {};
45
+ this.store.insertLedgerEntry({
46
+ run_id: args.run_id,
47
+ task_id: args.task_id,
48
+ session_id: args.session_id,
49
+ cost_usd: args.result.total_cost_usd ?? 0,
50
+ num_turns: args.result.num_turns ?? 0,
51
+ model: args.model,
52
+ tokens_in: u.input_tokens ?? 0,
53
+ tokens_out: u.output_tokens ?? 0,
54
+ cache_read: u.cache_read_input_tokens ?? 0,
55
+ cache_write: u.cache_creation_input_tokens ?? 0,
56
+ recorded_at: new Date().toISOString(),
57
+ });
58
+ // Roll the task's own cost forward too, for per-task display.
59
+ const taskCost = this.store.sumCostForTask(args.task_id);
60
+ this.store.updateTask(args.task_id, { cost_usd: taskCost });
61
+ return this.refresh(args.run_id);
62
+ }
63
+ /** Recomputes spend + state for a run from the ledger and emits a tick (§7.3). */
64
+ refresh(runId) {
65
+ const run = this.store.getRun(runId);
66
+ if (!run)
67
+ return "ok";
68
+ const spent = this.store.sumCostForRun(runId);
69
+ const state = computeBudgetState(spent, run.budget_usd, this.config);
70
+ this.store.updateRun(runId, {
71
+ budget_spent_usd: spent,
72
+ budget_state: state,
73
+ });
74
+ this.bus.publish({
75
+ type: "budget.tick",
76
+ run_id: runId,
77
+ payload: {
78
+ budget_usd: run.budget_usd,
79
+ spent_usd: spent,
80
+ state,
81
+ warn_at: this.config.warn_at,
82
+ stop_at: this.config.hard_stop_at,
83
+ },
84
+ });
85
+ return state;
86
+ }
87
+ /** True when dispatch must halt because the run hit the hard stop (§7.3). */
88
+ isStopped(runId) {
89
+ const run = this.store.getRun(runId);
90
+ return run?.budget_state === "stopped";
91
+ }
92
+ }
93
+ //# sourceMappingURL=budgetTracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgetTracker.js","sourceRoot":"","sources":["../src/budgetTracker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Default orchestrator configuration (§6, §7, §8). These are the recommended
3
+ * defaults from §14; `orchestrator.toml` overrides land in Phase 2. Values are
4
+ * exported so the router, budget tracker, and safety layer share one source.
5
+ */
6
+ import type { OrchestratorConfig } from "@orc-brain/shared";
7
+ /** Recommended defaults (§14: concurrency 3, warn 70%, hard-stop 90%). */
8
+ export declare const DEFAULT_CONFIG: OrchestratorConfig;
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,0EAA0E;AAC1E,eAAO,MAAM,cAAc,EAAE,kBAmG5B,CAAC"}