@tonoid/agent-loop 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.
- package/LICENSE +21 -0
- package/README.md +378 -0
- package/briefs/default/build.md +35 -0
- package/briefs/default/core.md +52 -0
- package/briefs/default/journal.optional.md +7 -0
- package/briefs/default/review.md +65 -0
- package/briefs/default/routine.md +18 -0
- package/briefs/default/screenshots.optional.md +10 -0
- package/briefs/default/subagents.optional.md +7 -0
- package/docs/cutover.md +121 -0
- package/package.json +46 -0
- package/src/adapters/gh.ts +77 -0
- package/src/adapters/git.ts +82 -0
- package/src/adapters/herdr.ts +121 -0
- package/src/adapters/run.ts +64 -0
- package/src/adopt.ts +47 -0
- package/src/brief.ts +124 -0
- package/src/check.ts +126 -0
- package/src/cli-pause.ts +16 -0
- package/src/cli.ts +290 -0
- package/src/config.ts +211 -0
- package/src/ctx.ts +64 -0
- package/src/discover.ts +344 -0
- package/src/effects/monitor.ts +148 -0
- package/src/effects/spawn.ts +246 -0
- package/src/effects/sweep.ts +27 -0
- package/src/engine/item.ts +32 -0
- package/src/engine/monitor.ts +117 -0
- package/src/engine/naming.ts +45 -0
- package/src/engine/spawn.ts +101 -0
- package/src/engine/sweep.ts +91 -0
- package/src/engine/tick.ts +25 -0
- package/src/filing.ts +56 -0
- package/src/globalstate.ts +228 -0
- package/src/journal.ts +13 -0
- package/src/kinds/builder.ts +128 -0
- package/src/kinds/index.ts +13 -0
- package/src/kinds/reviewer.ts +220 -0
- package/src/kinds/routine.ts +174 -0
- package/src/kinds/shared.ts +49 -0
- package/src/kinds/validate.ts +187 -0
- package/src/lock.ts +63 -0
- package/src/paths.ts +21 -0
- package/src/render.ts +42 -0
- package/src/router/budget.ts +81 -0
- package/src/router/providers/claude.ts +177 -0
- package/src/router/providers/codex.ts +120 -0
- package/src/router/providers/grok.ts +10 -0
- package/src/router/rate.ts +62 -0
- package/src/router/route.ts +179 -0
- package/src/router/window.ts +39 -0
- package/src/runtime/worker.ts +75 -0
- package/src/state.ts +128 -0
- package/src/status.ts +41 -0
- package/src/types.ts +225 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export type Provider = "claude" | "codex" | "grok"
|
|
2
|
+
|
|
3
|
+
export interface AccountConfig {
|
|
4
|
+
id: string
|
|
5
|
+
provider: Provider
|
|
6
|
+
configDir: string
|
|
7
|
+
reserve: number
|
|
8
|
+
// Held back per weekday the human still has before the window resets, on top
|
|
9
|
+
// of the flat reserve, which stays the floor. A weekly quota resetting on
|
|
10
|
+
// Sunday is one working day away, not three.
|
|
11
|
+
reservePerWeekday?: number
|
|
12
|
+
// True when loop workers are the only thing spending this account, which is
|
|
13
|
+
// the one condition under which its usage deltas measure a worker. Nothing
|
|
14
|
+
// else implies it: reserve is about protecting quota, not about who spends it.
|
|
15
|
+
soleConsumer?: boolean
|
|
16
|
+
// What an hour of a Saturday or Sunday is worth against an hour of a weekday,
|
|
17
|
+
// so a weekend keeps a small assignment rather than none. 0.25 by default.
|
|
18
|
+
weekendWeight?: number
|
|
19
|
+
maxConcurrent?: number
|
|
20
|
+
allowWhenUnreadable?: boolean
|
|
21
|
+
// The herdr agent kind this account's workers start. Spelled out rather than
|
|
22
|
+
// `kind`, which names a job's behavior in job.yml.
|
|
23
|
+
agentKind?: string
|
|
24
|
+
startArgs?: string[]
|
|
25
|
+
// The model this account's workers run. Model-scoped usage windows that name
|
|
26
|
+
// a different model are skipped. Unset keeps every scoped window, which is
|
|
27
|
+
// the conservative direction: an extra constraint can only lower concurrency.
|
|
28
|
+
model?: string
|
|
29
|
+
// Overrides the built-in OAuth client id used to refresh this account.
|
|
30
|
+
oauthClientId?: string
|
|
31
|
+
// The environment variable this provider reads its config directory from.
|
|
32
|
+
// Defaults per provider; set it explicitly for a provider whose variable
|
|
33
|
+
// the loop has not verified.
|
|
34
|
+
configEnv?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NamingConfig {
|
|
38
|
+
labels: { claim: string; failed: string; park: string; priority: string[] }
|
|
39
|
+
mergeMethod: "merge" | "squash"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkspaceConfig {
|
|
43
|
+
// The state directory key. Explicit rather than derived from the folder,
|
|
44
|
+
// which is called agent-loop in every project.
|
|
45
|
+
name: string
|
|
46
|
+
// The folder holding workspace.yml. Every relative path resolves from here.
|
|
47
|
+
dir: string
|
|
48
|
+
herdrWorkspace: string
|
|
49
|
+
worktreeBase: string
|
|
50
|
+
repos: Record<string, string>
|
|
51
|
+
naming: NamingConfig
|
|
52
|
+
// ~/.agent-loop/<name>/journal.md. Machine-local, so it is derived rather
|
|
53
|
+
// than configured: a path to it in a versioned file would not travel.
|
|
54
|
+
journalPath: string
|
|
55
|
+
jobs: Job[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Config {
|
|
59
|
+
accounts: AccountConfig[]
|
|
60
|
+
// Absolute paths, one per workspace folder. The definitions live at those
|
|
61
|
+
// paths and are read fresh on every tick; see src/discover.ts.
|
|
62
|
+
workspaces: string[]
|
|
63
|
+
maxConcurrentPerAccount: number
|
|
64
|
+
minFreeMb: number
|
|
65
|
+
usageMax: number
|
|
66
|
+
releaseBefore: number
|
|
67
|
+
maxSpawnsPerDay: number
|
|
68
|
+
blockedTimeoutMin: number
|
|
69
|
+
// Percentage points per minute per worker, used for a provider/window pair
|
|
70
|
+
// with no measured EWMA yet. Must be > 0.
|
|
71
|
+
workerRateSeed: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type ItemState = "OPEN" | "CLOSED" | "MERGED"
|
|
75
|
+
|
|
76
|
+
export interface WorkItem {
|
|
77
|
+
id: string
|
|
78
|
+
number: number
|
|
79
|
+
title: string
|
|
80
|
+
state: ItemState
|
|
81
|
+
labels: string[]
|
|
82
|
+
headRef?: string
|
|
83
|
+
url?: string
|
|
84
|
+
// ISO 8601, as the forge reports it. The filing audit needs a time bound and
|
|
85
|
+
// nothing else in the engine reads it.
|
|
86
|
+
createdAt?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type AgentStatus = "working" | "blocked" | "idle" | "missing"
|
|
90
|
+
|
|
91
|
+
export interface AgentView {
|
|
92
|
+
cwd: string
|
|
93
|
+
status: AgentStatus
|
|
94
|
+
paneId: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Marks {
|
|
98
|
+
has(job: string, key: string, mark: string): boolean
|
|
99
|
+
age(job: string, key: string, mark: string): number | null
|
|
100
|
+
set(job: string, key: string, mark: string): void
|
|
101
|
+
clear(job: string, key: string, mark: string): void
|
|
102
|
+
gc(olderThanDays: number): number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface Ctx {
|
|
106
|
+
workspace: WorkspaceConfig
|
|
107
|
+
// Every workspace this process is ticking, so account-scoped facts are
|
|
108
|
+
// counted across the box rather than per workspace (spec 7).
|
|
109
|
+
workspaces: WorkspaceConfig[]
|
|
110
|
+
config: Config
|
|
111
|
+
now: Date
|
|
112
|
+
// Whether this tick may mutate anything at all: the outside world, and
|
|
113
|
+
// agent-loop's own marks. The same flag arms the refusal in adapters/run.ts,
|
|
114
|
+
// so a false here and a mutation attempted anyway is a thrown error, not a
|
|
115
|
+
// silent write. The global database is the exception and records usage
|
|
116
|
+
// samples either way: measuring a quota is not a mutation, and it is what
|
|
117
|
+
// warms the router's rate estimate during a shadow week.
|
|
118
|
+
live: boolean
|
|
119
|
+
// Injected so the worker start dance's retries do not make the suite wait.
|
|
120
|
+
sleep(ms: number): Promise<void>
|
|
121
|
+
lock: import("./lock").LockImpl
|
|
122
|
+
gh: import("./adapters/gh").Gh
|
|
123
|
+
git(repo: string): import("./adapters/git").Git
|
|
124
|
+
herdr: import("./adapters/herdr").HerdrRead
|
|
125
|
+
marks: Marks
|
|
126
|
+
global: import("./globalstate").GlobalStore
|
|
127
|
+
usage(a: AccountConfig): Promise<AccountUsage>
|
|
128
|
+
memAvailableMb(): Promise<number>
|
|
129
|
+
log(d: Decision): void
|
|
130
|
+
cache<T>(key: string, fn: () => Promise<T>): Promise<T>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Backpressure, spec 5.2. Every job that creates work for another declares it,
|
|
134
|
+
// and the engine derives the budget from the consumer's depth rather than from
|
|
135
|
+
// a constant, so it closes on its own when the pipeline backs up.
|
|
136
|
+
export interface FilingConfig {
|
|
137
|
+
// The consumer job whose queue this one feeds.
|
|
138
|
+
queue: string
|
|
139
|
+
maxOpen: number
|
|
140
|
+
perRound: number
|
|
141
|
+
dedupeBy: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface Job {
|
|
145
|
+
name: string
|
|
146
|
+
// The job folder. Brief paths resolve from here.
|
|
147
|
+
dir: string
|
|
148
|
+
// Spawn walk order, ties broken by name. Reviewers before builders, so a
|
|
149
|
+
// merge in this tick relieves the builder's review-debt throttle in the
|
|
150
|
+
// same tick.
|
|
151
|
+
order?: number
|
|
152
|
+
workload: string
|
|
153
|
+
slots?: number
|
|
154
|
+
repo?: string
|
|
155
|
+
// The model this job's workers run, as the agent's own alias or full id.
|
|
156
|
+
// A job-level knob rather than an account one because the router picks the
|
|
157
|
+
// account by headroom: the same job must run the same model wherever it lands.
|
|
158
|
+
model?: string
|
|
159
|
+
sweepIgnoresWorking?: boolean
|
|
160
|
+
deleteRemote?: boolean
|
|
161
|
+
// Selectors: each matches an account by id or by provider.
|
|
162
|
+
requires?: string[]
|
|
163
|
+
prefer?: string[]
|
|
164
|
+
// Demote the account recorded as "built-by:" in this item's PR body.
|
|
165
|
+
distinctFrom?: boolean
|
|
166
|
+
filing?: FilingConfig
|
|
167
|
+
admit?(ctx: Ctx): Promise<string | null>
|
|
168
|
+
discover(ctx: Ctx): Promise<WorkItem[]>
|
|
169
|
+
discoverClaimed(ctx: Ctx): Promise<WorkItem[]>
|
|
170
|
+
key(ctx: Ctx, item: WorkItem): Promise<string>
|
|
171
|
+
attempt?(ctx: Ctx, item: WorkItem): Promise<number>
|
|
172
|
+
guard?(ctx: Ctx, item: WorkItem): Promise<boolean>
|
|
173
|
+
done(ctx: Ctx, item: WorkItem): Promise<boolean>
|
|
174
|
+
sweepOk?(ctx: Ctx, rawKey: string): Promise<boolean>
|
|
175
|
+
// Files copied into a fresh worktree before the worker starts; missing
|
|
176
|
+
// files are ignored and mode is preserved.
|
|
177
|
+
copyIntoWorktree?: string[]
|
|
178
|
+
base?(ctx: Ctx, item: WorkItem): Promise<string>
|
|
179
|
+
prepare?(ctx: Ctx, worktree: string): Promise<void>
|
|
180
|
+
// The brief is the product: what the worker is told to do. Plan 4 ships the
|
|
181
|
+
// default briefs; this plan only delivers whatever the job returns.
|
|
182
|
+
brief(ctx: Ctx, item: WorkItem): Promise<string>
|
|
183
|
+
nudge?(ctx: Ctx, item: WorkItem): Promise<string>
|
|
184
|
+
escalate?(ctx: Ctx, item: WorkItem): Promise<string>
|
|
185
|
+
onFail?(ctx: Ctx, item: WorkItem, transcriptTail: string): Promise<void>
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type Decision =
|
|
189
|
+
| { pass: "gc"; removed: number }
|
|
190
|
+
| { pass: "sweep"; job: string; worktree: string; branch: string; action: "clean" | "hold"; reason: string }
|
|
191
|
+
| { pass: "monitor"; job: string; key: string; action: MonitorAction; reason: string }
|
|
192
|
+
| { pass: "spawn"; job: string; key: string; action: "spawn" | "skip"; account?: string; reason: string }
|
|
193
|
+
// The workspace this tick pass covered, or "total" for the whole process:
|
|
194
|
+
// one cron line covers every workspace on the box, and the cadence rule in
|
|
195
|
+
// spec 4.2 has to be read against the sum, not one workspace's share.
|
|
196
|
+
| { pass: "tick"; workspace: string; ms: number }
|
|
197
|
+
| { pass: "error"; job: string; where: "sweep" | "monitor" | "spawn"; reason: string }
|
|
198
|
+
| { pass: "error"; where: "workspace"; workspace: string; reason: string }
|
|
199
|
+
// The filing audit, at sweep time. Advisory in the brief, audited here.
|
|
200
|
+
| { pass: "audit"; job: string; key: string; filed: number; budget: number }
|
|
201
|
+
// Anything a job wants an operator to see that is not a decision about an
|
|
202
|
+
// item: an identity that could not be resolved, a constraint ignored.
|
|
203
|
+
| { pass: "warn"; job: string; reason: string }
|
|
204
|
+
|
|
205
|
+
export type MonitorAction =
|
|
206
|
+
| "done" | "external" | "busy" | "blocked" | "escalate"
|
|
207
|
+
| "restart" | "nudge" | "fail" | "hold"
|
|
208
|
+
|
|
209
|
+
export interface Window {
|
|
210
|
+
kind: string // 'session' | 'weekly_all' | 'w300' | ...
|
|
211
|
+
group: string
|
|
212
|
+
percent: number
|
|
213
|
+
resetsAt: Date
|
|
214
|
+
windowMinutes: number
|
|
215
|
+
scope?: { model?: string }
|
|
216
|
+
observedAt: Date
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// `exhausted` is a 429: strictly more information than "unknown", and the one
|
|
220
|
+
// unreadable state that allowWhenUnreadable must not resurrect.
|
|
221
|
+
export type AccountUsage =
|
|
222
|
+
| { readable: true; windows: Window[] }
|
|
223
|
+
| { readable: false; reason: string; exhausted?: boolean }
|
|
224
|
+
|
|
225
|
+
export type UsageReader = (a: AccountConfig, now: Date) => Promise<AccountUsage>
|