@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.
Files changed (138) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -442
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
@@ -1,221 +1 @@
1
- // THE BACKUP ENVELOPE one wire format, one implementation.
2
- //
3
- // A ChatPanel backup is the only LOSSLESS channel the corpus has between clients: the
4
- // extension writes one, the gateway reads one, and a desktop client has to do both. Today
5
- // that format is implemented twice — the extension's `crypto-backup.js` (both directions)
6
- // and the gateway's `backup-decrypt.js` (decrypt only, re-derived from the format's shape).
7
- // Two implementations of a CRYPTO format is the worst kind of duplication: a divergence does
8
- // not show up as a wrong pixel, it shows up as a backup nobody can open.
9
- //
10
- // Envelope, unchanged and frozen:
11
- // { type:'chatpanel-backup-encrypted', version, kdf:{iterations,salt},
12
- // cipher:'AES-GCM', compression:'brotli'|'gzip'|'none'|absent, iv, ct }
13
- //
14
- // COMPRESSION IS INJECTED, crypto is not. WebCrypto is on every runtime we target
15
- // (browser, Node 19+, Electron), so it is imported as a global. Compression is not: the
16
- // browser has CompressionStream, Node has zlib, and neither can be reached from the other.
17
- // So the caller passes a `codec` — which is the same shape as the `page-capability.js`
18
- // factory and the `loop.js` clock, and for the same reason.
19
- //
20
- // COMPRESS THEN ENCRYPT, never the reverse. Ciphertext is indistinguishable from random and
21
- // does not compress, so the order is load-bearing rather than stylistic. The envelope
22
- // records which codec was used so a gzip backup written in 2025 still opens forever.
23
-
24
- export class BackupError extends Error {
25
- constructor(message) { super(message); this.name = 'BackupError'; }
26
- }
27
-
28
- export const ENCRYPTED_TYPE = 'chatpanel-backup-encrypted';
29
-
30
- /** PBKDF2-SHA256 rounds. Recorded IN the envelope so this can be raised without breaking old files. */
31
- export const KDF_ITERATIONS = 250_000;
32
-
33
- export const COMPRESSIONS = Object.freeze(['brotli', 'gzip', 'none']);
34
-
35
- // --------------------------------------------------------------------------
36
- // Bytes ↔ base64, without Buffer or atob assumptions
37
- // --------------------------------------------------------------------------
38
-
39
- const CHUNK = 0x8000;
40
-
41
- export function toB64(bytes) {
42
- if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64');
43
- let bin = '';
44
- for (let i = 0; i < bytes.length; i += CHUNK) {
45
- bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
46
- }
47
- return btoa(bin);
48
- }
49
-
50
- export function fromB64(s) {
51
- const str = String(s || '');
52
- if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(str, 'base64'));
53
- const bin = atob(str);
54
- const out = new Uint8Array(bin.length);
55
- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
56
- return out;
57
- }
58
-
59
- // --------------------------------------------------------------------------
60
- // Codecs
61
- // --------------------------------------------------------------------------
62
-
63
- /**
64
- * The no-op codec. A backup written with it is still encrypted — it is just larger.
65
- * Useful in tests and on a runtime that offers no compression at all.
66
- */
67
- export const identityCodec = Object.freeze({
68
- name: 'none',
69
- async compress(bytes) { return bytes; },
70
- async decompress(bytes) { return bytes; },
71
- });
72
-
73
- /**
74
- * The browser/Electron-renderer codec, built from CompressionStream.
75
- * Returns `null` when the runtime lacks the format, so a caller can fall back rather than
76
- * discover the gap at write time.
77
- */
78
- export function streamCodec(format = 'gzip') {
79
- if (typeof CompressionStream === 'undefined' || typeof DecompressionStream === 'undefined') return null;
80
- try {
81
- new CompressionStream(format);
82
- new DecompressionStream(format);
83
- } catch {
84
- return null;
85
- }
86
- const run = async (bytes, Stream) => {
87
- const stream = new Blob([bytes]).stream().pipeThrough(new Stream(format));
88
- return new Uint8Array(await new Response(stream).arrayBuffer());
89
- };
90
- return Object.freeze({
91
- name: format,
92
- compress: (b) => run(b, CompressionStream),
93
- decompress: (b) => run(b, DecompressionStream),
94
- });
95
- }
96
-
97
- /**
98
- * Build a codec from Node's zlib without importing it here — the caller passes the module,
99
- * so this file stays dependency-free and loadable in a browser.
100
- *
101
- * import zlib from 'node:zlib';
102
- * const codec = nodeCodec(zlib, 'brotli');
103
- */
104
- export function nodeCodec(zlib, format = 'brotli') {
105
- if (!zlib) return null;
106
- const pair = format === 'brotli'
107
- ? [zlib.brotliCompressSync, zlib.brotliDecompressSync]
108
- : format === 'gzip' ? [zlib.gzipSync, zlib.gunzipSync] : null;
109
- if (!pair || !pair[0] || !pair[1]) return null;
110
- const [enc, dec] = pair;
111
- return Object.freeze({
112
- name: format,
113
- async compress(bytes) { return new Uint8Array(enc(Buffer.from(bytes))); },
114
- async decompress(bytes) { return new Uint8Array(dec(Buffer.from(bytes))); },
115
- });
116
- }
117
-
118
- /** Pick the best codec a runtime offers, preferring the smallest output. */
119
- export function bestCodec({ zlib = null } = {}) {
120
- if (zlib) return nodeCodec(zlib, 'brotli') || nodeCodec(zlib, 'gzip') || identityCodec;
121
- return streamCodec('brotli') || streamCodec('gzip') || identityCodec;
122
- }
123
-
124
- // --------------------------------------------------------------------------
125
- // Key derivation
126
- // --------------------------------------------------------------------------
127
-
128
- async function deriveKey(passphrase, salt, iterations, usages, subtle) {
129
- const crypto_ = subtle || globalThis.crypto?.subtle;
130
- if (!crypto_) throw new BackupError('no WebCrypto available on this runtime');
131
- const base = await crypto_.importKey(
132
- 'raw', new TextEncoder().encode(String(passphrase)), 'PBKDF2', false, ['deriveKey'],
133
- );
134
- return crypto_.deriveKey(
135
- { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
136
- base,
137
- { name: 'AES-GCM', length: 256 },
138
- false,
139
- usages,
140
- );
141
- }
142
-
143
- // --------------------------------------------------------------------------
144
- // The two directions
145
- // --------------------------------------------------------------------------
146
-
147
- export function isEncryptedBackup(obj) {
148
- return !!obj && typeof obj === 'object' && obj.type === ENCRYPTED_TYPE;
149
- }
150
-
151
- /**
152
- * data + passphrase → envelope.
153
- *
154
- * `version` is the ENVELOPE's version, not the payload's — the backup body carries its own.
155
- */
156
- export async function encryptBackup(data, passphrase, {
157
- codec = null, iterations = KDF_ITERATIONS, subtle = null, random = null, version = 3,
158
- } = {}) {
159
- if (!passphrase) throw new BackupError('a passphrase is required to encrypt a backup');
160
- const c = codec || bestCodec();
161
- const rand = random || ((n) => globalThis.crypto.getRandomValues(new Uint8Array(n)));
162
-
163
- const plain = new TextEncoder().encode(JSON.stringify(data));
164
- const packed = await c.compress(plain);
165
-
166
- const salt = rand(16);
167
- const iv = rand(12);
168
- const key = await deriveKey(passphrase, salt, iterations, ['encrypt'], subtle);
169
- const cryptoSubtle = subtle || globalThis.crypto.subtle;
170
- const ct = new Uint8Array(await cryptoSubtle.encrypt({ name: 'AES-GCM', iv }, key, packed));
171
-
172
- return {
173
- type: ENCRYPTED_TYPE,
174
- version,
175
- kdf: { iterations, salt: toB64(salt) },
176
- cipher: 'AES-GCM',
177
- compression: c.name,
178
- iv: toB64(iv),
179
- ct: toB64(ct),
180
- };
181
- }
182
-
183
- /**
184
- * envelope + passphrase → the original data object.
185
- *
186
- * A wrong passphrase and a tampered file are the SAME failure here, and deliberately so:
187
- * AES-GCM's auth tag catches both, and telling them apart would be an oracle.
188
- */
189
- export async function decryptBackup(envelope, passphrase, { codec = null, subtle = null } = {}) {
190
- if (!isEncryptedBackup(envelope)) throw new BackupError('not an encrypted ChatPanel backup');
191
- if (!passphrase) throw new BackupError('a passphrase is required to decrypt this backup');
192
-
193
- const salt = fromB64(envelope.kdf?.salt);
194
- const iterations = Number(envelope.kdf?.iterations) || KDF_ITERATIONS;
195
- const key = await deriveKey(passphrase, salt, iterations, ['decrypt'], subtle);
196
- const cryptoSubtle = subtle || globalThis.crypto?.subtle;
197
- if (!cryptoSubtle) throw new BackupError('no WebCrypto available on this runtime');
198
-
199
- let payload;
200
- try {
201
- payload = new Uint8Array(await cryptoSubtle.decrypt(
202
- { name: 'AES-GCM', iv: fromB64(envelope.iv) }, key, fromB64(envelope.ct),
203
- ));
204
- } catch {
205
- throw new BackupError('wrong passphrase, or the backup file is corrupted');
206
- }
207
-
208
- // v1 wrote plaintext, v2 gzip, v3 may use brotli. The envelope says which; an absent
209
- // field means the oldest behaviour, which is why `none` and absent must agree.
210
- const want = envelope.compression || 'none';
211
- if (want !== 'none') {
212
- const c = codec || bestCodec();
213
- if (c.name !== want) {
214
- // Being explicit beats returning garbage: a gzip envelope handed a brotli-only codec
215
- // fails here with a readable reason instead of a JSON parse error twenty lines later.
216
- throw new BackupError(`this backup is ${want}-compressed; supply a matching codec`);
217
- }
218
- payload = await c.decompress(payload);
219
- }
220
- return JSON.parse(new TextDecoder().decode(payload));
221
- }
1
+ class a extends Error{constructor(n){super(n),this.name="BackupError"}}const w="chatpanel-backup-encrypted",h=25e4,O=Object.freeze(["brotli","gzip","none"]),b=32768;function f(e){if(typeof Buffer<"u")return Buffer.from(e).toString("base64");let n="";for(let r=0;r<e.length;r+=b)n+=String.fromCharCode.apply(null,e.subarray(r,r+b));return btoa(n)}function l(e){const n=String(e||"");if(typeof Buffer<"u")return new Uint8Array(Buffer.from(n,"base64"));const r=atob(n),o=new Uint8Array(r.length);for(let t=0;t<r.length;t++)o[t]=r.charCodeAt(t);return o}const S=Object.freeze({name:"none",async compress(e){return e},async decompress(e){return e}});function g(e="gzip"){if(typeof CompressionStream>"u"||typeof DecompressionStream>"u")return null;try{new CompressionStream(e),new DecompressionStream(e)}catch{return null}const n=async(r,o)=>{const t=new Blob([r]).stream().pipeThrough(new o(e));return new Uint8Array(await new Response(t).arrayBuffer())};return Object.freeze({name:e,compress:r=>n(r,CompressionStream),decompress:r=>n(r,DecompressionStream)})}function C(e,n="brotli"){if(!e)return null;const r=n==="brotli"?[e.brotliCompressSync,e.brotliDecompressSync]:n==="gzip"?[e.gzipSync,e.gunzipSync]:null;if(!r||!r[0]||!r[1])return null;const[o,t]=r;return Object.freeze({name:n,async compress(c){return new Uint8Array(o(Buffer.from(c)))},async decompress(c){return new Uint8Array(t(Buffer.from(c)))}})}function x({zlib:e=null}={}){return e?C(e,"brotli")||C(e,"gzip")||S:g("brotli")||g("gzip")||S}async function k(e,n,r,o,t){const c=t||globalThis.crypto?.subtle;if(!c)throw new a("no WebCrypto available on this runtime");const i=await c.importKey("raw",new TextEncoder().encode(String(e)),"PBKDF2",!1,["deriveKey"]);return c.deriveKey({name:"PBKDF2",salt:n,iterations:r,hash:"SHA-256"},i,{name:"AES-GCM",length:256},!1,o)}function D(e){return!!e&&typeof e=="object"&&e.type===w}async function U(e,n,{codec:r=null,iterations:o=h,subtle:t=null,random:c=null,version:i=3}={}){if(!n)throw new a("a passphrase is required to encrypt a backup");const p=r||x(),s=c||(T=>globalThis.crypto.getRandomValues(new Uint8Array(T))),u=new TextEncoder().encode(JSON.stringify(e)),y=await p.compress(u),d=s(16),m=s(12),B=await k(n,d,o,["encrypt"],t),A=t||globalThis.crypto.subtle,E=new Uint8Array(await A.encrypt({name:"AES-GCM",iv:m},B,y));return{type:w,version:i,kdf:{iterations:o,salt:f(d)},cipher:"AES-GCM",compression:p.name,iv:f(m),ct:f(E)}}async function K(e,n,{codec:r=null,subtle:o=null}={}){if(!D(e))throw new a("not an encrypted ChatPanel backup");if(!n)throw new a("a passphrase is required to decrypt this backup");const t=l(e.kdf?.salt),c=Number(e.kdf?.iterations)||h,i=await k(n,t,c,["decrypt"],o),p=o||globalThis.crypto?.subtle;if(!p)throw new a("no WebCrypto available on this runtime");let s;try{s=new Uint8Array(await p.decrypt({name:"AES-GCM",iv:l(e.iv)},i,l(e.ct)))}catch{throw new a("wrong passphrase, or the backup file is corrupted")}const u=e.compression||"none";if(u!=="none"){const y=r||x();if(y.name!==u)throw new a(`this backup is ${u}-compressed; supply a matching codec`);s=await y.decompress(s)}return JSON.parse(new TextDecoder().decode(s))}export{a as BackupError,O as COMPRESSIONS,w as ENCRYPTED_TYPE,h as KDF_ITERATIONS,x as bestCodec,K as decryptBackup,U as encryptBackup,l as fromB64,S as identityCodec,D as isEncryptedBackup,C as nodeCodec,g as streamCodec,f as toB64};
package/board-tool.js CHANGED
@@ -1,166 +1,3 @@
1
- // The `board` tool how a member operates on the board.
2
- //
3
- // Every role gets it, whatever its grants: it reaches nothing outside the run. Four actions:
4
- // `read` the threads this member may see, `post` a note or draft in a thread, `reply` to a
5
- // post (agree, dispute with a ref, extend), and `ask` — open an ask thread and WAIT for a
6
- // person to answer it, from either client. Members do not chat freely: a post is typed, a
7
- // reply hangs off a post, and an ask pauses the member's own task, never the run.
8
- //
9
- // A fifth, `request`: a member hands a PIECE of its task to someone else — the runner turns
10
- // it into a sub-task with its own thread, offers it to the members that fit, posts it on the
11
- // job board when none does, and (by default) waits for it and returns its findings here.
12
- //
13
- // Bound per task: the member's role and task are fixed at bind time, so a member cannot post
14
- // as someone else, and its ask lands in its own task's context.
1
+ import{boardText as g,POST_KINDS as x,ASK_TYPES as q}from"./team-board.js";import{normalizeRequest as T}from"./team-subtask.js";const b="board",S=10*6e4;function I(){return{name:b,annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1},description:`The team board — where the other members' work is, and where you speak to them and to the user. Actions: {"action":"read"} the threads you may see; {"action":"post","kind":"note|draft|question","text":"…","refs":["…"]} a post in your task's thread; {"action":"reply","postId":"…","kind":"note","text":"…","refs":["…"]} a reply to another member's post — agree, dispute with a ref, extend; {"action":"ask","type":"info|budget|permission|direction","text":"what you need and why","options":["…"]} asks the USER and waits for the answer (minutes). {"action":"request","title":"check the valuation","brief":"what to do and what done looks like","skills":["finance"],"grants":["web"],"wait":true} hands a piece of your task to another member (or, if none fits, to the job board): a sub-task with its own thread; with wait (default) you get its findings back here. Ask only when you are stuck — a fact you could not find, a choice only the user can make. Otherwise decide, say what you assumed, and go on. Request only work you should not do yourself — another skill, a tool you lack, or a second pair of eyes.`,parameters:{type:"object",properties:{action:{type:"string",enum:["read","post","reply","ask","request"]},kind:{type:"string",enum:x.filter(e=>e!=="finding"&&e!=="answer"&&e!=="decision")},text:{type:"string"},refs:{type:"array",items:{type:"string"}},postId:{type:"string",description:"For reply: the post to reply to."},threadId:{type:"string",description:"For post: another thread you may see (default: your task's)."},type:{type:"string",enum:[...q],description:"For ask."},options:{type:"array",items:{type:"string"},description:"For ask: choices to offer the user."},title:{type:"string",description:"For request: the sub-task in a few words."},brief:{type:"string",description:"For request: what to do and what done looks like."},skills:{type:"array",items:{type:"string"},description:"For request: skills it needs."},grants:{type:"array",items:{type:"string"},description:"For request: tools it needs — data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr."},wait:{type:"boolean",description:"For request: wait for it and get its findings back (default true); false queues it after your task."}},required:["action"]}}}const a=e=>JSON.stringify(e);function O({board:e,role:o,taskId:n,taskIds:s=null,waitFor:i=null,onAsk:u=null,onRequest:p=null,askTimeoutMs:l=S,signal:h=null}={}){const f=()=>e.threadForTask(n)||e.openThread({taskId:n,kind:"task",title:n,by:"runner"});return{id:"board",specs:[I()],system:"Other members' work is on the board (the `board` tool): read it before repeating a lookup, reply where you disagree, and ask the user only when you are stuck.",async execute(k,d){if(k!==b)return a({error:`Unknown tool: ${k}`});const c=String(d?.action||"");if(c==="read"){const r=g(e,{taskIds:s,role:o}),y=e.threads().filter(t=>t.kind!=="proposal").map(t=>({id:t.id,kind:t.kind,title:t.title,status:t.status,by:t.by,posts:t.posts}));return a({board:r||"Nothing on the board yet.",threads:y,postIds:e.posts().filter(t=>t.kind!=="answer").map(t=>({id:t.id,threadId:t.threadId,by:t.by,kind:t.kind,head:t.text.slice(0,80)}))})}const w=String(d?.text||"").trim();if(c==="post"||c==="reply"){if(!w)return a({error:`${c} needs text.`});let r=f().id,y=null;if(c==="reply"){const m=e.postById(String(d?.postId||""));if(!m)return a({error:`No post ${d?.postId}. Read the board for post ids.`});r=m.threadId,y=m.id}else d?.threadId&&e.thread(String(d.threadId))&&(r=String(d.threadId));const t=e.post({threadId:r,by:o,kind:d?.kind||"note",text:w,refs:d?.refs,replyTo:y});return a({posted:t.id,threadId:r,replyTo:y})}if(c==="ask"){if(!w)return a({error:"ask needs text — what you need and why."});if(typeof i!="function")return a({error:"This run cannot wait for the user. Decide on your best assumption and say what you assumed."});const{thread:r,post:y}=e.ask({taskId:n,by:o,type:d?.type,text:w,options:d?.options,timeoutMs:l});u?.(r,y);const t=await i(r.id,l,h);return a(t?{answered:!0,threadId:r.id,answer:t.text,by:t.by}:{answered:!1,threadId:r.id,hint:"No answer arrived in time. Proceed on your best assumption, say what you assumed, and note that the user did not answer."})}if(c==="request"){if(typeof p!="function")return a({error:"This run cannot delegate. Do the work yourself with the tools you have, and say what you could not do."});const r=T(d);if(!r.ok)return a({error:r.error});const y=e.post({threadId:f().id,by:o,kind:"request",text:`${r.request.title}${r.request.brief&&r.request.brief!==r.request.title?` — ${r.request.brief}`:""}`,refs:[]}),t=await p({...r.request,postId:y.id});return a(t||{error:"the request was not taken"})}return a({error:`Unknown action "${c}". Use read, post, reply, ask or request.`})}}}function _(){const e=new Map,o=new Map;return{wait(n,s,i=null){if(o.has(n)){const u=o.get(n);return o.delete(n),Promise.resolve(u)}return new Promise(u=>{let p=null;const l=f=>{clearTimeout(p),e.delete(n),i?.removeEventListener?.("abort",h),u(f)},h=()=>l(null);e.set(n,l),s>0&&(p=setTimeout(()=>l(null),s)),i?.addEventListener?.("abort",h,{once:!0})})},answer(n,s){const i={text:String(s?.text??s??""),by:s?.by||"person",id:s?.id||null},u=e.get(n);return u?u(i):o.set(n,i),!!u},get pending(){return[...e.keys()]}}}function E(e,o){if(!e)return{specs:[...o.specs],system:o.system,execute:(s,i)=>o.execute(s,i)};const n=new Set(o.specs.map(s=>s.name));return{...e,specs:[...o.specs,...(e.specs||[]).filter(s=>!n.has(s.name))],system:[o.system,e.system].filter(Boolean).join(`
15
2
 
16
- import { boardText, POST_KINDS, ASK_TYPES } from './team-board.js';
17
- import { normalizeRequest } from './team-subtask.js';
18
-
19
- export const BOARD_TOOL_NAME = 'board';
20
- export const DEFAULT_ASK_TIMEOUT_MS = 10 * 60_000;
21
-
22
- export function boardToolSpec() {
23
- return {
24
- name: BOARD_TOOL_NAME,
25
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
26
- description:
27
- 'The team board — where the other members\' work is, and where you speak to them and to the user. '
28
- + 'Actions: {"action":"read"} the threads you may see; '
29
- + '{"action":"post","kind":"note|draft|question","text":"…","refs":["…"]} a post in your task\'s thread; '
30
- + '{"action":"reply","postId":"…","kind":"note","text":"…","refs":["…"]} a reply to another member\'s post — agree, dispute with a ref, extend; '
31
- + '{"action":"ask","type":"info|budget|permission|direction","text":"what you need and why","options":["…"]} asks the USER and waits for the answer (minutes). '
32
- + '{"action":"request","title":"check the valuation","brief":"what to do and what done looks like","skills":["finance"],"grants":["web"],"wait":true} hands a piece of your task to another member (or, if none fits, to the job board): a sub-task with its own thread; with wait (default) you get its findings back here. '
33
- + 'Ask only when you are stuck — a fact you could not find, a choice only the user can make. Otherwise decide, say what you assumed, and go on. Request only work you should not do yourself — another skill, a tool you lack, or a second pair of eyes.',
34
- parameters: {
35
- type: 'object',
36
- properties: {
37
- action: { type: 'string', enum: ['read', 'post', 'reply', 'ask', 'request'] },
38
- kind: { type: 'string', enum: POST_KINDS.filter((k) => k !== 'finding' && k !== 'answer' && k !== 'decision') },
39
- text: { type: 'string' },
40
- refs: { type: 'array', items: { type: 'string' } },
41
- postId: { type: 'string', description: 'For reply: the post to reply to.' },
42
- threadId: { type: 'string', description: 'For post: another thread you may see (default: your task\'s).' },
43
- type: { type: 'string', enum: [...ASK_TYPES], description: 'For ask.' },
44
- options: { type: 'array', items: { type: 'string' }, description: 'For ask: choices to offer the user.' },
45
- title: { type: 'string', description: 'For request: the sub-task in a few words.' },
46
- brief: { type: 'string', description: 'For request: what to do and what done looks like.' },
47
- skills: { type: 'array', items: { type: 'string' }, description: 'For request: skills it needs.' },
48
- grants: { type: 'array', items: { type: 'string' }, description: 'For request: tools it needs — data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr.' },
49
- wait: { type: 'boolean', description: 'For request: wait for it and get its findings back (default true); false queues it after your task.' },
50
- },
51
- required: ['action'],
52
- },
53
- };
54
- }
55
-
56
- const json = (v) => JSON.stringify(v);
57
-
58
- /**
59
- * @param board the run's live board (team-board.js createBoard)
60
- * @param role the member's role id
61
- * @param taskId the member's task
62
- * @param taskIds the tasks this member may read (its dependencies, or null for all)
63
- * @param waitFor `async (threadId, timeoutMs, signal) => { text, by } | null` — the runner's answer box
64
- * @param onAsk `(thread, post) => void` — the runner marks the task waiting
65
- * @param onRequest `async ({ title, brief, needs, wait }) => result` — the runner turns a request
66
- * into a sub-task (team-subtask.js); absent, a member is told to do it itself
67
- */
68
- export function boardToolProvider({ board, role, taskId, taskIds = null, waitFor = null, onAsk = null, onRequest = null, askTimeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal = null } = {}) {
69
- const ownThread = () => board.threadForTask(taskId) || board.openThread({ taskId, kind: 'task', title: taskId, by: 'runner' });
70
- return {
71
- id: 'board',
72
- specs: [boardToolSpec()],
73
- system: 'Other members\' work is on the board (the `board` tool): read it before repeating a lookup, reply where you disagree, and ask the user only when you are stuck.',
74
- async execute(name, input) {
75
- if (name !== BOARD_TOOL_NAME) return json({ error: `Unknown tool: ${name}` });
76
- const action = String(input?.action || '');
77
- if (action === 'read') {
78
- const text = boardText(board, { taskIds, role });
79
- const threads = board.threads().filter((t) => t.kind !== 'proposal').map((t) => ({ id: t.id, kind: t.kind, title: t.title, status: t.status, by: t.by, posts: t.posts }));
80
- return json({ board: text || 'Nothing on the board yet.', threads, postIds: board.posts().filter((p) => p.kind !== 'answer').map((p) => ({ id: p.id, threadId: p.threadId, by: p.by, kind: p.kind, head: p.text.slice(0, 80) })) });
81
- }
82
- const text = String(input?.text || '').trim();
83
- if (action === 'post' || action === 'reply') {
84
- if (!text) return json({ error: `${action} needs text.` });
85
- let threadId = ownThread().id;
86
- let replyTo = null;
87
- if (action === 'reply') {
88
- const target = board.postById(String(input?.postId || ''));
89
- if (!target) return json({ error: `No post ${input?.postId}. Read the board for post ids.` });
90
- threadId = target.threadId; replyTo = target.id;
91
- } else if (input?.threadId && board.thread(String(input.threadId))) {
92
- threadId = String(input.threadId);
93
- }
94
- const post = board.post({ threadId, by: role, kind: input?.kind || 'note', text, refs: input?.refs, replyTo });
95
- return json({ posted: post.id, threadId, replyTo });
96
- }
97
- if (action === 'ask') {
98
- if (!text) return json({ error: 'ask needs text — what you need and why.' });
99
- if (typeof waitFor !== 'function') return json({ error: 'This run cannot wait for the user. Decide on your best assumption and say what you assumed.' });
100
- const { thread, post } = board.ask({ taskId, by: role, type: input?.type, text, options: input?.options, timeoutMs: askTimeoutMs });
101
- onAsk?.(thread, post);
102
- const answer = await waitFor(thread.id, askTimeoutMs, signal);
103
- if (!answer) return json({ answered: false, threadId: thread.id, hint: 'No answer arrived in time. Proceed on your best assumption, say what you assumed, and note that the user did not answer.' });
104
- return json({ answered: true, threadId: thread.id, answer: answer.text, by: answer.by });
105
- }
106
- if (action === 'request') {
107
- if (typeof onRequest !== 'function') return json({ error: 'This run cannot delegate. Do the work yourself with the tools you have, and say what you could not do.' });
108
- const r = normalizeRequest(input);
109
- if (!r.ok) return json({ error: r.error });
110
- // The request is a post in the member's own thread — the record of who asked for what.
111
- const post = board.post({ threadId: ownThread().id, by: role, kind: 'request', text: `${r.request.title}${r.request.brief && r.request.brief !== r.request.title ? ` — ${r.request.brief}` : ''}`, refs: [] });
112
- const result = await onRequest({ ...r.request, postId: post.id });
113
- return json(result || { error: 'the request was not taken' });
114
- }
115
- return json({ error: `Unknown action "${action}". Use read, post, reply, ask or request.` });
116
- },
117
- };
118
- }
119
-
120
- /**
121
- * The runner's answer box: `wait(threadId, ms, signal)` resolves when `answer(threadId, …)`
122
- * is called — by the host, from its own UI or from the run store's tail when the OTHER
123
- * client answered. An answer that arrives before anyone waits is kept.
124
- */
125
- export function createAnswerBox() {
126
- const waiting = new Map(); // threadId -> resolve
127
- const early = new Map(); // threadId -> answer
128
- return {
129
- wait(threadId, timeoutMs, signal = null) {
130
- if (early.has(threadId)) { const a = early.get(threadId); early.delete(threadId); return Promise.resolve(a); }
131
- return new Promise((resolve) => {
132
- let timer = null;
133
- const done = (v) => { clearTimeout(timer); waiting.delete(threadId); signal?.removeEventListener?.('abort', onAbort); resolve(v); };
134
- const onAbort = () => done(null);
135
- waiting.set(threadId, done);
136
- if (timeoutMs > 0) timer = setTimeout(() => done(null), timeoutMs);
137
- if (typeof timer?.unref === 'function') timer.unref();
138
- signal?.addEventListener?.('abort', onAbort, { once: true });
139
- });
140
- },
141
- answer(threadId, answer) {
142
- const a = { text: String(answer?.text ?? answer ?? ''), by: answer?.by || 'person', id: answer?.id || null };
143
- const w = waiting.get(threadId);
144
- if (w) w(a); else early.set(threadId, a);
145
- return !!w;
146
- },
147
- get pending() { return [...waiting.keys()]; },
148
- };
149
- }
150
-
151
- /** The host's toolset with the board tool added — first, so it is read first. */
152
- export function withBoardTool(toolset, provider) {
153
- if (!toolset) {
154
- return {
155
- specs: [...provider.specs], system: provider.system,
156
- execute: (name, input) => provider.execute(name, input),
157
- };
158
- }
159
- const own = new Set(provider.specs.map((s) => s.name));
160
- return {
161
- ...toolset,
162
- specs: [...provider.specs, ...(toolset.specs || []).filter((s) => !own.has(s.name))],
163
- system: [provider.system, toolset.system].filter(Boolean).join('\n\n'),
164
- execute: (name, input, ...rest) => (own.has(name) ? provider.execute(name, input) : toolset.execute(name, input, ...rest)),
165
- };
166
- }
3
+ `),execute:(s,i,...u)=>n.has(s)?o.execute(s,i):e.execute(s,i,...u)}}export{b as BOARD_TOOL_NAME,S as DEFAULT_ASK_TIMEOUT_MS,O as boardToolProvider,I as boardToolSpec,_ as createAnswerBox,E as withBoardTool};
package/budget.js CHANGED
@@ -1,116 +1 @@
1
- // A budget the number a run may not exceed, and the record of what it spent.
2
- //
3
- // There was no spend cap anywhere. A live monitor is declared class C and starts model turns
4
- // for the length of a meeting; a spoken "keep an eye on X" arms that with nothing bounding
5
- // it; `jobs.js` caps a per-day job COUNT, not spend. The class declarations on every intent
6
- // and rule were made for exactly this, and nothing read them.
7
- //
8
- // A budget is a VALUE: declared on the thing that spends (a team, a schedule, a monitor),
9
- // charged by whatever runs it, and carried on the run record so the ledger can say what a
10
- // run cost in the same units it was capped in. Four dimensions, because the expensive thing
11
- // differs by executor: tokens and calls for a model, wall time for an agent that thinks for
12
- // minutes, cost when the gateway can report it. Any dimension may be absent; an absent one is
13
- // not enforced. A budget with NO dimension is not a budget — `validateBudget` refuses it, and
14
- // a team without one does not run (F8, O1).
15
- //
16
- // Pure. `now` is injected so a wall-time cap is testable.
17
-
18
- export const BUDGET_DIMENSIONS = Object.freeze(['tokens', 'calls', 'ms', 'usd']);
19
-
20
- export class BudgetError extends Error {
21
- constructor(code, message) { super(message); this.name = 'BudgetError'; this.code = code; }
22
- }
23
-
24
- /** `{ ok, errors }` — a budget must cap at least one thing, and every cap must be a positive number. */
25
- export function validateBudget(b) {
26
- const errors = [];
27
- if (!b || typeof b !== 'object') return { ok: false, errors: ['budget must be an object'] };
28
- let any = false;
29
- for (const k of BUDGET_DIMENSIONS) {
30
- if (b[k] === undefined || b[k] === null) continue;
31
- const n = Number(b[k]);
32
- if (!Number.isFinite(n) || n <= 0) errors.push(`${k}: a positive number`);
33
- else any = true;
34
- }
35
- for (const k of Object.keys(b)) if (!BUDGET_DIMENSIONS.includes(k)) errors.push(`${k}: not a budget dimension (${BUDGET_DIMENSIONS.join(', ')})`);
36
- if (!any) errors.push('a budget must cap at least one of tokens, calls, ms, usd');
37
- return { ok: errors.length === 0, errors };
38
- }
39
-
40
- /** Only the declared dimensions, as numbers. */
41
- export function normalizeBudget(b) {
42
- const out = {};
43
- for (const k of BUDGET_DIMENSIONS) {
44
- const n = Number(b?.[k]);
45
- if (Number.isFinite(n) && n > 0) out[k] = n;
46
- }
47
- return out;
48
- }
49
-
50
- /**
51
- * The usage a model call reports, in the shapes the providers use, as one record:
52
- * `{ tokens, calls, usd }`. `ms` is measured by the budget itself.
53
- */
54
- export function usageOf(u = {}) {
55
- const tokens = Number(u.tokens ?? u.total_tokens ?? ((Number(u.input_tokens ?? u.prompt_tokens) || 0) + (Number(u.output_tokens ?? u.completion_tokens) || 0)));
56
- return { tokens: Number.isFinite(tokens) ? tokens : 0, calls: Number(u.calls ?? 1) || 0, usd: Number(u.usd ?? u.cost) || 0 };
57
- }
58
-
59
- /**
60
- * A live budget for one run.
61
- *
62
- * charge(usage) add a model call's usage; returns what is left
63
- * canAfford(estimate) false when an estimated call would cross a cap — ask BEFORE calling
64
- * exhausted() the dimension that ran out, or null
65
- * snapshot() { cap, spent, remaining, exhausted } for the run record and the meter
66
- */
67
- export function createBudget(declared, { now = () => Date.now() } = {}) {
68
- const v = validateBudget(declared);
69
- if (!v.ok) throw new BudgetError('INVALID', v.errors.join('; '));
70
- const cap = { ...normalizeBudget(declared) };
71
- const startedAt = now();
72
- const spent = { tokens: 0, calls: 0, usd: 0 };
73
- const elapsed = () => now() - startedAt;
74
- const remaining = () => {
75
- const out = {};
76
- for (const k of BUDGET_DIMENSIONS) {
77
- if (cap[k] === undefined) continue;
78
- out[k] = Math.max(0, cap[k] - (k === 'ms' ? elapsed() : spent[k]));
79
- }
80
- return out;
81
- };
82
- const exhausted = () => {
83
- for (const k of BUDGET_DIMENSIONS) {
84
- if (cap[k] === undefined) continue;
85
- if ((k === 'ms' ? elapsed() : spent[k]) >= cap[k]) return k;
86
- }
87
- return null;
88
- };
89
- return {
90
- cap,
91
- charge(usage) {
92
- const u = usageOf(usage);
93
- spent.tokens += u.tokens; spent.calls += u.calls; spent.usd += u.usd;
94
- return remaining();
95
- },
96
- canAfford(estimate = {}) {
97
- if (exhausted()) return false;
98
- const e = usageOf({ calls: 1, ...estimate });
99
- for (const k of ['tokens', 'calls', 'usd']) {
100
- if (cap[k] !== undefined && spent[k] + e[k] > cap[k]) return false;
101
- }
102
- return true;
103
- },
104
- remaining,
105
- exhausted,
106
- /** A person raised the cap mid-run (a budget ask answered "allow"): by a factor, once. */
107
- raise(factor = 1.5) {
108
- const f = Math.max(1, Number(factor) || 1);
109
- for (const k of Object.keys(cap)) if (cap[k] !== undefined) cap[k] = Math.ceil(cap[k] * f);
110
- return { ...cap };
111
- },
112
- snapshot() {
113
- return { cap, spent: { ...spent, ms: elapsed() }, remaining: remaining(), exhausted: exhausted() };
114
- },
115
- };
116
- }
1
+ const i=Object.freeze(["tokens","calls","ms","usd"]);class k extends Error{constructor(n,r){super(r),this.name="BudgetError",this.code=n}}function p(t){const n=[];if(!t||typeof t!="object")return{ok:!1,errors:["budget must be an object"]};let r=!1;for(const e of i){if(t[e]===void 0||t[e]===null)continue;const f=Number(t[e]);!Number.isFinite(f)||f<=0?n.push(`${e}: a positive number`):r=!0}for(const e of Object.keys(t))i.includes(e)||n.push(`${e}: not a budget dimension (${i.join(", ")})`);return r||n.push("a budget must cap at least one of tokens, calls, ms, usd"),{ok:n.length===0,errors:n}}function b(t){const n={};for(const r of i){const e=Number(t?.[r]);Number.isFinite(e)&&e>0&&(n[r]=e)}return n}function m(t={}){const n=Number(t.tokens??t.total_tokens??(Number(t.input_tokens??t.prompt_tokens)||0)+(Number(t.output_tokens??t.completion_tokens)||0));return{tokens:Number.isFinite(n)?n:0,calls:Number(t.calls??1)||0,usd:Number(t.usd??t.cost)||0}}function h(t,{now:n=()=>Date.now()}={}){const r=p(t);if(!r.ok)throw new k("INVALID",r.errors.join("; "));const e={...b(t)},f=n(),c={tokens:0,calls:0,usd:0},a=()=>n()-f,l=()=>{const s={};for(const o of i)e[o]!==void 0&&(s[o]=Math.max(0,e[o]-(o==="ms"?a():c[o])));return s},d=()=>{for(const s of i)if(e[s]!==void 0&&(s==="ms"?a():c[s])>=e[s])return s;return null};return{cap:e,charge(s){const o=m(s);return c.tokens+=o.tokens,c.calls+=o.calls,c.usd+=o.usd,l()},canAfford(s={}){if(d())return!1;const o=m({calls:1,...s});for(const u of["tokens","calls","usd"])if(e[u]!==void 0&&c[u]+o[u]>e[u])return!1;return!0},remaining:l,exhausted:d,raise(s=1.5){const o=Math.max(1,Number(s)||1);for(const u of Object.keys(e))e[u]!==void 0&&(e[u]=Math.ceil(e[u]*o));return{...e}},snapshot(){return{cap:e,spent:{...c,ms:a()},remaining:l(),exhausted:d()}}}}export{i as BUDGET_DIMENSIONS,k as BudgetError,h as createBudget,b as normalizeBudget,m as usageOf,p as validateBudget};