tryharness 0.7.26 → 0.7.29
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/README.md +5 -4
- package/cli.mjs +72 -9
- package/lib/asks.mjs +260 -0
- package/lib/atlas.mjs +155 -31
- package/lib/cards.mjs +2 -1
- package/lib/context.mjs +44 -32
- package/lib/coordinator.mjs +444 -68
- package/lib/daemon.mjs +708 -122
- package/lib/desktop.mjs +24 -1
- package/lib/goals.mjs +199 -0
- package/lib/lane.mjs +187 -0
- package/lib/library.mjs +693 -0
- package/lib/listen.mjs +674 -0
- package/lib/mcp-stdio.mjs +118 -16
- package/lib/members.json +2 -2
- package/lib/nudge.mjs +3 -1
- package/lib/phones.mjs +19 -0
- package/lib/places.mjs +38 -0
- package/lib/readers.mjs +459 -0
- package/lib/runner.mjs +11 -1
- package/lib/service.mjs +21 -1
- package/lib/snippets.mjs +114 -7
- package/lib/store.mjs +79 -1
- package/lib/swarm.mjs +128 -3
- package/lib/t3.mjs +16 -2
- package/lib/video.mjs +332 -0
- package/package.json +1 -1
package/lib/store.mjs
CHANGED
|
@@ -861,11 +861,15 @@ export function openStore(file) {
|
|
|
861
861
|
if (status)
|
|
862
862
|
execute(
|
|
863
863
|
"run",
|
|
864
|
-
|
|
864
|
+
// An open goal belongs to no one: a claim left from before goals refused claims is cleared when it opens.
|
|
865
|
+
"update board_tasks set status = ?, result = coalesce(?, result), reported_at = case when ? in ('done', 'failed') then ? else reported_at end, claimed_by = case when ? = 'open' then null else claimed_by end, holder = case when ? = 'open' then null else holder end, lease_until = case when ? = 'open' then null else lease_until end, updated_at = ?, version = version + 1 where id = ?",
|
|
865
866
|
status,
|
|
866
867
|
result ?? null,
|
|
867
868
|
status,
|
|
868
869
|
now,
|
|
870
|
+
status,
|
|
871
|
+
status,
|
|
872
|
+
status,
|
|
869
873
|
now,
|
|
870
874
|
id
|
|
871
875
|
);
|
|
@@ -1031,6 +1035,25 @@ export function openStore(file) {
|
|
|
1031
1035
|
).n
|
|
1032
1036
|
);
|
|
1033
1037
|
},
|
|
1038
|
+
/** The room's held posts, oldest first, without their words (what held them may be a secret). */
|
|
1039
|
+
heldPosts(goalId) {
|
|
1040
|
+
return execute(
|
|
1041
|
+
"all",
|
|
1042
|
+
"select id, seq, author, created_at from board_contributions where task_id = ? and kind = 'held' order by seq asc",
|
|
1043
|
+
goalId
|
|
1044
|
+
);
|
|
1045
|
+
},
|
|
1046
|
+
/** The room's contribution just before a post, held ones included. */
|
|
1047
|
+
lastContribution(goalId, beforeSeq) {
|
|
1048
|
+
return (
|
|
1049
|
+
execute(
|
|
1050
|
+
"get",
|
|
1051
|
+
"select id, seq, kind, author from board_contributions where task_id = ? and seq < ? order by seq desc limit 1",
|
|
1052
|
+
goalId,
|
|
1053
|
+
beforeSeq
|
|
1054
|
+
) ?? null
|
|
1055
|
+
);
|
|
1056
|
+
},
|
|
1034
1057
|
roomCount(goalId) {
|
|
1035
1058
|
return Number(
|
|
1036
1059
|
execute(
|
|
@@ -1153,6 +1176,36 @@ export function openStore(file) {
|
|
|
1153
1176
|
progress: JSON.parse(row.progress),
|
|
1154
1177
|
}));
|
|
1155
1178
|
},
|
|
1179
|
+
/**
|
|
1180
|
+
* How the coordinator held up, a day at a time, newest first: rounds Harness decided, rounds a backup on this
|
|
1181
|
+
* computer decided after Harness could not, rounds decided here on the privacy setting, and rounds no one decided.
|
|
1182
|
+
*/
|
|
1183
|
+
roundHealth(goalId = null, days = 14) {
|
|
1184
|
+
const since = new Date(
|
|
1185
|
+
Date.now() - Math.max(1, Number(days) || 14) * 86_400_000
|
|
1186
|
+
).toISOString();
|
|
1187
|
+
return execute(
|
|
1188
|
+
"all",
|
|
1189
|
+
`select substr(at, 1, 10) as day,
|
|
1190
|
+
count(*) as rounds,
|
|
1191
|
+
sum(case when action is not null and coordinator = 'harness' then 1 else 0 end) as harness,
|
|
1192
|
+
sum(case when action is not null and coordinator != 'harness' and error is not null then 1 else 0 end) as backup,
|
|
1193
|
+
sum(case when action is not null and coordinator != 'harness' and error is null then 1 else 0 end) as local,
|
|
1194
|
+
sum(case when action is null then 1 else 0 end) as undecided
|
|
1195
|
+
from swarm_rounds where at >= ? and (? is null or goal_id = ?)
|
|
1196
|
+
group by day order by day desc`,
|
|
1197
|
+
since,
|
|
1198
|
+
goalId,
|
|
1199
|
+
goalId
|
|
1200
|
+
).map((row) => ({
|
|
1201
|
+
day: row.day,
|
|
1202
|
+
rounds: Number(row.rounds),
|
|
1203
|
+
harness: Number(row.harness),
|
|
1204
|
+
backup: Number(row.backup),
|
|
1205
|
+
local: Number(row.local),
|
|
1206
|
+
undecided: Number(row.undecided),
|
|
1207
|
+
}));
|
|
1208
|
+
},
|
|
1156
1209
|
// A thread's runs (wakes and jobs), oldest first, for its transcript.
|
|
1157
1210
|
threadRuns(goalId) {
|
|
1158
1211
|
return execute(
|
|
@@ -1307,6 +1360,17 @@ export function openStore(file) {
|
|
|
1307
1360
|
usage: row.usage ? JSON.parse(row.usage) : null,
|
|
1308
1361
|
}));
|
|
1309
1362
|
},
|
|
1363
|
+
/** The run working in an agent's session right now, if any: a session a running task holds. */
|
|
1364
|
+
runHoldingSession(agent, sessionId) {
|
|
1365
|
+
return (
|
|
1366
|
+
execute(
|
|
1367
|
+
"get",
|
|
1368
|
+
"select id, parent_id from board_tasks where target_agent = ? and status in ('claimed', 'running') and session_id is not null and instr(session_id, ?) > 0 order by updated_at desc limit 1",
|
|
1369
|
+
agent,
|
|
1370
|
+
String(sessionId)
|
|
1371
|
+
) ?? null
|
|
1372
|
+
);
|
|
1373
|
+
},
|
|
1310
1374
|
setRunSession(id, sessionId) {
|
|
1311
1375
|
execute(
|
|
1312
1376
|
"run",
|
|
@@ -1606,6 +1670,12 @@ export function openStore(file) {
|
|
|
1606
1670
|
String(id)
|
|
1607
1671
|
);
|
|
1608
1672
|
},
|
|
1673
|
+
presentFiles() {
|
|
1674
|
+
return execute(
|
|
1675
|
+
"all",
|
|
1676
|
+
"select id from board_files where present = 1 order by created_at asc"
|
|
1677
|
+
).map((row) => this.file(row.id));
|
|
1678
|
+
},
|
|
1609
1679
|
taskFiles(taskId) {
|
|
1610
1680
|
return execute(
|
|
1611
1681
|
"all",
|
|
@@ -2250,6 +2320,14 @@ export function openStore(file) {
|
|
|
2250
2320
|
error:
|
|
2251
2321
|
"a task for several agents is worked through its parts; claim the part posted for you",
|
|
2252
2322
|
};
|
|
2323
|
+
// A goal is a room the team works in, not a task to take: a claimed goal turned away the claimer's own posts and
|
|
2324
|
+
// could not be released (October 14, Codex in the keeper thread).
|
|
2325
|
+
if (this.getTask(id)?.kind === "goal")
|
|
2326
|
+
return {
|
|
2327
|
+
ok: false,
|
|
2328
|
+
error:
|
|
2329
|
+
"a goal is a room, not a task: read it with read_room and post with post_to_room; claim only a task posted for you",
|
|
2330
|
+
};
|
|
2253
2331
|
const now = new Date().toISOString();
|
|
2254
2332
|
const leaseUntil =
|
|
2255
2333
|
leaseMinutes == null
|
package/lib/swarm.mjs
CHANGED
|
@@ -34,6 +34,8 @@ export const PROCESS_NAMES = {
|
|
|
34
34
|
opencode: "opencode",
|
|
35
35
|
};
|
|
36
36
|
const SAY_LIMIT = 4_000;
|
|
37
|
+
// `@swarm` in the person's message: written to their whole swarm, not to one agent.
|
|
38
|
+
const SWARM = /(^|\s)@swarm\b/;
|
|
37
39
|
const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
38
40
|
|
|
39
41
|
// The id an agent resumes a session by: the index keys sessions by their file name.
|
|
@@ -102,7 +104,27 @@ export function createSwarm({
|
|
|
102
104
|
runsIn,
|
|
103
105
|
threadFiles = () => ({ files: [], rendering: [] }),
|
|
104
106
|
whyWaiting = () => null,
|
|
107
|
+
goals = null,
|
|
108
|
+
reofferHarness = () => {},
|
|
105
109
|
}) {
|
|
110
|
+
// What the person let a thread spend of their Harness credits, in dollars, or null for no limit of its own.
|
|
111
|
+
const ceilingOf = (threadId) => {
|
|
112
|
+
const limit = Number(store.meta(`spend-ceiling:${threadId}`));
|
|
113
|
+
return store.meta(`spend-ceiling:${threadId}`) && Number.isFinite(limit)
|
|
114
|
+
? limit
|
|
115
|
+
: null;
|
|
116
|
+
};
|
|
117
|
+
const setCeiling = (threadId, usd) =>
|
|
118
|
+
store.meta(
|
|
119
|
+
`spend-ceiling:${threadId}`,
|
|
120
|
+
usd === null ? "" : String(Math.round(usd * 100) / 100)
|
|
121
|
+
);
|
|
122
|
+
// A limit as the person gave it: dollars, at least zero; "off" or nothing for none.
|
|
123
|
+
const ceilingFrom = (value) => {
|
|
124
|
+
if (value === undefined || value === null || value === "off") return null;
|
|
125
|
+
const usd = Number(value);
|
|
126
|
+
return Number.isFinite(usd) && usd >= 0 ? usd : undefined;
|
|
127
|
+
};
|
|
106
128
|
const awaitingStart = (run) =>
|
|
107
129
|
run.status === "open" &&
|
|
108
130
|
Boolean(run.run_workspace) &&
|
|
@@ -114,12 +136,30 @@ export function createSwarm({
|
|
|
114
136
|
const agentsOf = (thread) =>
|
|
115
137
|
thread.goal.team.filter((agent) => agent !== thread.goal.facilitator);
|
|
116
138
|
|
|
117
|
-
// Who a message is for: the agents it names, else
|
|
139
|
+
// Who a message is for: the agents it names, else whoever coordinates, which brings in every agent with a part in it.
|
|
140
|
+
// Where the coordinator cannot assemble (the privacy setting, a Harness without it) the thread's only agent takes it.
|
|
141
|
+
// `@swarm` is for the whole swarm (the agents it names hear it from the post itself).
|
|
118
142
|
const addressed = (thread, text) => {
|
|
143
|
+
if (SWARM.test(text)) return [thread.goal.facilitator];
|
|
119
144
|
const named = mentionsIn(text, thread.goal.team);
|
|
120
145
|
if (named.length) return named;
|
|
121
146
|
const agents = agentsOf(thread);
|
|
122
|
-
return agents.length === 1
|
|
147
|
+
return agents.length === 1 && !coordinator.assembles()
|
|
148
|
+
? agents
|
|
149
|
+
: [thread.goal.facilitator];
|
|
150
|
+
};
|
|
151
|
+
// The person's post is for the swarm when it says `@swarm`, or when it falls to the coordinator: then the coordinator
|
|
152
|
+
// brings in every agent with a part in it at once (coordinator.mjs heard). A message to one agent is that agent's.
|
|
153
|
+
const heardBy = (thread, post, text) => {
|
|
154
|
+
if (!post?.seq || post.kind !== "post") return;
|
|
155
|
+
const swarm = SWARM.test(text);
|
|
156
|
+
if (!swarm && addressed(thread, text)[0] !== thread.goal.facilitator)
|
|
157
|
+
return;
|
|
158
|
+
coordinator.heard(thread.id, {
|
|
159
|
+
seq: post.seq,
|
|
160
|
+
swarm,
|
|
161
|
+
named: mentionsIn(text, roomAgents),
|
|
162
|
+
});
|
|
123
163
|
};
|
|
124
164
|
|
|
125
165
|
// What the thread waits on the person for, if anything: a pause, an agent's question, a run's question.
|
|
@@ -217,7 +257,20 @@ export function createSwarm({
|
|
|
217
257
|
coordination: {
|
|
218
258
|
mode: coordinator.mode(),
|
|
219
259
|
rounds: coordinator.state(thread.id).rounds,
|
|
260
|
+
// Who decided this thread's rounds: Harness, a backup here after Harness could not, this computer, no one.
|
|
261
|
+
decided: store.roundHealth(thread.id, 3650).reduce(
|
|
262
|
+
(sum, day) => ({
|
|
263
|
+
harness: sum.harness + day.harness,
|
|
264
|
+
backup: sum.backup + day.backup,
|
|
265
|
+
local: sum.local + day.local,
|
|
266
|
+
undecided: sum.undecided + day.undecided,
|
|
267
|
+
}),
|
|
268
|
+
{ harness: 0, backup: 0, local: 0, undecided: 0 }
|
|
269
|
+
),
|
|
220
270
|
},
|
|
271
|
+
// The goal as the swarm keeps it (goals.mjs): what the person wants, its checks, where it stands, every version.
|
|
272
|
+
goal: goals?.record(thread.id) ?? null,
|
|
273
|
+
ceilingUsd: ceilingOf(thread.id),
|
|
221
274
|
changes: worktrees.summary(thread.id),
|
|
222
275
|
};
|
|
223
276
|
};
|
|
@@ -312,6 +365,15 @@ export function createSwarm({
|
|
|
312
365
|
thread: summary(thread),
|
|
313
366
|
posts: store.roomPosts(thread.id, 0, 500),
|
|
314
367
|
runs,
|
|
368
|
+
// Posts held for the person (who wrote one, when, never its words), and the post that replaced each one an agent
|
|
369
|
+
// posted again.
|
|
370
|
+
held: store.heldPosts(thread.id).map((post) => ({
|
|
371
|
+
id: post.id,
|
|
372
|
+
seq: post.seq,
|
|
373
|
+
author: post.author,
|
|
374
|
+
at: post.created_at,
|
|
375
|
+
replacedBy: store.meta(`held-replaced:${post.id}`) || null,
|
|
376
|
+
})),
|
|
315
377
|
...threadFiles(thread, hostedCaller),
|
|
316
378
|
};
|
|
317
379
|
};
|
|
@@ -328,6 +390,7 @@ export function createSwarm({
|
|
|
328
390
|
store.setGoalTeam(thread.id, [...thread.goal.team, ...joining]);
|
|
329
391
|
if (thread.status !== "open") {
|
|
330
392
|
store.setGoalState(thread.id, { status: "open" });
|
|
393
|
+
goals?.reopen(thread.id);
|
|
331
394
|
store.roomPost({
|
|
332
395
|
goalId: thread.id,
|
|
333
396
|
author: "user",
|
|
@@ -336,7 +399,7 @@ export function createSwarm({
|
|
|
336
399
|
});
|
|
337
400
|
}
|
|
338
401
|
const current = store.getTask(thread.id);
|
|
339
|
-
|
|
402
|
+
const posted = goalCommand(
|
|
340
403
|
{
|
|
341
404
|
command: "board.room.post",
|
|
342
405
|
id: thread.id,
|
|
@@ -345,6 +408,9 @@ export function createSwarm({
|
|
|
345
408
|
},
|
|
346
409
|
"user"
|
|
347
410
|
);
|
|
411
|
+
if (!to && posted?.ok && !posted.paused)
|
|
412
|
+
heardBy(current, posted.post, text);
|
|
413
|
+
return posted;
|
|
348
414
|
};
|
|
349
415
|
|
|
350
416
|
return function swarmCommand(request, agent) {
|
|
@@ -576,6 +642,14 @@ export function createSwarm({
|
|
|
576
642
|
request.limit
|
|
577
643
|
),
|
|
578
644
|
};
|
|
645
|
+
if (command === "board.swarm.health")
|
|
646
|
+
return {
|
|
647
|
+
ok: true,
|
|
648
|
+
days: store.roundHealth(
|
|
649
|
+
request.id ? String(request.id) : null,
|
|
650
|
+
request.days
|
|
651
|
+
),
|
|
652
|
+
};
|
|
579
653
|
if (command === "board.swarm.coordinator") {
|
|
580
654
|
if (request.mode === "harness" || request.mode === "local")
|
|
581
655
|
coordinator.setMode(request.mode);
|
|
@@ -596,6 +670,10 @@ export function createSwarm({
|
|
|
596
670
|
.trim()
|
|
597
671
|
.slice(0, 20_000);
|
|
598
672
|
if (!text) return { ok: false, error: "a thread starts with a message" };
|
|
673
|
+
// What it may spend of the person's Harness credits, set once as it starts (owner, October 15).
|
|
674
|
+
const ceiling = ceilingFrom(request.ceilingUsd);
|
|
675
|
+
if (ceiling === undefined)
|
|
676
|
+
return { ok: false, error: "a limit is a number of dollars, or off" };
|
|
599
677
|
const from =
|
|
600
678
|
request.continueSession && typeof request.continueSession === "object"
|
|
601
679
|
? request.continueSession
|
|
@@ -662,6 +740,7 @@ export function createSwarm({
|
|
|
662
740
|
maxPosts: defaults.maxPosts,
|
|
663
741
|
maxWakes: defaults.maxWakes,
|
|
664
742
|
});
|
|
743
|
+
if (ceiling !== null) setCeiling(thread.id, ceiling);
|
|
665
744
|
if (from) {
|
|
666
745
|
store.setGoalSession(
|
|
667
746
|
thread.id,
|
|
@@ -679,6 +758,7 @@ export function createSwarm({
|
|
|
679
758
|
mentions: addressed(opened, text),
|
|
680
759
|
});
|
|
681
760
|
wakeFor(thread.id, first, "user");
|
|
761
|
+
if (!to) heardBy(opened, first, text);
|
|
682
762
|
notifyBoard(store.getTask(thread.id));
|
|
683
763
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
684
764
|
};
|
|
@@ -772,6 +852,51 @@ export function createSwarm({
|
|
|
772
852
|
stopThreadRuns(thread.id);
|
|
773
853
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
774
854
|
}
|
|
855
|
+
// What the thread may spend of the person's Harness credits, set or cleared by the person; a task waiting on the old
|
|
856
|
+
// limit is offered again.
|
|
857
|
+
if (command === "board.swarm.ceiling") {
|
|
858
|
+
if (agent !== "user")
|
|
859
|
+
return { ok: false, error: "only you set what a thread may spend" };
|
|
860
|
+
const usd = ceilingFrom(request.usd);
|
|
861
|
+
if (usd === undefined)
|
|
862
|
+
return { ok: false, error: "a limit is a number of dollars, or off" };
|
|
863
|
+
setCeiling(thread.id, usd);
|
|
864
|
+
store.roomPost({
|
|
865
|
+
goalId: thread.id,
|
|
866
|
+
author: "user",
|
|
867
|
+
kind: "note",
|
|
868
|
+
text:
|
|
869
|
+
usd === null
|
|
870
|
+
? "You took this thread's own spending limit off."
|
|
871
|
+
: `You set what this thread may spend of your Harness credits to $${usd.toFixed(2)}.`,
|
|
872
|
+
});
|
|
873
|
+
reofferHarness();
|
|
874
|
+
notifyBoard(store.getTask(thread.id));
|
|
875
|
+
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
876
|
+
}
|
|
877
|
+
// The goal in the person's own words, or the lead's (the thread's coordinator): a new version, kept with the rest.
|
|
878
|
+
if (command === "board.swarm.goal") {
|
|
879
|
+
if (agent !== "user" && agent !== thread.goal.facilitator)
|
|
880
|
+
return {
|
|
881
|
+
ok: false,
|
|
882
|
+
error: `only you or ${thread.goal.facilitator} set this thread's goal`,
|
|
883
|
+
};
|
|
884
|
+
if (!goals) return { ok: false, error: "this daemon keeps no goals" };
|
|
885
|
+
const set = goals.set(
|
|
886
|
+
thread.id,
|
|
887
|
+
{ statement: request.statement, criteria: request.criteria },
|
|
888
|
+
{
|
|
889
|
+
by: agent,
|
|
890
|
+
seq: store.roomPosts(thread.id, 0, 500).at(-1)?.seq ?? null,
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
if (!set)
|
|
894
|
+
return {
|
|
895
|
+
ok: false,
|
|
896
|
+
error: "a goal needs a statement and at least one check",
|
|
897
|
+
};
|
|
898
|
+
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
899
|
+
}
|
|
775
900
|
if (command === "board.swarm.resume") {
|
|
776
901
|
if (thread.status !== "paused")
|
|
777
902
|
return { ok: false, error: `the thread is ${thread.status}` };
|
package/lib/t3.mjs
CHANGED
|
@@ -447,12 +447,19 @@ export function runInT3(run, emit, observe) {
|
|
|
447
447
|
stopped = true;
|
|
448
448
|
if (threadId) void interrupt();
|
|
449
449
|
});
|
|
450
|
-
const
|
|
450
|
+
const letGo = () => {
|
|
451
451
|
clearTimeout(timer);
|
|
452
452
|
handed.delete(run.runId);
|
|
453
453
|
steerable.delete(run.runId);
|
|
454
454
|
release();
|
|
455
|
-
|
|
455
|
+
};
|
|
456
|
+
// What the turn used, as T3 recorded it, so a turn there counts toward its thread's spend like one run here.
|
|
457
|
+
const finish = (kind, text) => {
|
|
458
|
+
letGo();
|
|
459
|
+
emit(kind, text.slice(0, 20_000), {
|
|
460
|
+
usage:
|
|
461
|
+
threadId && runId ? (run.usageOf?.(threadId, runId) ?? null) : null,
|
|
462
|
+
});
|
|
456
463
|
};
|
|
457
464
|
const read = async () => {
|
|
458
465
|
try {
|
|
@@ -582,6 +589,13 @@ export function runInT3(run, emit, observe) {
|
|
|
582
589
|
if (stopped) await interrupt();
|
|
583
590
|
void read();
|
|
584
591
|
} catch (error) {
|
|
592
|
+
// T3 Code did not answer before the turn began, though it was there a moment before (October 14 and 15: twice
|
|
593
|
+
// a turn failed this way while T3 was running): where the caller can run the agent itself, it does.
|
|
594
|
+
if (error.code === "not_running" && !runId && run.fallback && !stopped) {
|
|
595
|
+
letGo();
|
|
596
|
+
run.fallback(error.message);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
585
599
|
finish("failed", `T3 Code did not take it: ${error.message}`);
|
|
586
600
|
}
|
|
587
601
|
})();
|