@alisio/core 0.1.0-alpha.9 → 0.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.
Files changed (187) hide show
  1. package/README.md +52 -7
  2. package/dist/agents/active.d.ts +99 -0
  3. package/dist/agents/active.js +218 -0
  4. package/dist/agents/capabilities.d.ts +26 -0
  5. package/dist/agents/capabilities.js +120 -0
  6. package/dist/agents/create-agent-skill.d.ts +11 -0
  7. package/dist/agents/create-agent-skill.js +60 -0
  8. package/dist/agents/definitions.d.ts +88 -0
  9. package/dist/agents/definitions.js +404 -0
  10. package/dist/agents/draft.d.ts +41 -0
  11. package/dist/agents/draft.js +133 -0
  12. package/dist/agents/service.d.ts +57 -0
  13. package/dist/agents/service.js +86 -0
  14. package/dist/agents/templates.d.ts +8 -0
  15. package/dist/agents/templates.js +102 -0
  16. package/dist/analysis/capabilities.d.ts +48 -0
  17. package/dist/analysis/capabilities.js +90 -0
  18. package/dist/analysis/data/csv.d.ts +46 -0
  19. package/dist/analysis/data/csv.js +240 -0
  20. package/dist/analysis/data/datasets.d.ts +132 -0
  21. package/dist/analysis/data/datasets.js +441 -0
  22. package/dist/analysis/data/describe.d.ts +27 -0
  23. package/dist/analysis/data/describe.js +103 -0
  24. package/dist/analysis/data/engine-client.d.ts +58 -0
  25. package/dist/analysis/data/engine-client.js +304 -0
  26. package/dist/analysis/data/engine-entry.d.ts +1 -0
  27. package/dist/analysis/data/engine-entry.js +59 -0
  28. package/dist/analysis/data/engine-source.d.ts +4 -0
  29. package/dist/analysis/data/engine-source.js +6 -0
  30. package/dist/analysis/data/engine-types.d.ts +86 -0
  31. package/dist/analysis/data/engine-types.js +7 -0
  32. package/dist/analysis/data/infer.d.ts +42 -0
  33. package/dist/analysis/data/infer.js +118 -0
  34. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  35. package/dist/analysis/data/ingest-worker.js +365 -0
  36. package/dist/analysis/data/json.d.ts +19 -0
  37. package/dist/analysis/data/json.js +44 -0
  38. package/dist/analysis/data/query-worker.d.ts +12 -0
  39. package/dist/analysis/data/query-worker.js +75 -0
  40. package/dist/analysis/data/rows.d.ts +39 -0
  41. package/dist/analysis/data/rows.js +120 -0
  42. package/dist/analysis/data/sql-guard.d.ts +20 -0
  43. package/dist/analysis/data/sql-guard.js +129 -0
  44. package/dist/analysis/data/stats.d.ts +18 -0
  45. package/dist/analysis/data/stats.js +77 -0
  46. package/dist/analysis/install-hints.d.ts +33 -0
  47. package/dist/analysis/install-hints.js +144 -0
  48. package/dist/analysis/janitor.d.ts +60 -0
  49. package/dist/analysis/janitor.js +264 -0
  50. package/dist/analysis/jobs.d.ts +94 -0
  51. package/dist/analysis/jobs.js +165 -0
  52. package/dist/analysis/oci.d.ts +129 -0
  53. package/dist/analysis/oci.js +330 -0
  54. package/dist/analysis/python/sources.d.ts +2 -0
  55. package/dist/analysis/python/sources.js +10 -0
  56. package/dist/analysis/requirements.d.ts +32 -0
  57. package/dist/analysis/requirements.js +73 -0
  58. package/dist/analysis/rerun.d.ts +40 -0
  59. package/dist/analysis/rerun.js +136 -0
  60. package/dist/analysis/runtime-manager.d.ts +146 -0
  61. package/dist/analysis/runtime-manager.js +472 -0
  62. package/dist/analysis/status.d.ts +21 -0
  63. package/dist/analysis/status.js +52 -0
  64. package/dist/application.d.ts +105 -3
  65. package/dist/application.js +446 -18
  66. package/dist/artifacts/kinds.d.ts +35 -0
  67. package/dist/artifacts/kinds.js +177 -0
  68. package/dist/artifacts/publisher.d.ts +7 -0
  69. package/dist/artifacts/publisher.js +29 -0
  70. package/dist/artifacts/store.d.ts +119 -0
  71. package/dist/artifacts/store.js +581 -0
  72. package/dist/background/janitor.d.ts +22 -0
  73. package/dist/background/janitor.js +62 -0
  74. package/dist/background/mirror.d.ts +14 -0
  75. package/dist/background/mirror.js +41 -0
  76. package/dist/background/notify.d.ts +58 -0
  77. package/dist/background/notify.js +172 -0
  78. package/dist/background/output.d.ts +46 -0
  79. package/dist/background/output.js +179 -0
  80. package/dist/background/service.d.ts +137 -0
  81. package/dist/background/service.js +410 -0
  82. package/dist/background/store.d.ts +103 -0
  83. package/dist/background/store.js +195 -0
  84. package/dist/changelog/data.d.ts +6 -0
  85. package/dist/changelog/data.js +293 -0
  86. package/dist/changelog/index.d.ts +5 -0
  87. package/dist/changelog/index.js +7 -0
  88. package/dist/changelog/parse.d.ts +53 -0
  89. package/dist/changelog/parse.js +160 -0
  90. package/dist/commands/builtins.d.ts +10 -0
  91. package/dist/commands/builtins.js +161 -0
  92. package/dist/commands/catalog.d.ts +86 -0
  93. package/dist/commands/catalog.js +304 -0
  94. package/dist/config.d.ts +206 -6
  95. package/dist/config.js +426 -20
  96. package/dist/core/compaction.js +13 -2
  97. package/dist/core/contracts.d.ts +180 -3
  98. package/dist/core/opt-in.d.ts +12 -0
  99. package/dist/core/opt-in.js +12 -0
  100. package/dist/core/output-limit.d.ts +34 -0
  101. package/dist/core/output-limit.js +39 -0
  102. package/dist/core/run-stats.d.ts +16 -0
  103. package/dist/core/run-stats.js +8 -0
  104. package/dist/core/runner.d.ts +134 -4
  105. package/dist/core/runner.js +780 -138
  106. package/dist/core/timeout.d.ts +15 -0
  107. package/dist/core/timeout.js +61 -0
  108. package/dist/core/truncation.d.ts +30 -0
  109. package/dist/core/truncation.js +83 -0
  110. package/dist/goal/blockers.d.ts +45 -0
  111. package/dist/goal/blockers.js +51 -0
  112. package/dist/goal/budget.d.ts +24 -0
  113. package/dist/goal/budget.js +53 -0
  114. package/dist/goal/command.d.ts +36 -0
  115. package/dist/goal/command.js +98 -0
  116. package/dist/goal/format.d.ts +19 -0
  117. package/dist/goal/format.js +102 -0
  118. package/dist/goal/host.d.ts +29 -0
  119. package/dist/goal/host.js +42 -0
  120. package/dist/goal/index.d.ts +9 -0
  121. package/dist/goal/index.js +9 -0
  122. package/dist/goal/machine.d.ts +186 -0
  123. package/dist/goal/machine.js +344 -0
  124. package/dist/goal/prompts.d.ts +10 -0
  125. package/dist/goal/prompts.js +75 -0
  126. package/dist/goal/service.d.ts +113 -0
  127. package/dist/goal/service.js +273 -0
  128. package/dist/goal/store.d.ts +59 -0
  129. package/dist/goal/store.js +184 -0
  130. package/dist/index.d.ts +50 -5
  131. package/dist/index.js +49 -4
  132. package/dist/mcp/connector.js +38 -6
  133. package/dist/mcp/rich.d.ts +41 -0
  134. package/dist/mcp/rich.js +362 -0
  135. package/dist/permissions/modes.d.ts +72 -0
  136. package/dist/permissions/modes.js +98 -0
  137. package/dist/plan/exit-plan.d.ts +25 -0
  138. package/dist/plan/exit-plan.js +130 -0
  139. package/dist/plan/state.d.ts +102 -0
  140. package/dist/plan/state.js +165 -0
  141. package/dist/plugins/host.d.ts +34 -1
  142. package/dist/plugins/host.js +143 -9
  143. package/dist/providers/settings.d.ts +15 -0
  144. package/dist/providers/settings.js +38 -0
  145. package/dist/reload.d.ts +75 -0
  146. package/dist/reload.js +204 -0
  147. package/dist/resources/context.js +6 -1
  148. package/dist/runtime/access.d.ts +62 -0
  149. package/dist/runtime/access.js +115 -0
  150. package/dist/runtime/blobs.d.ts +51 -0
  151. package/dist/runtime/blobs.js +177 -0
  152. package/dist/runtime/diff.d.ts +23 -0
  153. package/dist/runtime/diff.js +154 -0
  154. package/dist/runtime/ids.d.ts +8 -0
  155. package/dist/runtime/ids.js +32 -0
  156. package/dist/runtime/paths.d.ts +11 -0
  157. package/dist/runtime/paths.js +17 -1
  158. package/dist/runtime/process.d.ts +22 -0
  159. package/dist/runtime/process.js +100 -15
  160. package/dist/runtime/sqlite.d.ts +2 -0
  161. package/dist/runtime/sqlite.js +18 -0
  162. package/dist/runtime/store.d.ts +91 -3
  163. package/dist/runtime/store.js +465 -11
  164. package/dist/runtime/zip.d.ts +24 -0
  165. package/dist/runtime/zip.js +126 -0
  166. package/dist/sessions/children.js +17 -1
  167. package/dist/sessions/side-questions.d.ts +57 -0
  168. package/dist/sessions/side-questions.js +161 -0
  169. package/dist/tools/analysis.d.ts +37 -0
  170. package/dist/tools/analysis.js +573 -0
  171. package/dist/tools/artifacts.d.ts +14 -0
  172. package/dist/tools/artifacts.js +143 -0
  173. package/dist/tools/background.d.ts +7 -0
  174. package/dist/tools/background.js +165 -0
  175. package/dist/tools/data.d.ts +7 -0
  176. package/dist/tools/data.js +160 -0
  177. package/dist/tools/execute.d.ts +5 -0
  178. package/dist/tools/execute.js +15 -3
  179. package/dist/tools/goal.d.ts +19 -0
  180. package/dist/tools/goal.js +126 -0
  181. package/dist/tools/search.d.ts +17 -3
  182. package/dist/tools/search.js +153 -0
  183. package/dist/tools/standard.d.ts +4 -1
  184. package/dist/tools/standard.js +99 -18
  185. package/dist/version.d.ts +5 -3
  186. package/dist/version.js +35 -6
  187. package/package.json +2 -2
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Glue every host (TUI, web server) needs and must not write twice: turning what a run did into
3
+ * the controller's `GoalRunOutcome`, and the opt-in tools a goal run offers the model.
4
+ */
5
+ import { GOAL_TOOLS } from "../core/opt-in.js";
6
+ import { runStatsOf } from "../core/run-stats.js";
7
+ import { RunTimeoutError } from "../core/timeout.js";
8
+ /**
9
+ * The outcome of one run. `cancelled` is whether the run's abort signal fired (the user stopped
10
+ * it, or the host is shutting down); a failed run carries its error; a provider timeout is flagged
11
+ * so the goal does not block on something the runner already retried.
12
+ */
13
+ export function goalOutcome(input) {
14
+ const { result, error } = input;
15
+ const failed = error !== undefined && !input.cancelled;
16
+ // A run that threw still spent tokens (the runner remembers them against the error).
17
+ const stats = error !== undefined ? runStatsOf(error) : undefined;
18
+ return {
19
+ runId: input.runId,
20
+ status: input.cancelled
21
+ ? "cancelled"
22
+ : failed
23
+ ? "failed"
24
+ : result?.status === "turns-exceeded"
25
+ ? "turns-exceeded"
26
+ : "completed",
27
+ ...(failed ? { error: error instanceof Error ? error.message : String(error) } : {}),
28
+ ...(failed && error instanceof RunTimeoutError ? { timedOut: true } : {}),
29
+ usage: result?.usage ?? stats?.usage ?? { input: 0, output: 0 },
30
+ text: result?.text ?? "",
31
+ toolCalls: result?.toolCalls ?? stats?.toolCalls ?? 0,
32
+ startedAt: input.startedAt,
33
+ durationMs: Math.max(0, input.endedAt - input.startedAt),
34
+ };
35
+ }
36
+ /**
37
+ * The opt-in tools a run of this session gets: the goal tools while the goal is `active` and the
38
+ * agent is not the plan agent (a plan run never works on a goal).
39
+ */
40
+ export function goalOptInTools(goal, planMode) {
41
+ return goal?.status === "active" && !planMode ? [...GOAL_TOOLS] : [];
42
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./blockers.ts";
2
+ export * from "./budget.ts";
3
+ export * from "./command.ts";
4
+ export * from "./format.ts";
5
+ export * from "./host.ts";
6
+ export * from "./machine.ts";
7
+ export * from "./prompts.ts";
8
+ export * from "./service.ts";
9
+ export * from "./store.ts";
@@ -0,0 +1,9 @@
1
+ export * from "./blockers.js";
2
+ export * from "./budget.js";
3
+ export * from "./command.js";
4
+ export * from "./format.js";
5
+ export * from "./host.js";
6
+ export * from "./machine.js";
7
+ export * from "./prompts.js";
8
+ export * from "./service.js";
9
+ export * from "./store.js";
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The goal state machine: pure functions over a `GoalRecord`, no I/O, no clock of their own.
3
+ * The store applies them inside one transaction (compare-and-set), the controller decides what
4
+ * to do after a run, and every surface calls the same code.
5
+ *
6
+ * ┌───────── resume ─────────┐
7
+ * created ─▶ active ──▶ paused | blocked | budget_limited | complete
8
+ * ▲ └─ complete (model, with evidence) ─▶ complete
9
+ * └──── budget raised / cleared (token_budget only) ── budget_limited
10
+ *
11
+ * Who may do what:
12
+ * - user: create, pause, resume (paused|blocked), edit, clear, change the budget.
13
+ * - model: `update_goal` complete / blocked, only while the goal is `active`.
14
+ * - system: the caps and breakers (pause / budget_limited / blocked on error), user interrupt,
15
+ * restart. The system never resumes a goal.
16
+ */
17
+ import type { GoalAction, GoalEvidence, GoalInfo, GoalReason, GoalStatus } from "@alisio/sdk";
18
+ export type GoalActor = "user" | "model" | "system";
19
+ export declare const GOAL_STATUSES: readonly GoalStatus[];
20
+ /** The longest `detail` kept (an error text is truncated). */
21
+ export declare const GOAL_DETAIL_MAX = 300;
22
+ /** Every N-th turn the continuation carries the audit-style status reminder. */
23
+ export declare const GOAL_REMINDER_EVERY = 5;
24
+ /** The caps and breaker limits in force (from `config.goal`, snapshotted per goal for the caps). */
25
+ export interface GoalLimits {
26
+ maxTurns: number;
27
+ maxWallMs: number;
28
+ repeatedReplyLimit: number;
29
+ noToolTurnsLimit: number;
30
+ blockedRepeats: number;
31
+ }
32
+ export declare const DEFAULT_GOAL_LIMITS: GoalLimits;
33
+ /** The persisted goal (one per session). */
34
+ export interface GoalRecord {
35
+ sessionId: string;
36
+ goalId: string;
37
+ objective: string;
38
+ status: GoalStatus;
39
+ reason?: GoalReason;
40
+ detail?: string;
41
+ summary?: string;
42
+ evidence?: GoalEvidence[];
43
+ epoch: number;
44
+ tokenBudget?: number;
45
+ tokensUsed: number;
46
+ turnsUsed: number;
47
+ maxTurns: number;
48
+ maxWallMs: number;
49
+ activeMs: number;
50
+ /** Consecutive runs without a tool call. */
51
+ noToolStreak: number;
52
+ /** Consecutive replies identical to their predecessor (a fresh reply is 0). */
53
+ repeatStreak: number;
54
+ lastReplyHash?: string;
55
+ /** Consecutive runs in which the model reported the goal blocked. */
56
+ blockedStreak: number;
57
+ /** The run of the last blocked report (a later run without one resets the streak). */
58
+ blockedRun?: string;
59
+ /** Continuations claimed so far (the `n` of `goal-<goalId>-<n>`). */
60
+ continuations: number;
61
+ /** The request id of a claimed continuation whose run has not settled yet. */
62
+ inflight?: string;
63
+ /** The last run folded into the totals (so settling twice counts once). */
64
+ lastRunId?: string;
65
+ /** The full contract has been sent (later continuations carry a short hint only). */
66
+ kickoffSent: boolean;
67
+ /** The process that last drove the goal (a restart pauses goals whose owner is dead). */
68
+ ownerPid?: number;
69
+ createdAt: number;
70
+ updatedAt: number;
71
+ completedAt?: number;
72
+ }
73
+ /** What a run did, as the host reports it after the run ended. */
74
+ export interface GoalRunOutcome {
75
+ runId: string;
76
+ status: "completed" | "turns-exceeded" | "cancelled" | "failed";
77
+ /** The error text of a failed run. */
78
+ error?: string;
79
+ /** A provider timeout (the runner already retried it): not a reason to block the goal. */
80
+ timedOut?: boolean;
81
+ usage: {
82
+ input: number;
83
+ output: number;
84
+ };
85
+ /** The final assistant text (used for the repeated-reply fingerprint and the token estimate). */
86
+ text: string;
87
+ toolCalls: number;
88
+ startedAt: number;
89
+ durationMs: number;
90
+ }
91
+ export type GoalLog = {
92
+ event: "repeat_observed";
93
+ count: number;
94
+ } | {
95
+ event: "no_tool_turn";
96
+ count: number;
97
+ };
98
+ export interface GoalSettlement {
99
+ goal: GoalRecord;
100
+ logs: GoalLog[];
101
+ }
102
+ /** The user actions per state (what the UIs offer). `clear`, `edit` and `budget` work anywhere. */
103
+ export declare function userActions(status: GoalStatus): GoalAction[];
104
+ export declare const isFinalGoalStatus: (status: GoalStatus) => boolean;
105
+ export declare const clip: (text: string, max?: number) => string;
106
+ export type GoalRefusal = "not_allowed" | "not_active";
107
+ export type GoalResult<T = GoalRecord> = {
108
+ ok: true;
109
+ goal: T;
110
+ } | {
111
+ ok: false;
112
+ code: GoalRefusal;
113
+ message: string;
114
+ };
115
+ /** A new goal (the user's `/goal <objective>`). The caller supplies the id and the limits. */
116
+ export declare function newGoal(input: {
117
+ sessionId: string;
118
+ goalId: string;
119
+ objective: string;
120
+ tokenBudget?: number;
121
+ limits: Pick<GoalLimits, "maxTurns" | "maxWallMs">;
122
+ epoch: number;
123
+ now: number;
124
+ ownerPid?: number;
125
+ }): GoalRecord;
126
+ /** The user pauses the automatic continuation (only an `active` goal can be paused). */
127
+ export declare function userPause(goal: GoalRecord, now: number): GoalResult;
128
+ /**
129
+ * The user resumes a paused or blocked goal. The streaks start again, and a goal that stopped
130
+ * at its turn or time cap gets one more allowance of that cap (otherwise it would stop at once).
131
+ */
132
+ export declare function userResume(goal: GoalRecord, now: number, limits: Pick<GoalLimits, "maxTurns" | "maxWallMs">, ownerPid?: number): GoalResult;
133
+ /** The user changes the objective in any state; the next continuation sends the contract again. */
134
+ export declare function userEdit(goal: GoalRecord, now: number, objective: string): GoalResult;
135
+ /**
136
+ * The user changes the token budget (`null` removes it) in any state. Raising or clearing the
137
+ * budget of a goal that stopped for it re-arms it; lowering it to what was already spent stops
138
+ * an active goal at once (the budget is hard).
139
+ */
140
+ export declare function userBudget(goal: GoalRecord, now: number, tokens: number | null, ownerPid?: number): GoalResult;
141
+ /**
142
+ * The model reports the goal complete. Evidence is mandatory, and only an `active` goal can be
143
+ * completed (a goal the user paused stays paused).
144
+ */
145
+ export declare function modelComplete(goal: GoalRecord, now: number, report: {
146
+ summary: string;
147
+ evidence: GoalEvidence[];
148
+ }): GoalResult;
149
+ export interface BlockedReport {
150
+ summary: string;
151
+ evidence: GoalEvidence[];
152
+ runId: string | undefined;
153
+ }
154
+ /**
155
+ * The model reports the goal blocked. A permission denial (`denied` evidence) blocks at once;
156
+ * anything else must be reported again, with evidence, in the next turn (`blockedRepeats`
157
+ * consecutive runs) before the goal stops. Returns whether it stopped.
158
+ */
159
+ export declare function modelBlocked(goal: GoalRecord, now: number, report: BlockedReport, limits: Pick<GoalLimits, "blockedRepeats">): GoalResult<{
160
+ goal: GoalRecord;
161
+ stopped: boolean;
162
+ needed: number;
163
+ }>;
164
+ /** A process restart: an `active` goal pauses (never auto-resumes). */
165
+ export declare function systemRestart(goal: GoalRecord, now: number): GoalRecord;
166
+ /** A stable fingerprint of the final reply: case, spacing and trailing punctuation do not matter. */
167
+ export declare function replyFingerprint(text: string): string;
168
+ /** Roughly four characters a token, for providers that report no usage (documented estimate). */
169
+ export declare const estimateReplyTokens: (text: string) => number;
170
+ /** The breaker stage of a symptom seen `count` times in a row: logged, then nudged, then paused. */
171
+ export declare function breakerStage(count: number, limit: number): "none" | "log" | "nudge" | "pause";
172
+ /** Whether the continuation after this state must carry a nudge (and which breaker asks). */
173
+ export declare function pendingNudge(goal: GoalRecord, limits: Pick<GoalLimits, "repeatedReplyLimit" | "noToolTurnsLimit">): "repeat" | "no_tool" | undefined;
174
+ /**
175
+ * Folds one finished run into the goal and decides whether the goal stops. Idempotent per run id.
176
+ * The order of the checks is the order of the stops:
177
+ * user interrupt, token budget, failed run, turn cap, time cap, no-progress breakers.
178
+ */
179
+ export declare function settleRun(goal: GoalRecord, outcome: GoalRunOutcome, limits: GoalLimits, now: number): GoalSettlement;
180
+ /**
181
+ * The caps checked again right before a continuation starts (a run only settles after it ends, but
182
+ * the budget may have been lowered, the cap reached by another surface, or the goal re-armed).
183
+ */
184
+ export declare function checkCaps(goal: GoalRecord, now: number): GoalRecord | undefined;
185
+ /** The wire view (no internal counters). `waiting` is added by the host. */
186
+ export declare function toGoalInfo(goal: GoalRecord): GoalInfo;
@@ -0,0 +1,344 @@
1
+ export const GOAL_STATUSES = [
2
+ "active",
3
+ "paused",
4
+ "blocked",
5
+ "budget_limited",
6
+ "complete",
7
+ ];
8
+ /** The longest `detail` kept (an error text is truncated). */
9
+ export const GOAL_DETAIL_MAX = 300;
10
+ /** Every N-th turn the continuation carries the audit-style status reminder. */
11
+ export const GOAL_REMINDER_EVERY = 5;
12
+ export const DEFAULT_GOAL_LIMITS = {
13
+ maxTurns: 50,
14
+ maxWallMs: 120 * 60_000,
15
+ repeatedReplyLimit: 3,
16
+ noToolTurnsLimit: 3,
17
+ blockedRepeats: 2,
18
+ };
19
+ /** The user actions per state (what the UIs offer). `clear`, `edit` and `budget` work anywhere. */
20
+ export function userActions(status) {
21
+ switch (status) {
22
+ case "active":
23
+ return ["pause", "edit", "budget", "clear"];
24
+ case "paused":
25
+ case "blocked":
26
+ return ["resume", "edit", "budget", "clear"];
27
+ default:
28
+ return ["edit", "budget", "clear"];
29
+ }
30
+ }
31
+ export const isFinalGoalStatus = (status) => status === "complete" || status === "budget_limited";
32
+ export const clip = (text, max = GOAL_DETAIL_MAX) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
33
+ /** A goal with the given fields changed and `updatedAt` stamped. `epoch` moves with `visible`. */
34
+ function touch(goal, now, patch, visible) {
35
+ return { ...goal, ...patch, updatedAt: now, epoch: visible ? goal.epoch + 1 : goal.epoch };
36
+ }
37
+ /** Moves to `status`; fields that only make sense in another state are reset. */
38
+ function moveTo(goal, now, status, reason, extra = {}) {
39
+ return touch(goal, now, {
40
+ status,
41
+ reason,
42
+ detail: undefined,
43
+ ...(status === "complete" ? { completedAt: now } : {}),
44
+ ...extra,
45
+ }, true);
46
+ }
47
+ const refuse = (code, message) => ({
48
+ ok: false,
49
+ code,
50
+ message,
51
+ });
52
+ /** A new goal (the user's `/goal <objective>`). The caller supplies the id and the limits. */
53
+ export function newGoal(input) {
54
+ return {
55
+ sessionId: input.sessionId,
56
+ goalId: input.goalId,
57
+ objective: input.objective,
58
+ status: "active",
59
+ reason: "created",
60
+ epoch: input.epoch,
61
+ ...(input.tokenBudget ? { tokenBudget: input.tokenBudget } : {}),
62
+ tokensUsed: 0,
63
+ turnsUsed: 0,
64
+ maxTurns: input.limits.maxTurns,
65
+ maxWallMs: input.limits.maxWallMs,
66
+ activeMs: 0,
67
+ noToolStreak: 0,
68
+ repeatStreak: 0,
69
+ blockedStreak: 0,
70
+ continuations: 0,
71
+ kickoffSent: false,
72
+ ...(input.ownerPid ? { ownerPid: input.ownerPid } : {}),
73
+ createdAt: input.now,
74
+ updatedAt: input.now,
75
+ };
76
+ }
77
+ /** The user pauses the automatic continuation (only an `active` goal can be paused). */
78
+ export function userPause(goal, now) {
79
+ if (goal.status !== "active")
80
+ return refuse("not_allowed", `The goal is ${goal.status}: only an active goal can be paused.`);
81
+ return { ok: true, goal: moveTo(goal, now, "paused", "user_paused", { inflight: undefined }) };
82
+ }
83
+ /**
84
+ * The user resumes a paused or blocked goal. The streaks start again, and a goal that stopped
85
+ * at its turn or time cap gets one more allowance of that cap (otherwise it would stop at once).
86
+ */
87
+ export function userResume(goal, now, limits, ownerPid) {
88
+ if (goal.status !== "paused" && goal.status !== "blocked")
89
+ return refuse("not_allowed", goal.status === "active"
90
+ ? "The goal is already active."
91
+ : goal.status === "complete"
92
+ ? "The goal is complete: start a new one with /goal <objective>."
93
+ : "The goal ran out of its token budget: raise it with /goal budget=<n> to continue.");
94
+ return {
95
+ ok: true,
96
+ goal: moveTo(goal, now, "active", "resumed", {
97
+ noToolStreak: 0,
98
+ repeatStreak: 0,
99
+ blockedStreak: 0,
100
+ blockedRun: undefined,
101
+ lastReplyHash: undefined,
102
+ inflight: undefined,
103
+ ...(goal.reason === "max_turns" ? { maxTurns: goal.turnsUsed + limits.maxTurns } : {}),
104
+ ...(goal.reason === "max_wall" ? { maxWallMs: goal.activeMs + limits.maxWallMs } : {}),
105
+ ...(ownerPid ? { ownerPid } : {}),
106
+ }),
107
+ };
108
+ }
109
+ /** The user changes the objective in any state; the next continuation sends the contract again. */
110
+ export function userEdit(goal, now, objective) {
111
+ return {
112
+ ok: true,
113
+ goal: touch(goal, now, { objective, kickoffSent: false, blockedStreak: 0, blockedRun: undefined }, true),
114
+ };
115
+ }
116
+ /**
117
+ * The user changes the token budget (`null` removes it) in any state. Raising or clearing the
118
+ * budget of a goal that stopped for it re-arms it; lowering it to what was already spent stops
119
+ * an active goal at once (the budget is hard).
120
+ */
121
+ export function userBudget(goal, now, tokens, ownerPid) {
122
+ const base = { tokenBudget: tokens === null ? undefined : tokens };
123
+ const exhausted = tokens !== null && goal.tokensUsed >= tokens;
124
+ if (goal.status === "budget_limited" && goal.reason === "token_budget" && !exhausted)
125
+ return {
126
+ ok: true,
127
+ goal: moveTo(goal, now, "active", "resumed", {
128
+ ...base,
129
+ noToolStreak: 0,
130
+ repeatStreak: 0,
131
+ blockedStreak: 0,
132
+ blockedRun: undefined,
133
+ inflight: undefined,
134
+ completedAt: undefined,
135
+ ...(ownerPid ? { ownerPid } : {}),
136
+ }),
137
+ };
138
+ if (goal.status === "active" && exhausted)
139
+ return {
140
+ ok: true,
141
+ goal: moveTo(goal, now, "budget_limited", "token_budget", { ...base, inflight: undefined }),
142
+ };
143
+ return { ok: true, goal: touch(goal, now, base, true) };
144
+ }
145
+ /**
146
+ * The model reports the goal complete. Evidence is mandatory, and only an `active` goal can be
147
+ * completed (a goal the user paused stays paused).
148
+ */
149
+ export function modelComplete(goal, now, report) {
150
+ if (goal.status !== "active")
151
+ return refuse("not_active", `The goal is ${goal.status}; only an active goal can be completed.`);
152
+ if (!report.evidence.length)
153
+ return refuse("not_allowed", "Completion needs evidence (files, tests, logs or commands).");
154
+ return {
155
+ ok: true,
156
+ goal: moveTo(goal, now, "complete", "model_complete", {
157
+ summary: report.summary,
158
+ evidence: report.evidence,
159
+ inflight: goal.inflight,
160
+ }),
161
+ };
162
+ }
163
+ /**
164
+ * The model reports the goal blocked. A permission denial (`denied` evidence) blocks at once;
165
+ * anything else must be reported again, with evidence, in the next turn (`blockedRepeats`
166
+ * consecutive runs) before the goal stops. Returns whether it stopped.
167
+ */
168
+ export function modelBlocked(goal, now, report, limits) {
169
+ if (goal.status !== "active")
170
+ return refuse("not_active", `The goal is ${goal.status}; only an active goal can be reported blocked.`);
171
+ if (!report.evidence.length)
172
+ return refuse("not_allowed", "A blocked report needs evidence (what you tried and what failed).");
173
+ const denied = report.evidence.some((item) => item.kind === "denied");
174
+ // The same run reporting twice counts once: the streak is about consecutive runs.
175
+ const streak = report.runId && goal.blockedRun === report.runId ? goal.blockedStreak : goal.blockedStreak + 1;
176
+ const needed = Math.max(1, limits.blockedRepeats);
177
+ const base = { summary: report.summary, evidence: report.evidence, blockedRun: report.runId };
178
+ if (denied || streak >= needed)
179
+ return {
180
+ ok: true,
181
+ goal: {
182
+ goal: moveTo(goal, now, "blocked", denied ? "policy_denied" : "model_blocked", {
183
+ ...base,
184
+ blockedStreak: streak,
185
+ inflight: goal.inflight,
186
+ }),
187
+ stopped: true,
188
+ needed,
189
+ },
190
+ };
191
+ return {
192
+ ok: true,
193
+ goal: {
194
+ goal: touch(goal, now, { ...base, blockedStreak: streak }, false),
195
+ stopped: false,
196
+ needed,
197
+ },
198
+ };
199
+ }
200
+ /** A process restart: an `active` goal pauses (never auto-resumes). */
201
+ export function systemRestart(goal, now) {
202
+ if (goal.status !== "active")
203
+ return goal;
204
+ return moveTo(goal, now, "paused", "restart", { inflight: undefined });
205
+ }
206
+ /** A stable fingerprint of the final reply: case, spacing and trailing punctuation do not matter. */
207
+ export function replyFingerprint(text) {
208
+ const normalized = text
209
+ .toLowerCase()
210
+ .replace(/\s+/g, " ")
211
+ .trim()
212
+ .replace(/[.!?…\s]+$/u, "");
213
+ if (!normalized)
214
+ return "";
215
+ // FNV-1a over the normalized text: collisions only cost a false repeat, and this stays portable.
216
+ let hash = 0x811c9dc5;
217
+ for (let i = 0; i < normalized.length; i++) {
218
+ hash ^= normalized.charCodeAt(i);
219
+ hash = Math.imul(hash, 0x01000193) >>> 0;
220
+ }
221
+ return `${normalized.length.toString(36)}:${hash.toString(36)}`;
222
+ }
223
+ /** Roughly four characters a token, for providers that report no usage (documented estimate). */
224
+ export const estimateReplyTokens = (text) => Math.ceil(text.length / 4);
225
+ /** The breaker stage of a symptom seen `count` times in a row: logged, then nudged, then paused. */
226
+ export function breakerStage(count, limit) {
227
+ const cap = Math.max(2, limit);
228
+ if (count >= cap)
229
+ return "pause";
230
+ if (count >= 2)
231
+ return "nudge";
232
+ return count === 1 ? "log" : "none";
233
+ }
234
+ /** Whether the continuation after this state must carry a nudge (and which breaker asks). */
235
+ export function pendingNudge(goal, limits) {
236
+ if (breakerStage(goal.repeatStreak, limits.repeatedReplyLimit) === "nudge")
237
+ return "repeat";
238
+ if (breakerStage(goal.noToolStreak, limits.noToolTurnsLimit) === "nudge")
239
+ return "no_tool";
240
+ return undefined;
241
+ }
242
+ /**
243
+ * Folds one finished run into the goal and decides whether the goal stops. Idempotent per run id.
244
+ * The order of the checks is the order of the stops:
245
+ * user interrupt, token budget, failed run, turn cap, time cap, no-progress breakers.
246
+ */
247
+ export function settleRun(goal, outcome, limits, now) {
248
+ if (goal.lastRunId === outcome.runId)
249
+ return { goal, logs: [] };
250
+ // A run that began while the goal was active still counts after the goal changed state
251
+ // (`updatedAt` moved during the run); runs of a goal that was already over do not.
252
+ const counts = outcome.startedAt >= goal.createdAt &&
253
+ (goal.status === "active" || goal.updatedAt >= outcome.startedAt);
254
+ if (!counts)
255
+ return { goal, logs: [] };
256
+ const reported = outcome.usage.input + outcome.usage.output;
257
+ const tokens = reported > 0 ? reported : estimateReplyTokens(outcome.text);
258
+ let next = {
259
+ ...goal,
260
+ tokensUsed: goal.tokensUsed + tokens,
261
+ turnsUsed: goal.turnsUsed + 1,
262
+ activeMs: goal.activeMs + Math.max(0, outcome.durationMs),
263
+ lastRunId: outcome.runId,
264
+ inflight: undefined,
265
+ updatedAt: now,
266
+ // A blocked report only counts while it is in consecutive runs.
267
+ ...(goal.blockedRun !== outcome.runId ? { blockedStreak: 0, blockedRun: undefined } : {}),
268
+ };
269
+ if (goal.status !== "active")
270
+ return { goal: next, logs: [] };
271
+ const logs = [];
272
+ // Breakers (counted for every settled run so the numbers are right when a cap stops it first).
273
+ const hash = replyFingerprint(outcome.text);
274
+ const repeated = !!hash && hash === goal.lastReplyHash;
275
+ next = {
276
+ ...next,
277
+ ...(hash ? { lastReplyHash: hash } : {}),
278
+ repeatStreak: repeated ? goal.repeatStreak + 1 : 0,
279
+ noToolStreak: outcome.toolCalls === 0 ? goal.noToolStreak + 1 : 0,
280
+ };
281
+ if (repeated && breakerStage(next.repeatStreak, limits.repeatedReplyLimit) !== "pause")
282
+ logs.push({ event: "repeat_observed", count: next.repeatStreak });
283
+ if (outcome.toolCalls === 0 && breakerStage(next.noToolStreak, limits.noToolTurnsLimit) === "log")
284
+ logs.push({ event: "no_tool_turn", count: next.noToolStreak });
285
+ if (outcome.status === "cancelled")
286
+ return { goal: moveTo(next, now, "paused", "user_interrupt"), logs };
287
+ if (next.tokenBudget !== undefined && next.tokensUsed >= next.tokenBudget)
288
+ return { goal: moveTo(next, now, "budget_limited", "token_budget"), logs };
289
+ if (outcome.status === "failed" && !outcome.timedOut)
290
+ return {
291
+ goal: moveTo(next, now, "blocked", "run_error", {
292
+ detail: clip(outcome.error?.trim() || "The run failed."),
293
+ }),
294
+ logs,
295
+ };
296
+ if (next.turnsUsed >= next.maxTurns)
297
+ return { goal: moveTo(next, now, "paused", "max_turns"), logs };
298
+ if (next.activeMs >= next.maxWallMs)
299
+ return { goal: moveTo(next, now, "paused", "max_wall"), logs };
300
+ if (breakerStage(next.repeatStreak, limits.repeatedReplyLimit) === "pause")
301
+ return { goal: moveTo(next, now, "paused", "no_progress", { detail: "repeated_reply" }), logs };
302
+ if (breakerStage(next.noToolStreak, limits.noToolTurnsLimit) === "pause")
303
+ return { goal: moveTo(next, now, "paused", "no_progress", { detail: "no_tool_turns" }), logs };
304
+ return { goal: next, logs };
305
+ }
306
+ /**
307
+ * The caps checked again right before a continuation starts (a run only settles after it ends, but
308
+ * the budget may have been lowered, the cap reached by another surface, or the goal re-armed).
309
+ */
310
+ export function checkCaps(goal, now) {
311
+ if (goal.status !== "active")
312
+ return undefined;
313
+ if (goal.tokenBudget !== undefined && goal.tokensUsed >= goal.tokenBudget)
314
+ return moveTo(goal, now, "budget_limited", "token_budget", { inflight: undefined });
315
+ if (goal.turnsUsed >= goal.maxTurns)
316
+ return moveTo(goal, now, "paused", "max_turns", { inflight: undefined });
317
+ if (goal.activeMs >= goal.maxWallMs)
318
+ return moveTo(goal, now, "paused", "max_wall", { inflight: undefined });
319
+ return undefined;
320
+ }
321
+ /** The wire view (no internal counters). `waiting` is added by the host. */
322
+ export function toGoalInfo(goal) {
323
+ return {
324
+ sessionId: goal.sessionId,
325
+ goalId: goal.goalId,
326
+ objective: goal.objective,
327
+ status: goal.status,
328
+ ...(goal.reason ? { reason: goal.reason } : {}),
329
+ ...(goal.detail ? { detail: goal.detail } : {}),
330
+ epoch: goal.epoch,
331
+ ...(goal.tokenBudget !== undefined ? { tokenBudget: goal.tokenBudget } : {}),
332
+ tokensUsed: goal.tokensUsed,
333
+ turnsUsed: goal.turnsUsed,
334
+ maxTurns: goal.maxTurns,
335
+ activeMs: goal.activeMs,
336
+ maxWallMs: goal.maxWallMs,
337
+ ...(goal.summary ? { summary: goal.summary } : {}),
338
+ ...(goal.evidence?.length ? { evidence: goal.evidence } : {}),
339
+ actions: userActions(goal.status),
340
+ createdAt: goal.createdAt,
341
+ updatedAt: goal.updatedAt,
342
+ ...(goal.completedAt ? { completedAt: goal.completedAt } : {}),
343
+ };
344
+ }
@@ -0,0 +1,10 @@
1
+ import { type GoalLimits, type GoalRecord } from "./machine.ts";
2
+ export type GoalPromptKind = "kickoff" | "hint" | "reminder";
3
+ export interface GoalPrompt {
4
+ kind: GoalPromptKind;
5
+ text: string;
6
+ /** What the transcript shows instead of the text (short; never the whole objective). */
7
+ display: string;
8
+ }
9
+ /** The text of the next message the goal sends (the caller decides when; see the controller). */
10
+ export declare function buildGoalPrompt(goal: GoalRecord, limits: Pick<GoalLimits, "repeatedReplyLimit" | "noToolTurnsLimit">): GoalPrompt;