@chatpanel/events 0.102.0 → 0.103.1
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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/project-run.js
CHANGED
|
@@ -1,444 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// to the person as a `permission` ask before it exists (§7, D-A2).
|
|
22
|
-
|
|
23
|
-
import { defineSchema, describeSchema, coerce } from './structured.js';
|
|
24
|
-
import { GRANT_RE } from './team.js';
|
|
25
|
-
import { normalizeJob, readyJobs, canTransition as jobCanMove } from './job.js';
|
|
26
|
-
import { emptyProjectRecord, foldProject, projectProgress } from './project.js';
|
|
27
|
-
import { effectiveGate, gateAllows } from './gate.js';
|
|
28
|
-
import { carveBudget } from './recruit.js';
|
|
29
|
-
|
|
30
|
-
export const MAX_JOBS_PER_ROUND = 8;
|
|
31
|
-
export const MAX_ROUNDS = 4;
|
|
32
|
-
export const EXECUTIVE = 'executive';
|
|
33
|
-
export const PROJECT_RUN_STATUSES = Object.freeze(['done', 'open', 'waiting', 'stopped', 'over-budget', 'failed']);
|
|
34
|
-
|
|
35
|
-
export class ProjectRunError extends Error {
|
|
36
|
-
constructor(code, message) { super(message); this.name = 'ProjectRunError'; this.code = code; }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const clip = (s, n) => String(s || '').trim().slice(0, n);
|
|
40
|
-
const slug = (s) => String(s || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^[^a-z]+/, '').replace(/-+$/, '').slice(0, 48);
|
|
41
|
-
|
|
42
|
-
// ── What the executive answers in ────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
const JOB_FIELDS = {
|
|
45
|
-
id: { type: 'string', required: true, max: 48, describe: 'a short id like j1' },
|
|
46
|
-
title: { type: 'string', required: true, max: 120 },
|
|
47
|
-
brief: { type: 'string', required: true, max: 2000, describe: 'what to do and what done looks like — enough for someone who has not read the goal' },
|
|
48
|
-
skills: { type: 'string[]', maxItems: 6, describe: 'skills the job needs, by name (finance, review, …) — used to pick who takes it' },
|
|
49
|
-
grants: { type: 'string[]', maxItems: 6, describe: 'tools it needs: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr — or none' },
|
|
50
|
-
dependsOn: { type: 'string[]', maxItems: 6, describe: 'job ids whose results this one needs' },
|
|
51
|
-
why: { type: 'string', max: 200, describe: 'why this job, in a few words' },
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/** The first jobs for a goal, and the follow-ups after a round. */
|
|
55
|
-
export const PROJECT_JOBS_SCHEMA = defineSchema({
|
|
56
|
-
name: 'project_jobs',
|
|
57
|
-
purpose: 'the jobs to post on a project for its goal',
|
|
58
|
-
fields: {
|
|
59
|
-
jobs: { type: 'object[]', required: true, maxItems: MAX_JOBS_PER_ROUND, describe: 'independent where possible; a job that needs another\'s result names it in dependsOn', fields: JOB_FIELDS },
|
|
60
|
-
note: { type: 'string', max: 600, describe: 'what you decided and why, for the project page' },
|
|
61
|
-
},
|
|
62
|
-
nothing: { jobs: [], note: '' },
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
/** After a round: does done-when hold, what is the report, what is still needed. */
|
|
66
|
-
export const PROJECT_REVIEW_SCHEMA = defineSchema({
|
|
67
|
-
name: 'project_review',
|
|
68
|
-
purpose: 'the executive\'s reading of a round of jobs against the project\'s goal and done-when',
|
|
69
|
-
fields: {
|
|
70
|
-
done: { type: 'boolean', required: true, describe: 'true only when done-when holds on the results as they are' },
|
|
71
|
-
report: { type: 'string', required: true, max: 6000, describe: 'the project\'s report so far: what was established, with the job it came from; what was not' },
|
|
72
|
-
followUps: { type: 'object[]', maxItems: MAX_JOBS_PER_ROUND, describe: 'the jobs still needed for done-when to hold — empty when it holds or nothing more would help', fields: JOB_FIELDS },
|
|
73
|
-
why: { type: 'string', max: 400, describe: 'why done or not, in a few words' },
|
|
74
|
-
},
|
|
75
|
-
nothing: { done: false, report: '', followUps: [], why: '' },
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const poolLines = (pool) => (pool || []).filter((a) => a && a.id && a.enabled !== false).slice(0, 40)
|
|
79
|
-
.map((a) => `- ${a.id}: ${a.name || a.id}${a.purpose ? ` — ${clip(a.purpose, 140)}` : ''}${a.skills?.length ? ` (skills: ${a.skills.join(', ')})` : ''}${a.grants?.length ? ` (tools: ${a.grants.join(', ')})` : ''}`).join('\n');
|
|
80
|
-
|
|
81
|
-
const jobLines = (jobs) => (jobs || []).map((j) => `- ${j.id} [${j.status}]: ${j.title}${j.result?.text ? ` — result: ${clip(j.result.text, 400)}` : ''}`).join('\n');
|
|
82
|
-
|
|
83
|
-
/** The executive's first instruction: the goal, done-when, the pool, the shape. */
|
|
84
|
-
export function jobsPrompt(project, { pool = [], record = null } = {}) {
|
|
85
|
-
const had = (record?.jobs || []).length ? `\nJobs already on the project (do not repeat them):\n${jobLines(record.jobs)}\n` : '';
|
|
86
|
-
return [
|
|
87
|
-
`You are the executive of the project "${project.title}". Post the jobs that get it to done — between 1 and ${MAX_JOBS_PER_ROUND}, independent where possible, each with a brief a stranger could act on.`,
|
|
88
|
-
'',
|
|
89
|
-
`Goal: ${project.goal}`,
|
|
90
|
-
project.doneWhen ? `Done when: ${project.doneWhen}` : '',
|
|
91
|
-
project.budget ? `Budget for the whole project: ${Object.entries(project.budget).map(([k, v]) => `${k} ${v}`).join(', ')}` : '',
|
|
92
|
-
had,
|
|
93
|
-
'Agents in the pool that may take a job (name the skills and tools a job needs; the fit picks who takes it):',
|
|
94
|
-
poolLines(pool) || '- (nobody yet — name the skills and tools anyway; a job nobody fits is proposed as a new agent)',
|
|
95
|
-
'',
|
|
96
|
-
describeSchema(PROJECT_JOBS_SCHEMA),
|
|
97
|
-
].filter((l) => l !== null && l !== undefined).join('\n');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** The executive's reading of a round: the goal, done-when, every job's result, the shape. */
|
|
101
|
-
export function reviewPrompt(project, record, { round = 1 } = {}) {
|
|
102
|
-
return [
|
|
103
|
-
`You are the executive of the project "${project.title}". Round ${round} of jobs has finished. Read the results against the goal and done-when. Write the report so far. Say whether done-when HOLDS on these results — not whether it could with more work. If it does not hold and more work would help, post the follow-up jobs (at most ${MAX_JOBS_PER_ROUND}); if nothing more would help, post none and say why.`,
|
|
104
|
-
'',
|
|
105
|
-
`Goal: ${project.goal}`,
|
|
106
|
-
project.doneWhen ? `Done when: ${project.doneWhen}` : 'Done when: (not stated — hold the results to the goal as written)',
|
|
107
|
-
'',
|
|
108
|
-
'Jobs and their results:',
|
|
109
|
-
jobLines(record?.jobs || []),
|
|
110
|
-
record?.report?.text ? `\nThe report before this round:\n${clip(record.report.text, 3000)}` : '',
|
|
111
|
-
'',
|
|
112
|
-
describeSchema(PROJECT_REVIEW_SCHEMA),
|
|
113
|
-
].join('\n');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* The executive's answer (text or an already-shaped value) as normalized jobs on the project.
|
|
118
|
-
* Ids the executive reused are suffixed; grants it made up are dropped; a dependency on a job
|
|
119
|
-
* that is not on the project (or on itself) is dropped rather than left dangling.
|
|
120
|
-
*/
|
|
121
|
-
export function parseJobs(answer, project, { existing = [], now = Date.now(), by = EXECUTIVE, schema = PROJECT_JOBS_SCHEMA, field = 'jobs' } = {}) {
|
|
122
|
-
const value = typeof answer === 'string' ? coerce(answer, schema)?.value : answer;
|
|
123
|
-
const raw = Array.isArray(value?.[field]) ? value[field] : [];
|
|
124
|
-
const taken = new Set((existing || []).map((j) => j.id));
|
|
125
|
-
const out = [];
|
|
126
|
-
for (const [i, j] of raw.entries()) {
|
|
127
|
-
if (!j || !clip(j.title, 200) || !clip(j.brief, 8000)) continue;
|
|
128
|
-
let id = slug(j.id) || `j${i + 1}`;
|
|
129
|
-
if (!/^[a-z]/.test(id)) id = `j-${id}`;
|
|
130
|
-
let n = 1;
|
|
131
|
-
const base = id;
|
|
132
|
-
while (taken.has(id)) { n += 1; id = `${base}-${n}`; }
|
|
133
|
-
taken.add(id);
|
|
134
|
-
const grants = [...new Set((Array.isArray(j.grants) ? j.grants : []).map((g) => String(g).trim().toLowerCase()).filter((g) => GRANT_RE.test(g) && g !== 'none'))].slice(0, 6);
|
|
135
|
-
try {
|
|
136
|
-
out.push(normalizeJob({
|
|
137
|
-
id, projectId: project.id, title: j.title, brief: j.brief,
|
|
138
|
-
needs: { skills: j.skills, grants, tools: [] },
|
|
139
|
-
dependsOn: Array.isArray(j.dependsOn) ? j.dependsOn.map((d) => slug(d)) : [],
|
|
140
|
-
postedBy: by, postedAt: now, status: 'open',
|
|
141
|
-
...(j.why ? { origin: { kind: 'executive', why: clip(j.why, 200) } } : {}),
|
|
142
|
-
}));
|
|
143
|
-
} catch { /* a job the form refuses is not posted */ }
|
|
144
|
-
if (out.length >= MAX_JOBS_PER_ROUND) break;
|
|
145
|
-
}
|
|
146
|
-
const ids = new Set([...out.map((j) => j.id), ...(existing || []).map((j) => j.id)]);
|
|
147
|
-
for (const j of out) j.dependsOn = j.dependsOn.filter((d) => ids.has(d) && d !== j.id);
|
|
148
|
-
return out;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** The review, coerced: `{ done, report, followUps: jobs[], why }`. */
|
|
152
|
-
export function parseReview(answer, project, { existing = [], now = Date.now() } = {}) {
|
|
153
|
-
const value = typeof answer === 'string' ? coerce(answer, PROJECT_REVIEW_SCHEMA)?.value : answer;
|
|
154
|
-
if (!value || typeof value !== 'object') return null;
|
|
155
|
-
return {
|
|
156
|
-
done: value.done === true,
|
|
157
|
-
report: clip(value.report, 6000),
|
|
158
|
-
why: clip(value.why, 400),
|
|
159
|
-
followUps: parseJobs(value, project, { existing, now, field: 'followUps', schema: PROJECT_REVIEW_SCHEMA }),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* The team a round runs as: one role per recruited job (the role id IS the job id, so two
|
|
165
|
-
* jobs taken by the same agent are two members), the job's dependencies as the role's, a
|
|
166
|
-
* fixed plan (one task per role, the brief as the task), the members' work side by side —
|
|
167
|
-
* the executive reads it, so no judge. The budget is the round's carve.
|
|
168
|
-
*/
|
|
169
|
-
export function teamForRound(project, recruited, { budget, round = 1 } = {}) {
|
|
170
|
-
const ids = new Set(recruited.map((r) => r.job.id));
|
|
171
|
-
return {
|
|
172
|
-
name: project.id,
|
|
173
|
-
description: `${project.title} — round ${round}`,
|
|
174
|
-
plan: 'fixed', merge: 'concat',
|
|
175
|
-
roles: recruited.map(({ job, role }) => ({
|
|
176
|
-
...role,
|
|
177
|
-
id: job.id,
|
|
178
|
-
name: `${role.name || role.agent || job.id} · ${job.title}`.slice(0, 120),
|
|
179
|
-
dependsOn: (job.dependsOn || []).filter((d) => ids.has(d)),
|
|
180
|
-
job: job.id,
|
|
181
|
-
})),
|
|
182
|
-
budget,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** What a round's run says about each job: the task with the job's id, its status and text. */
|
|
187
|
-
export function jobResults(run, jobs, { now = Date.now() } = {}) {
|
|
188
|
-
const out = [];
|
|
189
|
-
for (const job of jobs) {
|
|
190
|
-
const task = (run?.tasks || []).find((t) => t.id === `t_${job.id}` || t.role === job.id);
|
|
191
|
-
if (!task) { out.push({ id: job.id, status: 'failed', result: { text: 'the run never started this job', by: 'runner', at: now } }); continue; }
|
|
192
|
-
const ok = task.status === 'ok';
|
|
193
|
-
const findings = (task.findings || []).map((f) => `- ${f.text}${f.refs?.length ? ` (${f.refs.join(', ')})` : ''}`).join('\n');
|
|
194
|
-
out.push({
|
|
195
|
-
id: job.id,
|
|
196
|
-
status: ok ? 'done' : 'failed',
|
|
197
|
-
result: { text: clip(ok ? (findings || task.text) : (task.error || task.status || 'failed'), 8000), by: task.role || job.id, at: now, refs: [`run:${run.runId}`, ...(task.findings || []).flatMap((f) => f.refs || []).slice(0, 8)] },
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
return out;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* The jobs a round runs: every open job whose dependencies are done — and, with them, every
|
|
205
|
-
* open job whose dependencies are done OR in this round. A chain (facts → memo) is one round
|
|
206
|
-
* and one team; the runner's barriers order it (team-run.js), and the executive reviews the
|
|
207
|
-
* whole chain, not half of it.
|
|
208
|
-
*/
|
|
209
|
-
export function roundJobs(jobs) {
|
|
210
|
-
const wave = readyJobs(jobs);
|
|
211
|
-
const inWave = new Set(wave.map((j) => j.id));
|
|
212
|
-
const byId = new Map((jobs || []).map((j) => [j.id, j]));
|
|
213
|
-
for (;;) {
|
|
214
|
-
const more = (jobs || []).filter((j) => j.status === 'open' && !inWave.has(j.id) && (j.dependsOn || []).every((d) => byId.get(d)?.status === 'done' || inWave.has(d)));
|
|
215
|
-
if (!more.length) break;
|
|
216
|
-
for (const j of more) { wave.push(j); inWave.add(j.id); }
|
|
217
|
-
}
|
|
218
|
-
return wave;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** The round's budget: what the project has left, split over the jobs that run, never under a floor a run can start with. */
|
|
222
|
-
export function roundBudget(project, record, jobs) {
|
|
223
|
-
const cap = project.budget || {};
|
|
224
|
-
const spent = record?.spend || {};
|
|
225
|
-
const out = {};
|
|
226
|
-
for (const k of ['tokens', 'calls', 'ms', 'usd']) {
|
|
227
|
-
if (!(Number(cap[k]) > 0)) continue;
|
|
228
|
-
const left = Math.max(0, Number(cap[k]) - (Number(spent[k]) || 0));
|
|
229
|
-
if (left > 0) out[k] = k === 'usd' ? Math.round(left * 100) / 100 : Math.floor(left);
|
|
230
|
-
}
|
|
231
|
-
// A job's own budget caps its share; the round is the sum of the shares, within what is left.
|
|
232
|
-
const shares = jobs.map((j) => carveBudget(j, record)).filter(Boolean);
|
|
233
|
-
if (shares.length === jobs.length) {
|
|
234
|
-
for (const k of Object.keys(out)) {
|
|
235
|
-
const sum = shares.reduce((n, s) => n + (Number(s[k]) || 0), 0);
|
|
236
|
-
if (sum > 0) out[k] = Math.min(out[k], sum);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return Object.keys(out).length ? out : null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ── The loop ─────────────────────────────────────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* @param project the page (project.js normalizeProject)
|
|
246
|
-
* @param record the project record so far (foldProject), for a resume; null starts fresh
|
|
247
|
-
* @param pool the agent cards the executive may recruit from (read for the prompt; the
|
|
248
|
-
* host's `recruit` does the pass)
|
|
249
|
-
* @param gate the org's gate (gate.js); the project's own partial gate overrides it
|
|
250
|
-
* @param executive the agent id that runs the loop — on every decision as `by`
|
|
251
|
-
* @param plan `async (prompt, schema) => value | text | null` — the host's structured call
|
|
252
|
-
* @param recruit `async (job, { projectId, create? }) => { role, agentId, engine, why, fit } |
|
|
253
|
-
* { proposal, why } | null` — the host's pool (recruit.js recruitForRun)
|
|
254
|
-
* @param runJobs `async ({ team, request, projectId, round }) => run result` — the host's
|
|
255
|
-
* team runner; `team` is teamForRound's
|
|
256
|
-
* @param ask `async ({ type, text, options }) => { text, by } | null` — the stakeholder;
|
|
257
|
-
* absent, every ask is answered "no" and the loop stops where it would ask
|
|
258
|
-
* @param emit `(type, payload)` — every project-record event, in order; the host lands
|
|
259
|
-
* them on the gateway's project record (and this call folds them too)
|
|
260
|
-
* @param maxRounds rounds of post → recruit → run → review before the loop stops and asks
|
|
261
|
-
*/
|
|
262
|
-
export async function runProject({
|
|
263
|
-
project, record = null, pool = [], gate = null, executive = EXECUTIVE,
|
|
264
|
-
plan, recruit = null, runJobs, ask = null, emit = () => {},
|
|
265
|
-
now = () => Date.now(), signal = null, maxRounds = MAX_ROUNDS,
|
|
266
|
-
} = {}) {
|
|
267
|
-
if (!project?.id || !project.goal) throw new ProjectRunError('BAD_PROJECT', 'a project with an id and a goal is required');
|
|
268
|
-
if (typeof plan !== 'function') throw new ProjectRunError('BAD_RUN', 'plan required');
|
|
269
|
-
if (typeof runJobs !== 'function') throw new ProjectRunError('BAD_RUN', 'runJobs required');
|
|
270
|
-
const rec = record && record.id === project.id ? record : emptyProjectRecord({ id: project.id, now: now() });
|
|
271
|
-
if (!rec.page) foldProject(rec, { type: 'project.created', at: now(), payload: { project } });
|
|
272
|
-
const g = effectiveGate(gate, project.gate || null);
|
|
273
|
-
const stopped = () => !!signal?.aborted;
|
|
274
|
-
const say = (type, payload = {}) => { const ev = { type, at: now(), ...payload }; foldProject(rec, { type, at: ev.at, payload }); emit(type, { projectId: project.id, ...ev }); };
|
|
275
|
-
const decide = (kind, text, refs = []) => say('project.decision', { by: executive, kind, text: clip(text, 2000), refs });
|
|
276
|
-
// The recruiter's own events (recruit.js recruitEvents: evaluating → recruited | open + a
|
|
277
|
-
// proposal decision) land on the record as they are.
|
|
278
|
-
const land = (events) => { for (const ev of events || []) { if (!ev?.type) continue; const { type, at: _at, ...payload } = ev; say(type, payload); } };
|
|
279
|
-
const runs = [];
|
|
280
|
-
let rounds = 0;
|
|
281
|
-
const finish = (status, extra = {}) => ({ projectId: project.id, status, rounds, jobs: rec.jobs.map((j) => ({ id: j.id, title: j.title, status: j.status, ...(j.recruited ? { agentId: j.recruited.agentId } : {}), ...(j.runId ? { runId: j.runId } : {}) })), runs, report: rec.report?.text || '', spend: rec.spend, progress: projectProgress(rec), record: rec, ...extra });
|
|
282
|
-
|
|
283
|
-
/** The stakeholder, where the gate says a person decides; recorded either way. */
|
|
284
|
-
const askPerson = async ({ type, text, options }) => {
|
|
285
|
-
decide('ask', `${text}${options?.length ? ` [${options.join(' / ')}]` : ''}`);
|
|
286
|
-
if (typeof ask !== 'function') { decide('answer', 'nobody to ask — taken as no'); return null; }
|
|
287
|
-
let a = null;
|
|
288
|
-
try { a = await ask({ type, text, options }); } catch { a = null; }
|
|
289
|
-
decide('answer', a ? `${a.by || 'person'}: ${a.text}` : 'no answer');
|
|
290
|
-
return a;
|
|
291
|
-
};
|
|
292
|
-
const yes = (a) => !!a && /^(yes|ok|allow|approve|go|post|recruit|create|raise|close|done|continue|run)/i.test(String(a.text || '').trim());
|
|
293
|
-
|
|
294
|
-
/** The executive's structured call, either shape, never a throw. */
|
|
295
|
-
const structured = async (prompt, schema) => { try { const v = await plan(prompt, schema); return typeof v === 'string' ? coerce(v, schema)?.value ?? null : (v && typeof v === 'object' ? v : null); } catch { return null; } };
|
|
296
|
-
|
|
297
|
-
/** Post jobs — the person's say first when the gate wants it. */
|
|
298
|
-
const post = async (jobs, { what }) => {
|
|
299
|
-
if (!jobs.length) return [];
|
|
300
|
-
const lines = jobs.map((j) => `• ${j.id}: ${j.title}${j.needs?.skills?.length ? ` (${j.needs.skills.join(', ')})` : ''}${j.dependsOn?.length ? ` — after ${j.dependsOn.join(', ')}` : ''}`).join('\n');
|
|
301
|
-
// Scope is the stakeholder's (D-A3): the first jobs and any follow-up are posted with
|
|
302
|
-
// their say unless the gate lets the executive recruit on its own.
|
|
303
|
-
if (!gateAllows(g, 'recruit').allowed) {
|
|
304
|
-
const a = await askPerson({ type: 'direction', text: `${what} — post ${jobs.length} job${jobs.length === 1 ? '' : 's'}?\n${lines}`, options: ['Post them', 'Stop here'] });
|
|
305
|
-
if (!yes(a)) return null;
|
|
306
|
-
} else decide('plan', `${what}: ${jobs.length} job${jobs.length === 1 ? '' : 's'} posted\n${lines}`);
|
|
307
|
-
for (const j of jobs) say('job.posted', { job: j, by: executive });
|
|
308
|
-
return jobs;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
if (rec.status === 'draft') say('project.status', { status: 'open', by: executive });
|
|
312
|
-
if (['done', 'closed'].includes(rec.status)) return finish(rec.status === 'done' ? 'done' : 'stopped', { why: `the project is ${rec.status}` });
|
|
313
|
-
// A RESUME: a job the last loop left recruited or running (its process died) is open
|
|
314
|
-
// again — recruited again, run again; one it finished is not. The record is the checkpoint.
|
|
315
|
-
if (record) {
|
|
316
|
-
const stuck = rec.jobs.filter((j) => ['evaluating', 'recruited', 'in-progress'].includes(j.status));
|
|
317
|
-
for (const j of stuck) say('job.updated', { job: { id: j.id, status: 'open' }, by: executive });
|
|
318
|
-
if (stuck.length) decide('resume', `resumed: ${stuck.map((j) => j.id).join(', ')} back to open (left ${stuck.map((j) => j.status).join(', ')} by the last loop)`);
|
|
319
|
-
else if (rec.jobs.length) decide('resume', `resumed with ${rec.jobs.filter((j) => j.status === 'done').length} of ${rec.jobs.length} jobs done`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ── 1. the first jobs (skipped on a resume that already has some) ──
|
|
323
|
-
if (!rec.jobs.length) {
|
|
324
|
-
const value = await structured(jobsPrompt(project, { pool, record: rec }), PROJECT_JOBS_SCHEMA);
|
|
325
|
-
const jobs = parseJobs(value || {}, project, { existing: rec.jobs, now: now(), by: executive });
|
|
326
|
-
if (!jobs.length) {
|
|
327
|
-
decide('plan', 'the executive could not turn the goal into jobs');
|
|
328
|
-
const a = await askPerson({ type: 'direction', text: `I could not turn the goal into jobs. Post one job for the whole goal, or stop?`, options: ['Post one job', 'Stop'] });
|
|
329
|
-
if (!yes(a)) return finish('failed', { why: 'no jobs' });
|
|
330
|
-
jobs.push(normalizeJob({ id: 'j1', projectId: project.id, title: clip(project.title, 200), brief: `${project.goal}${project.doneWhen ? `\n\nDone when: ${project.doneWhen}` : ''}`, needs: { skills: [], grants: ['data', 'web'], tools: [] }, postedBy: executive, postedAt: now() }));
|
|
331
|
-
} else if (value?.note) decide('plan', value.note);
|
|
332
|
-
const posted = await post(jobs, { what: 'First jobs' });
|
|
333
|
-
if (!posted) return finish('stopped', { why: 'the stakeholder did not post the first jobs' });
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ── rounds: recruit what is ready → run → fold → review → follow-ups ──
|
|
337
|
-
for (;;) {
|
|
338
|
-
if (stopped()) return finish('stopped');
|
|
339
|
-
if (rounds >= maxRounds) {
|
|
340
|
-
decide('review', `${rounds} rounds run; the loop stops here and asks`);
|
|
341
|
-
const a = await askPerson({ type: 'direction', text: `${rounds} rounds have run and done-when does not hold yet. Run another round, or stop with the report so far?`, options: ['Run another round', 'Stop here'] });
|
|
342
|
-
if (!yes(a)) return finish('open', { why: 'rounds exhausted' });
|
|
343
|
-
maxRounds += 1;
|
|
344
|
-
}
|
|
345
|
-
const ready = roundJobs(rec.jobs);
|
|
346
|
-
const stillOpen = rec.jobs.filter((j) => ['open', 'evaluating', 'recruited', 'in-progress'].includes(j.status));
|
|
347
|
-
if (!ready.length) {
|
|
348
|
-
if (!stillOpen.length) return finish(rec.status === 'done' ? 'done' : 'open', { why: 'nothing left to run' });
|
|
349
|
-
// Open jobs whose dependencies failed: they cannot run.
|
|
350
|
-
decide('review', `${stillOpen.length} job${stillOpen.length === 1 ? '' : 's'} cannot run: a dependency failed`);
|
|
351
|
-
for (const j of stillOpen) say('job.updated', { job: { id: j.id, status: 'failed', result: { text: 'a job it depends on failed', by: executive, at: now() } }, by: executive });
|
|
352
|
-
return finish('failed', { why: 'dependencies failed' });
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ── 2. recruit ──
|
|
356
|
-
const recruited = [];
|
|
357
|
-
for (const job of ready) {
|
|
358
|
-
if (stopped()) return finish('stopped');
|
|
359
|
-
if (typeof recruit !== 'function') { say('job.updated', { job: { id: job.id, status: 'failed', result: { text: 'this host has no pool to recruit from', by: executive, at: now() } }, by: executive }); continue; }
|
|
360
|
-
let r = null;
|
|
361
|
-
try { r = await recruit(job, { projectId: project.id }); } catch (e) { decide('recruit', `recruiting for "${job.title}" failed: ${clip(e?.message || e, 200)}`, [`job:${job.id}`]); }
|
|
362
|
-
// The recruiter's own events (evaluating → recruited | open + proposal) land on the record.
|
|
363
|
-
land(r?.events);
|
|
364
|
-
if (!r?.role && r?.proposal) {
|
|
365
|
-
// Nobody fits: the agent the job describes, proposed — created only on the person's say.
|
|
366
|
-
const card = r.proposal;
|
|
367
|
-
say('project.decision', { by: executive, kind: 'proposal', text: `No one in the pool fits "${job.title}"${r.why ? ` — ${r.why}` : ''}. Proposed: ${card.name}${card.skills?.length ? ` — skills ${card.skills.join(', ')}` : ''}${card.grants?.length ? `; grants ${card.grants.join(', ')}` : ''}.`, refs: [`job:${job.id}`], proposal: { kind: 'agent', agent: card, jobId: job.id } });
|
|
368
|
-
const allowed = gateAllows(g, 'newAgent').allowed;
|
|
369
|
-
const a = allowed ? { text: 'Create it', by: 'gate' } : await askPerson({ type: 'permission', text: `No one fits "${job.title}". Create the agent "${card.name}" (${[card.skills?.length ? `skills ${card.skills.join(', ')}` : '', card.grants?.length ? `grants ${card.grants.join(', ')}` : ''].filter(Boolean).join('; ') || 'no particular skills'}) and give it the job?`, options: ['Create it', 'Skip'] });
|
|
370
|
-
if (yes(a)) {
|
|
371
|
-
try { r = await recruit(job, { projectId: project.id, create: card }); } catch (e) { decide('recruit', `creating "${card.name}" failed: ${clip(e?.message || e, 200)}`, [`job:${job.id}`]); r = null; }
|
|
372
|
-
land(r?.events);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (!r?.role) {
|
|
376
|
-
say('job.updated', { job: { id: job.id, status: 'failed', result: { text: `nobody took it${r?.why ? ` — ${r.why}` : ''}`, by: executive, at: now() } }, by: executive });
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
// A recruited job is recorded so a resume does not recruit it again (the recruiter's
|
|
380
|
-
// events say so when it landed them; a bare role is recorded here).
|
|
381
|
-
const nowJob = rec.jobs.find((j) => j.id === job.id);
|
|
382
|
-
if (nowJob && nowJob.status !== 'recruited' && jobCanMove(nowJob.status, 'recruited')) say('job.updated', { job: { id: job.id, status: 'recruited', recruited: { agentId: r.agentId || r.role.agent || r.role.id, ...(r.engine ? { engine: r.engine } : {}), by: 'fit', at: now(), ...(r.why ? { why: clip(r.why, 600) } : {}) } }, by: executive });
|
|
383
|
-
decide('recruit', `${r.agentId || r.role.agent || r.role.id} took "${job.title}"${r.why ? ` — ${r.why}` : ''}`, [`job:${job.id}`]);
|
|
384
|
-
recruited.push({ job: rec.jobs.find((j) => j.id === job.id) || job, role: r.role });
|
|
385
|
-
}
|
|
386
|
-
if (!recruited.length) {
|
|
387
|
-
if (rec.jobs.some((j) => ['open', 'recruited'].includes(j.status))) continue; // dependents of a failed job: the next pass fails them
|
|
388
|
-
return finish('failed', { why: 'nobody took any job' });
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ── 3. run the round as one team ──
|
|
392
|
-
const jobs = recruited.map((x) => x.job);
|
|
393
|
-
const budget = roundBudget(project, rec, jobs);
|
|
394
|
-
if (!budget) {
|
|
395
|
-
decide('review', 'the project has no budget left for another round');
|
|
396
|
-
const allowed = gateAllows(g, 'budgetRaise').allowed;
|
|
397
|
-
const a = allowed ? null : await askPerson({ type: 'budget', text: `The project has spent its budget (${Object.entries(rec.spend).filter(([, v]) => v).map(([k, v]) => `${k} ${v}`).join(', ')}) with ${jobs.length} job${jobs.length === 1 ? '' : 's'} recruited and not run. Raise it by half, or stop here with the report so far?`, options: ['Raise by half', 'Stop here'] });
|
|
398
|
-
if (!allowed && !yes(a)) return finish('over-budget');
|
|
399
|
-
for (const k of Object.keys(project.budget || {})) project.budget[k] = Math.ceil(project.budget[k] * 1.5);
|
|
400
|
-
say('project.updated', { project: { ...project } });
|
|
401
|
-
decide('answer', `budget raised by half: ${Object.entries(project.budget).map(([k, v]) => `${k} ${v}`).join(', ')}`);
|
|
402
|
-
}
|
|
403
|
-
rounds += 1;
|
|
404
|
-
const team = teamForRound(project, recruited, { budget: roundBudget(project, rec, jobs), round: rounds });
|
|
405
|
-
for (const j of jobs) say('job.updated', { job: { id: j.id, status: 'in-progress' }, by: executive });
|
|
406
|
-
decide('run', `round ${rounds}: ${jobs.map((j) => j.id).join(', ')} run as one team (${Object.entries(team.budget || {}).map(([k, v]) => `${k} ${v}`).join(', ')})`);
|
|
407
|
-
let run = null;
|
|
408
|
-
try {
|
|
409
|
-
run = await runJobs({ team, request: [project.goal, project.doneWhen ? `Done when: ${project.doneWhen}` : ''].filter(Boolean).join('\n'), projectId: project.id, round: rounds, jobs });
|
|
410
|
-
} catch (e) {
|
|
411
|
-
decide('run', `round ${rounds} failed to run: ${clip(e?.message || e, 300)}`);
|
|
412
|
-
}
|
|
413
|
-
if (run?.runId) {
|
|
414
|
-
runs.push({ runId: run.runId, round: rounds, status: run.status });
|
|
415
|
-
for (const j of jobs) say('run.linked', { runId: run.runId, jobId: j.id });
|
|
416
|
-
if (run.usage?.spent) say('run.spent', { runId: run.runId, spent: { ...run.usage.spent, ms: Number(run.usage.spent.ms) || Math.max(0, (run.endedAt || now()) - (run.startedAt || now())) } });
|
|
417
|
-
}
|
|
418
|
-
for (const res of jobResults(run, jobs, { now: now() })) say('job.updated', { job: { ...res, ...(run?.runId ? { runId: run.runId } : {}) }, by: executive });
|
|
419
|
-
if (run && ['stopped', 'waiting'].includes(run.status)) return finish(run.status, { why: `round ${rounds} ${run.status}` });
|
|
420
|
-
if (stopped()) return finish('stopped');
|
|
421
|
-
|
|
422
|
-
// ── 4. review: done-when, the report, the follow-ups ──
|
|
423
|
-
const value = await structured(reviewPrompt(project, rec, { round: rounds }), PROJECT_REVIEW_SCHEMA);
|
|
424
|
-
const review = parseReview(value || {}, project, { existing: rec.jobs, now: now() });
|
|
425
|
-
if (review?.report) say('project.report', { text: review.report, by: executive });
|
|
426
|
-
else say('project.report', { text: rec.jobs.map((j) => `## ${j.title} (${j.status})\n${j.result?.text || ''}`).join('\n\n'), by: 'concat' });
|
|
427
|
-
decide('review', review ? `${review.done ? 'done-when holds' : 'done-when does not hold yet'}${review.why ? ` — ${review.why}` : ''}${review.followUps.length ? `; ${review.followUps.length} follow-up${review.followUps.length === 1 ? '' : 's'}` : ''}` : 'the executive did not answer the review; the report is the results as they are');
|
|
428
|
-
if (review?.done) {
|
|
429
|
-
// Closing is the stakeholder's (project.js: done-when held, a person closed it) —
|
|
430
|
-
// unless the gate lets the executive write back on its own.
|
|
431
|
-
const allowed = gateAllows(g, 'writeBack').allowed;
|
|
432
|
-
const a = allowed ? { text: 'Close as done', by: 'gate' } : await askPerson({ type: 'permission', text: `Done-when holds${review.why ? ` — ${review.why}` : ''}. Close the project as done?`, options: ['Close as done', 'Keep it open'] });
|
|
433
|
-
if (yes(a)) { say('project.status', { status: 'done', by: a.by || 'person' }); return finish('done'); }
|
|
434
|
-
return finish('open', { why: 'done-when holds; left open by the stakeholder' });
|
|
435
|
-
}
|
|
436
|
-
if (!review?.followUps?.length) {
|
|
437
|
-
const left = readyJobs(rec.jobs);
|
|
438
|
-
if (left.length) continue; // jobs queued behind this round run next
|
|
439
|
-
return finish('open', { why: review ? 'nothing more would help' : 'no review' });
|
|
440
|
-
}
|
|
441
|
-
const posted = await post(review.followUps, { what: `After round ${rounds}` });
|
|
442
|
-
if (!posted) return finish('open', { why: 'follow-ups not posted' });
|
|
443
|
-
}
|
|
444
|
-
}
|
|
1
|
+
import{defineSchema as F,describeSchema as H,coerce as J}from"./structured.js";import{GRANT_RE as re}from"./team.js";import{normalizeJob as X,readyJobs as V,canTransition as ie}from"./job.js";import{emptyProjectRecord as ae,foldProject as Y,projectProgress as de}from"./project.js";import{effectiveGate as le,gateAllows as C}from"./gate.js";import{carveBudget as ue}from"./recruit.js";const D=8,he=4,K="executive",Se=Object.freeze(["done","open","waiting","stopped","over-budget","failed"]);class M extends Error{constructor(t,h){super(h),this.name="ProjectRunError",this.code=t}}const x=(e,t)=>String(e||"").trim().slice(0,t),Q=e=>String(e||"").trim().toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^[^a-z]+/,"").replace(/-+$/,"").slice(0,48),Z={id:{type:"string",required:!0,max:48,describe:"a short id like j1"},title:{type:"string",required:!0,max:120},brief:{type:"string",required:!0,max:2e3,describe:"what to do and what done looks like — enough for someone who has not read the goal"},skills:{type:"string[]",maxItems:6,describe:"skills the job needs, by name (finance, review, …) — used to pick who takes it"},grants:{type:"string[]",maxItems:6,describe:"tools it needs: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr — or none"},dependsOn:{type:"string[]",maxItems:6,describe:"job ids whose results this one needs"},why:{type:"string",max:200,describe:"why this job, in a few words"}},N=F({name:"project_jobs",purpose:"the jobs to post on a project for its goal",fields:{jobs:{type:"object[]",required:!0,maxItems:D,describe:"independent where possible; a job that needs another's result names it in dependsOn",fields:Z},note:{type:"string",max:600,describe:"what you decided and why, for the project page"}},nothing:{jobs:[],note:""}}),U=F({name:"project_review",purpose:"the executive's reading of a round of jobs against the project's goal and done-when",fields:{done:{type:"boolean",required:!0,describe:"true only when done-when holds on the results as they are"},report:{type:"string",required:!0,max:6e3,describe:"the project's report so far: what was established, with the job it came from; what was not"},followUps:{type:"object[]",maxItems:D,describe:"the jobs still needed for done-when to hold — empty when it holds or nothing more would help",fields:Z},why:{type:"string",max:400,describe:"why done or not, in a few words"}},nothing:{done:!1,report:"",followUps:[],why:""}}),pe=e=>(e||[]).filter(t=>t&&t.id&&t.enabled!==!1).slice(0,40).map(t=>`- ${t.id}: ${t.name||t.id}${t.purpose?` — ${x(t.purpose,140)}`:""}${t.skills?.length?` (skills: ${t.skills.join(", ")})`:""}${t.grants?.length?` (tools: ${t.grants.join(", ")})`:""}`).join(`
|
|
2
|
+
`),ee=e=>(e||[]).map(t=>`- ${t.id} [${t.status}]: ${t.title}${t.result?.text?` — result: ${x(t.result.text,400)}`:""}`).join(`
|
|
3
|
+
`);function fe(e,{pool:t=[],record:h=null}={}){const $=(h?.jobs||[]).length?`
|
|
4
|
+
Jobs already on the project (do not repeat them):
|
|
5
|
+
${ee(h.jobs)}
|
|
6
|
+
`:"";return[`You are the executive of the project "${e.title}". Post the jobs that get it to done — between 1 and ${D}, independent where possible, each with a brief a stranger could act on.`,"",`Goal: ${e.goal}`,e.doneWhen?`Done when: ${e.doneWhen}`:"",e.budget?`Budget for the whole project: ${Object.entries(e.budget).map(([o,r])=>`${o} ${r}`).join(", ")}`:"",$,"Agents in the pool that may take a job (name the skills and tools a job needs; the fit picks who takes it):",pe(t)||"- (nobody yet — name the skills and tools anyway; a job nobody fits is proposed as a new agent)","",H(N)].filter(o=>o!=null).join(`
|
|
7
|
+
`)}function ce(e,t,{round:h=1}={}){return[`You are the executive of the project "${e.title}". Round ${h} of jobs has finished. Read the results against the goal and done-when. Write the report so far. Say whether done-when HOLDS on these results — not whether it could with more work. If it does not hold and more work would help, post the follow-up jobs (at most ${D}); if nothing more would help, post none and say why.`,"",`Goal: ${e.goal}`,e.doneWhen?`Done when: ${e.doneWhen}`:"Done when: (not stated — hold the results to the goal as written)","","Jobs and their results:",ee(t?.jobs||[]),t?.report?.text?`
|
|
8
|
+
The report before this round:
|
|
9
|
+
${x(t.report.text,3e3)}`:"","",H(U)].join(`
|
|
10
|
+
`)}function te(e,t,{existing:h=[],now:$=Date.now(),by:o=K,schema:r=N,field:y="jobs"}={}){const f=typeof e=="string"?J(e,r)?.value:e,b=Array.isArray(f?.[y])?f[y]:[],A=new Set((h||[]).map(I=>I.id)),c=[];for(const[I,n]of b.entries()){if(!n||!x(n.title,200)||!x(n.brief,8e3))continue;let O=Q(n.id)||`j${I+1}`;/^[a-z]/.test(O)||(O=`j-${O}`);let _=1;const g=O;for(;A.has(O);)_+=1,O=`${g}-${_}`;A.add(O);const w=[...new Set((Array.isArray(n.grants)?n.grants:[]).map(R=>String(R).trim().toLowerCase()).filter(R=>re.test(R)&&R!=="none"))].slice(0,6);try{c.push(X({id:O,projectId:t.id,title:n.title,brief:n.brief,needs:{skills:n.skills,grants:w,tools:[]},dependsOn:Array.isArray(n.dependsOn)?n.dependsOn.map(R=>Q(R)):[],postedBy:o,postedAt:$,status:"open",...n.why?{origin:{kind:"executive",why:x(n.why,200)}}:{}}))}catch{}if(c.length>=D)break}const B=new Set([...c.map(I=>I.id),...(h||[]).map(I=>I.id)]);for(const I of c)I.dependsOn=I.dependsOn.filter(n=>B.has(n)&&n!==I.id);return c}function be(e,t,{existing:h=[],now:$=Date.now()}={}){const o=typeof e=="string"?J(e,U)?.value:e;return!o||typeof o!="object"?null:{done:o.done===!0,report:x(o.report,6e3),why:x(o.why,400),followUps:te(o,t,{existing:h,now:$,field:"followUps",schema:U})}}function ge(e,t,{budget:h,round:$=1}={}){const o=new Set(t.map(r=>r.job.id));return{name:e.id,description:`${e.title} — round ${$}`,plan:"fixed",merge:"concat",roles:t.map(({job:r,role:y})=>({...y,id:r.id,name:`${y.name||y.agent||r.id} · ${r.title}`.slice(0,120),dependsOn:(r.dependsOn||[]).filter(f=>o.has(f)),job:r.id})),budget:h}}function we(e,t,{now:h=Date.now()}={}){const $=[];for(const o of t){const r=(e?.tasks||[]).find(b=>b.id===`t_${o.id}`||b.role===o.id);if(!r){$.push({id:o.id,status:"failed",result:{text:"the run never started this job",by:"runner",at:h}});continue}const y=r.status==="ok",f=(r.findings||[]).map(b=>`- ${b.text}${b.refs?.length?` (${b.refs.join(", ")})`:""}`).join(`
|
|
11
|
+
`);$.push({id:o.id,status:y?"done":"failed",result:{text:x(y?f||r.text:r.error||r.status||"failed",8e3),by:r.role||o.id,at:h,refs:[`run:${e.runId}`,...(r.findings||[]).flatMap(b=>b.refs||[]).slice(0,8)]}})}return $}function ye(e){const t=V(e),h=new Set(t.map(o=>o.id)),$=new Map((e||[]).map(o=>[o.id,o]));for(;;){const o=(e||[]).filter(r=>r.status==="open"&&!h.has(r.id)&&(r.dependsOn||[]).every(y=>$.get(y)?.status==="done"||h.has(y)));if(!o.length)break;for(const r of o)t.push(r),h.add(r.id)}return t}function oe(e,t,h){const $=e.budget||{},o=t?.spend||{},r={};for(const f of["tokens","calls","ms","usd"]){if(!(Number($[f])>0))continue;const b=Math.max(0,Number($[f])-(Number(o[f])||0));b>0&&(r[f]=f==="usd"?Math.round(b*100)/100:Math.floor(b))}const y=h.map(f=>ue(f,t)).filter(Boolean);if(y.length===h.length)for(const f of Object.keys(r)){const b=y.reduce((A,c)=>A+(Number(c[f])||0),0);b>0&&(r[f]=Math.min(r[f],b))}return Object.keys(r).length?r:null}async function Re({project:e,record:t=null,pool:h=[],gate:$=null,executive:o=K,plan:r,recruit:y=null,runJobs:f,ask:b=null,emit:A=()=>{},now:c=()=>Date.now(),signal:B=null,maxRounds:I=he}={}){if(!e?.id||!e.goal)throw new M("BAD_PROJECT","a project with an id and a goal is required");if(typeof r!="function")throw new M("BAD_RUN","plan required");if(typeof f!="function")throw new M("BAD_RUN","runJobs required");const n=t&&t.id===e.id?t:ae({id:e.id,now:c()});n.page||Y(n,{type:"project.created",at:c(),payload:{project:e}});const O=le($,e.gate||null),_=()=>!!B?.aborted,g=(i,a={})=>{const d={type:i,at:c(),...a};Y(n,{type:i,at:d.at,payload:a}),A(i,{projectId:e.id,...d})},w=(i,a,d=[])=>g("project.decision",{by:o,kind:i,text:x(a,2e3),refs:d}),R=i=>{for(const a of i||[]){if(!a?.type)continue;const{type:d,at:u,...L}=a;g(d,L)}},T=[];let v=0;const j=(i,a={})=>({projectId:e.id,status:i,rounds:v,jobs:n.jobs.map(d=>({id:d.id,title:d.title,status:d.status,...d.recruited?{agentId:d.recruited.agentId}:{},...d.runId?{runId:d.runId}:{}})),runs:T,report:n.report?.text||"",spend:n.spend,progress:de(n),record:n,...a}),P=async({type:i,text:a,options:d})=>{if(w("ask",`${a}${d?.length?` [${d.join(" / ")}]`:""}`),typeof b!="function")return w("answer","nobody to ask — taken as no"),null;let u=null;try{u=await b({type:i,text:a,options:d})}catch{u=null}return w("answer",u?`${u.by||"person"}: ${u.text}`:"no answer"),u},E=i=>!!i&&/^(yes|ok|allow|approve|go|post|recruit|create|raise|close|done|continue|run)/i.test(String(i.text||"").trim()),W=async(i,a)=>{try{const d=await r(i,a);return typeof d=="string"?J(d,a)?.value??null:d&&typeof d=="object"?d:null}catch{return null}},q=async(i,{what:a})=>{if(!i.length)return[];const d=i.map(u=>`• ${u.id}: ${u.title}${u.needs?.skills?.length?` (${u.needs.skills.join(", ")})`:""}${u.dependsOn?.length?` — after ${u.dependsOn.join(", ")}`:""}`).join(`
|
|
12
|
+
`);if(C(O,"recruit").allowed)w("plan",`${a}: ${i.length} job${i.length===1?"":"s"} posted
|
|
13
|
+
${d}`);else{const u=await P({type:"direction",text:`${a} — post ${i.length} job${i.length===1?"":"s"}?
|
|
14
|
+
${d}`,options:["Post them","Stop here"]});if(!E(u))return null}for(const u of i)g("job.posted",{job:u,by:o});return i};if(n.status==="draft"&&g("project.status",{status:"open",by:o}),["done","closed"].includes(n.status))return j(n.status==="done"?"done":"stopped",{why:`the project is ${n.status}`});if(t){const i=n.jobs.filter(a=>["evaluating","recruited","in-progress"].includes(a.status));for(const a of i)g("job.updated",{job:{id:a.id,status:"open"},by:o});i.length?w("resume",`resumed: ${i.map(a=>a.id).join(", ")} back to open (left ${i.map(a=>a.status).join(", ")} by the last loop)`):n.jobs.length&&w("resume",`resumed with ${n.jobs.filter(a=>a.status==="done").length} of ${n.jobs.length} jobs done`)}if(!n.jobs.length){const i=await W(fe(e,{pool:h,record:n}),N),a=te(i||{},e,{existing:n.jobs,now:c(),by:o});if(a.length)i?.note&&w("plan",i.note);else{w("plan","the executive could not turn the goal into jobs");const u=await P({type:"direction",text:"I could not turn the goal into jobs. Post one job for the whole goal, or stop?",options:["Post one job","Stop"]});if(!E(u))return j("failed",{why:"no jobs"});a.push(X({id:"j1",projectId:e.id,title:x(e.title,200),brief:`${e.goal}${e.doneWhen?`
|
|
15
|
+
|
|
16
|
+
Done when: ${e.doneWhen}`:""}`,needs:{skills:[],grants:["data","web"],tools:[]},postedBy:o,postedAt:c()}))}if(!await q(a,{what:"First jobs"}))return j("stopped",{why:"the stakeholder did not post the first jobs"})}for(;;){if(_())return j("stopped");if(v>=I){w("review",`${v} rounds run; the loop stops here and asks`);const s=await P({type:"direction",text:`${v} rounds have run and done-when does not hold yet. Run another round, or stop with the report so far?`,options:["Run another round","Stop here"]});if(!E(s))return j("open",{why:"rounds exhausted"});I+=1}const i=ye(n.jobs),a=n.jobs.filter(s=>["open","evaluating","recruited","in-progress"].includes(s.status));if(!i.length){if(!a.length)return j(n.status==="done"?"done":"open",{why:"nothing left to run"});w("review",`${a.length} job${a.length===1?"":"s"} cannot run: a dependency failed`);for(const s of a)g("job.updated",{job:{id:s.id,status:"failed",result:{text:"a job it depends on failed",by:o,at:c()}},by:o});return j("failed",{why:"dependencies failed"})}const d=[];for(const s of i){if(_())return j("stopped");if(typeof y!="function"){g("job.updated",{job:{id:s.id,status:"failed",result:{text:"this host has no pool to recruit from",by:o,at:c()}},by:o});continue}let l=null;try{l=await y(s,{projectId:e.id})}catch(p){w("recruit",`recruiting for "${s.title}" failed: ${x(p?.message||p,200)}`,[`job:${s.id}`])}if(R(l?.events),!l?.role&&l?.proposal){const p=l.proposal;g("project.decision",{by:o,kind:"proposal",text:`No one in the pool fits "${s.title}"${l.why?` — ${l.why}`:""}. Proposed: ${p.name}${p.skills?.length?` — skills ${p.skills.join(", ")}`:""}${p.grants?.length?`; grants ${p.grants.join(", ")}`:""}.`,refs:[`job:${s.id}`],proposal:{kind:"agent",agent:p,jobId:s.id}});const se=C(O,"newAgent").allowed?{text:"Create it",by:"gate"}:await P({type:"permission",text:`No one fits "${s.title}". Create the agent "${p.name}" (${[p.skills?.length?`skills ${p.skills.join(", ")}`:"",p.grants?.length?`grants ${p.grants.join(", ")}`:""].filter(Boolean).join("; ")||"no particular skills"}) and give it the job?`,options:["Create it","Skip"]});if(E(se)){try{l=await y(s,{projectId:e.id,create:p})}catch(G){w("recruit",`creating "${p.name}" failed: ${x(G?.message||G,200)}`,[`job:${s.id}`]),l=null}R(l?.events)}}if(!l?.role){g("job.updated",{job:{id:s.id,status:"failed",result:{text:`nobody took it${l?.why?` — ${l.why}`:""}`,by:o,at:c()}},by:o});continue}const S=n.jobs.find(p=>p.id===s.id);S&&S.status!=="recruited"&&ie(S.status,"recruited")&&g("job.updated",{job:{id:s.id,status:"recruited",recruited:{agentId:l.agentId||l.role.agent||l.role.id,...l.engine?{engine:l.engine}:{},by:"fit",at:c(),...l.why?{why:x(l.why,600)}:{}}},by:o}),w("recruit",`${l.agentId||l.role.agent||l.role.id} took "${s.title}"${l.why?` — ${l.why}`:""}`,[`job:${s.id}`]),d.push({job:n.jobs.find(p=>p.id===s.id)||s,role:l.role})}if(!d.length){if(n.jobs.some(s=>["open","recruited"].includes(s.status)))continue;return j("failed",{why:"nobody took any job"})}const u=d.map(s=>s.job);if(!oe(e,n,u)){w("review","the project has no budget left for another round");const s=C(O,"budgetRaise").allowed,l=s?null:await P({type:"budget",text:`The project has spent its budget (${Object.entries(n.spend).filter(([,S])=>S).map(([S,p])=>`${S} ${p}`).join(", ")}) with ${u.length} job${u.length===1?"":"s"} recruited and not run. Raise it by half, or stop here with the report so far?`,options:["Raise by half","Stop here"]});if(!s&&!E(l))return j("over-budget");for(const S of Object.keys(e.budget||{}))e.budget[S]=Math.ceil(e.budget[S]*1.5);g("project.updated",{project:{...e}}),w("answer",`budget raised by half: ${Object.entries(e.budget).map(([S,p])=>`${S} ${p}`).join(", ")}`)}v+=1;const z=ge(e,d,{budget:oe(e,n,u),round:v});for(const s of u)g("job.updated",{job:{id:s.id,status:"in-progress"},by:o});w("run",`round ${v}: ${u.map(s=>s.id).join(", ")} run as one team (${Object.entries(z.budget||{}).map(([s,l])=>`${s} ${l}`).join(", ")})`);let m=null;try{m=await f({team:z,request:[e.goal,e.doneWhen?`Done when: ${e.doneWhen}`:""].filter(Boolean).join(`
|
|
17
|
+
`),projectId:e.id,round:v,jobs:u})}catch(s){w("run",`round ${v} failed to run: ${x(s?.message||s,300)}`)}if(m?.runId){T.push({runId:m.runId,round:v,status:m.status});for(const s of u)g("run.linked",{runId:m.runId,jobId:s.id});m.usage?.spent&&g("run.spent",{runId:m.runId,spent:{...m.usage.spent,ms:Number(m.usage.spent.ms)||Math.max(0,(m.endedAt||c())-(m.startedAt||c()))}})}for(const s of we(m,u,{now:c()}))g("job.updated",{job:{...s,...m?.runId?{runId:m.runId}:{}},by:o});if(m&&["stopped","waiting"].includes(m.status))return j(m.status,{why:`round ${v} ${m.status}`});if(_())return j("stopped");const ne=await W(ce(e,n,{round:v}),U),k=be(ne||{},e,{existing:n.jobs,now:c()});if(k?.report?g("project.report",{text:k.report,by:o}):g("project.report",{text:n.jobs.map(s=>`## ${s.title} (${s.status})
|
|
18
|
+
${s.result?.text||""}`).join(`
|
|
19
|
+
|
|
20
|
+
`),by:"concat"}),w("review",k?`${k.done?"done-when holds":"done-when does not hold yet"}${k.why?` — ${k.why}`:""}${k.followUps.length?`; ${k.followUps.length} follow-up${k.followUps.length===1?"":"s"}`:""}`:"the executive did not answer the review; the report is the results as they are"),k?.done){const l=C(O,"writeBack").allowed?{text:"Close as done",by:"gate"}:await P({type:"permission",text:`Done-when holds${k.why?` — ${k.why}`:""}. Close the project as done?`,options:["Close as done","Keep it open"]});return E(l)?(g("project.status",{status:"done",by:l.by||"person"}),j("done")):j("open",{why:"done-when holds; left open by the stakeholder"})}if(!k?.followUps?.length){if(V(n.jobs).length)continue;return j("open",{why:k?"nothing more would help":"no review"})}if(!await q(k.followUps,{what:`After round ${v}`}))return j("open",{why:"follow-ups not posted"})}}export{K as EXECUTIVE,D as MAX_JOBS_PER_ROUND,he as MAX_ROUNDS,N as PROJECT_JOBS_SCHEMA,U as PROJECT_REVIEW_SCHEMA,Se as PROJECT_RUN_STATUSES,M as ProjectRunError,we as jobResults,fe as jobsPrompt,te as parseJobs,be as parseReview,ce as reviewPrompt,oe as roundBudget,ye as roundJobs,Re as runProject,ge as teamForRound};
|