@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/recruit.js
CHANGED
|
@@ -1,457 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
// Applications are COMPUTED, not asked for: every agent in the pool applies to every job at
|
|
5
|
-
// once, scored by `fit` (scorecard.js) on its skills, tools, grants and its attested record —
|
|
6
|
-
// the model-ADJUSTED rating when the engine rows say what its engines were worth. Recruiting
|
|
7
|
-
// costs one model turn per job, not one per applicant, and that turn is optional.
|
|
8
|
-
//
|
|
9
|
-
// What is recruited is an (agent, engine) PAIR. An agent's engine may be fixed (`model`,
|
|
10
|
-
// `harness`), the chat's (`assistant`), or `auto` with a policy; `routeFor` resolves it
|
|
11
|
-
// against the ENGINE ROWS the host knows — every model or harness it can run, with reach,
|
|
12
|
-
// capabilities, quality / latency / cost (the engine card's observed values over the router's
|
|
13
|
-
// guess, `engineRow`) and whether it is available right now. Requirements eliminate first
|
|
14
|
-
// (reach from the project's privacy setting — never learned, only typed; a work grant needs a
|
|
15
|
-
// harness; tools need `tools`), the policy orders what survives over observed values, and the
|
|
16
|
-
// agent's own record on each engine breaks ties. An agent whose engine does not clear is
|
|
17
|
-
// still an applicant — a person should see it — but is not recruitable now.
|
|
18
|
-
//
|
|
19
|
-
// The EVALUATOR is one structured call (RECRUIT_SCHEMA) over the top applications: the pick
|
|
20
|
-
// and why, or "none fits" with the agent that should exist. The call is the host's
|
|
21
|
-
// (`runStructured` in a client; a gateway without a model skips it): `decide` takes its parsed
|
|
22
|
-
// answer when there is one and falls back to the best fit above a floor when there is not, so
|
|
23
|
-
// a job is recruited with or without a model. The decision lands on the project record as
|
|
24
|
-
// events (`recruitEvents`) — the pick and the reasons on the job, a proposal as a decision a
|
|
25
|
-
// person reads — never as a mutation.
|
|
26
|
-
//
|
|
27
|
-
// Pure, dependency-free; imports only what the gateway already vendors (scorecard.js,
|
|
28
|
-
// model-ledger.js, engine.js, agent.js, job.js, team.js, structured.js, budget.js).
|
|
1
|
+
import{fit as K,engineKey as j,normalizeEngine as E}from"./scorecard.js";import{cardOverride as G,DEFAULT_MIN_CALLS as J}from"./model-ledger.js";import{engineKeyOf as Y,describeEngine as T}from"./engine.js";import{engineOf as H,agentFromForm as X,normalizeAgent as Q,resolveTeam as V}from"./agent.js";import{applyAll as Z,jobToRole as ee}from"./job.js";import{WORK_GRANTS as A}from"./team.js";import{defineSchema as ne,describeSchema as te,coerce as se}from"./structured.js";import{normalizeBudget as ie}from"./budget.js";const q=.5,oe=5,ae=1500,I={device:0,trusted:1,any:2},B=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),F=e=>e==null||!Number.isFinite(e)?null:Math.round(e*1e3)/1e3,k=(e,l)=>String(e||"").trim().slice(0,l),M=e=>(Array.isArray(e)?e:[]).map(l=>String(l).toLowerCase()),S=ne({name:"recruit",purpose:"which applicant gets the job, or that none fits and what agent should exist",fields:{pick:{type:"string",max:64,describe:'the id of the ONE applicant to recruit, or "" when none fits'},why:{type:"string",required:!0,max:400,describe:"one or two sentences a person reads on the job page"},confidence:{type:"number",describe:"0 to 1"},proposalName:{type:"string",max:60,describe:"when none fits: the agent that should exist"},proposalPurpose:{type:"string",max:300},proposalSkills:{type:"string[]",maxItems:12},proposalGrants:{type:"string[]",maxItems:8,describe:"from: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr"}},nothing:{pick:"",why:"none fits"}});function _(e,{card:l=null,minCalls:t=J,jobKind:s=null}={}){if(!B(e))return null;const o=e;let a=o.engine?E(o.engine):null;if(!a){const d=String(o.model||o.id||"");if(!d)return null;if(o.kind==="bridge"||o.kind==="harness"||o.classUsed==="A"){const f=d.indexOf("/");a=f>0?{kind:"harness",id:d.slice(0,f),model:d.slice(f+1)}:{kind:"harness",id:d}}else a={kind:"model",id:d}}const r=j(a);if(!r)return null;const{override:i,observed:c}=G(l,{minCalls:t,jobKind:s}),n=new Set(l?.capabilities?.withdrawn||[]),h=[...new Set(M(o.capabilities))].filter(d=>!n.has(d));n.size&&M(o.capabilities).some(d=>n.has(d))&&c.push("capabilities");const p=d=>d==null||d===""||!Number.isFinite(Number(d))?null:Number(d),u=I[o.reach]!=null?o.reach:"any",g=i.costPer1k??p(o.costPer1k)??(u==="device"&&a.kind==="model"?0:null);return{key:r,engine:a,label:k(o.label||o.name||le(a),120),reach:u,capabilities:h,quality:i.quality??p(o.quality),latencyMs:i.latencyMs??p(o.latencyMs),costPer1k:g,costPerTask:p(l?.cost?.perTask),availability:p(l?.availability?.rate),available:i.available??(o.available!==!1&&o.usable!==!1),observed:c}}const le=e=>`${e.id}${e.model&&e.model!==e.id?`/${e.model}`:""}`;function Ne(e,{cards:l={},minCalls:t,jobKind:s}={}){const o=[],a=new Set;for(const r of Array.isArray(e)?e:[]){const i=_(r,{card:null,minCalls:t,jobKind:s});!i||a.has(i.key)||(a.add(i.key),o.push(l[i.key]?_(r,{card:l[i.key],minCalls:t,jobKind:s}):i))}return o}function L(e,{reach:l="any"}={}){const t=M(e?.needs?.grants).filter(c=>c!=="none"),s=M(e?.needs?.tools),o=t.some(c=>A.includes(c)),a=[],r=[];(s.length||t.length)&&(a.push("tools"),r.push("the job uses tools")),o&&r.push(`a work grant (${t.filter(c=>A.includes(c)).join(", ")}) needs an agent tool`);const i=I[l]!=null?l:"any";return i!=="any"&&r.push(`reach ≤ ${i} (the project's privacy setting)`),{capabilities:a,harness:o,reach:i,why:r}}const C=(e,l,t)=>{const s=[];e.available===!1&&s.push("unavailable right now"),I[e.reach]>I[l.reach]&&s.push(`reach ${e.reach} exceeds ${l.reach}`),l.harness&&e.engine.kind!=="harness"&&s.push("not an agent tool");const o=l.capabilities.filter(a=>!e.capabilities.includes(a));if(o.length&&!(e.engine.kind==="harness"&&o.every(a=>a==="tools"))&&s.push(`lacks ${o.join(", ")}`),t){const a=r=>(r||[]).some(i=>i===e.key||i===`${e.engine.kind}:${e.engine.id}`||i===e.engine.id||i===e.engine.model);t.allow?.length&&!a(t.allow)&&s.push("not on the policy's allow list"),t.deny?.length&&a(t.deny)&&s.push("on the policy's deny list"),t.floor?.quality!=null&&e.quality!=null&&e.quality<t.floor.quality&&s.push(`quality ${e.quality} under the floor ${t.floor.quality}`),t.floor?.availability!=null&&e.availability!=null&&e.availability<t.floor.availability&&s.push(`availability ${e.availability} under the floor ${t.floor.availability}`),t.ceiling?.costPerTask!=null&&e.costPerTask!=null&&e.costPerTask>t.ceiling.costPerTask&&s.push(`$${e.costPerTask}/task over the ceiling`),t.ceiling?.latencyMs!=null&&e.latencyMs!=null&&e.latencyMs>t.ceiling.latencyMs&&s.push(`${e.latencyMs} ms over the ceiling`)}return s},D=(e,l)=>(e?.byEngine||[]).find(t=>t.key===l)?.rating?.avg??null;function re(e,l,t){const s=n=>n.quality??.5,o=n=>n.costPerTask??n.costPer1k??null,a=Math.max(...e.map(n=>o(n)??0),0)||1,r=Math.max(...e.map(n=>n.latencyMs??0),0)||1,i=n=>{const h=D(t,n.key);return h==null?0:h-.5},c=n=>{switch(l){case"cheapest-that-clears":return-((o(n)??a)/a)+s(n)*.01;case"best-quality":return s(n)-(o(n)??a)/a*.01;case"fastest":return-((n.latencyMs??r)/r)+s(n)*.01;default:return s(n)*.5+(1-(o(n)??a)/a)*.25+(1-(n.latencyMs??r)/r)*.25}};return e.map(n=>({row:n,score:c(n)+i(n)*.05})).sort((n,h)=>h.score-n.score).map(n=>n.row)}const z=e=>`${e.key} (quality ${e.quality??"?"}${e.observed.includes("quality")?" observed":""}${e.costPerTask!=null?`, $${e.costPerTask}/task`:e.costPer1k!=null?`, $${e.costPer1k}/1k`:""}${e.latencyMs!=null?`, ${e.latencyMs} ms`:""})`;function ce(e,l,{rows:t=[],summary:s=null,need:o=null,reach:a="any",chatModel:r=null,explore:i=!1}={}){const c=o||L(l,{reach:a}),n=H(e,{chatModel:r}),h=Array.isArray(t)?t.filter(Boolean):[],p=m=>({engine:null,key:null,reasons:m,alternatives:[],exploration:!1,clears:!1});if(n.kind!=="auto"){const m=Y(n),v=h.find(x=>x.key===m)||h.find(x=>x.engine.kind===n.kind&&(n.kind==="harness"?x.engine.id===n.harnessId&&!n.model:(x.engine.id===n.model||x.engine.model===n.model)&&!n.providerId));if(!h.length)return{engine:E({kind:n.kind,id:n.kind==="harness"?n.harnessId:n.providerId||n.model,model:n.model}),key:m,reasons:[`${T(n)} — pinned by the agent; no roster to check it against`],alternatives:[],exploration:!1,clears:!0};if(!v)return p([`${T(n)} is pinned by the agent but is not installed or configured here`]);const R=C(v,c,null);return R.length?p([`${T(n)} is pinned by the agent but ${R.join("; ")}`]):{engine:v.engine,key:v.key,reasons:["pinned by the agent",...c.why],alternatives:[],exploration:!1,clears:!0}}if(!h.length)return p(["engine is auto and the roster is empty"]);const u=n.policy||{},g=[],d=h.filter(m=>{const $=C(m,c,u);return $.length&&g.push(`${m.key}: ${$.join("; ")}`),!$.length});if(!d.length)return p([`no engine clears ${c.harness?"an agent tool with ":""}${c.capabilities.join(", ")||"the requirements"}${c.reach!=="any"?` within reach ${c.reach}`:""}${u.prefer?` under ${u.prefer}`:""}`,...g.slice(0,4)]);const f=re(d,u.prefer||"balanced",s);let y=f[0],b=!1;const w=m=>m.costPerTask??m.costPer1k??null;if(i&&y.engine.kind!=="harness"){const m=f.filter($=>$.engine.kind!=="harness"&&w($)!=null&&w(y)!=null&&w($)<w(y)).sort(($,v)=>w(v)-w($));m.length&&(b=!0,y=m[0])}const N=[b?`exploration: one tier cheaper than the policy's pick (${f[0].key})`:`${(u.prefer||"balanced").replace(/-/g," ")}: ${z(y)}`,...c.why],O=D(s,y.key);return O!=null&&N.push(`this agent rated ${Math.round(O*100)}% on it before`),(u.floor?.quality!=null||u.ceiling?.costPerTask!=null||u.ceiling?.latencyMs!=null)&&N.push(`${d.length} of ${h.length} engines clear the policy`),{engine:y.engine,key:y.key,reasons:N,alternatives:f.filter(m=>m!==y).slice(0,4).map(m=>m.engine),exploration:b,clears:!0}}function ue(e){const l=new Map((e||[]).filter(Boolean).map(t=>[t.key,t]));return{qualityOf:t=>l.get(t)?.quality??null,costOf:t=>{const s=l.get(t);return s?s.costPerTask??null:null}}}function pe(e,l,{summaries:t={},rows:s=[],reach:o="any",chatModel:a=null,adjust:r=!0,now:i=Date.now()}={}){const c=L(e,{reach:o}),n=ue(s),h=(u,g,d)=>{const f=K(u,{...g,tools:g.tools||g.grants},d,r?n:{adjust:!1}),y=ce(g,u,{rows:s,summary:d,need:c,chatModel:a});return{score:f.score,reasons:[...f.reasons,...y.reasons].slice(0,8),...y.clears?{engine:y.engine}:{},covers:U(u,g)}},p=new Map((l||[]).map(u=>[u?.id,U(e,u)]));return Z(e,l,h,{cards:t,now:i}).map(u=>({...u,covers:p.get(u.agentId)!==!1}))}function U(e,l){const t=M(e?.needs?.skills);if(!t.length)return!0;const s=new Set(M(l?.skills));return t.some(o=>s.has(o))}function de(e,l,t=[],{rows:s=[],top:o=oe}={}){const a=new Map((t||[]).map(n=>[n.id,n])),r=new Map((s||[]).filter(Boolean).map(n=>[n.key,n])),i=e?.needs||{},c=(l||[]).slice(0,o).map(n=>{const h=a.get(n.agentId)||{},p=n.engine?j(n.engine):null,u=p?r.get(p):null;return`- ${n.agentId} (fit ${Math.round((n.fit||0)*100)}%)${h.purpose?` — ${k(h.purpose,160)}`:""}; skills: ${(h.skills||[]).join(", ")||"none"}; grants: ${(h.grants||[]).join(", ")||"none"}; ${p?`engine: ${u?z(u):p}`:"NOT RECRUITABLE NOW — no engine clears"}; ${(n.reasons||[]).slice(0,4).join("; ")}`});return[`You are the evaluator for the job "${k(e?.title,200)}" on project ${e?.projectId||"?"}. Recruit ONE applicant, or say none fits.`,`Brief: ${k(e?.brief,ae)}`,`Needs — skills: ${(i.skills||[]).join(", ")||"none named"}; tools: ${(i.tools||[]).join(", ")||"none named"}; grants: ${(i.grants||[]).join(", ")||"none"}${e?.budget?`; budget: ${Object.entries(e.budget).map(([n,h])=>`${h} ${n}`).join(", ")}`:""}.`,"","Applicants, best computed fit first (fit = the skills, tools and grants the job names, then the attested record, then size):",...c.length?c:["- (no one applied)"],"",'Rules: prefer the applicant that has what the job names and a record of clearing work like it on the engine shown; never pick one marked NOT RECRUITABLE NOW; a lower fit is right only when its reasons show the higher one lacks something the brief needs. When no applicant has the skills the job names, answer pick "" and propose the agent that should exist.',"",te(S)].join(`
|
|
2
|
+
`)}function W(e,l=[]){const t=se(e,S);if(!t)return null;const s=t.value,o=String(s.pick||"").trim(),a=o?(l||[]).find(i=>i.agentId===o):null,r=s.proposalName?{name:k(s.proposalName,60),purpose:k(s.proposalPurpose,300),skills:(s.proposalSkills||[]).map(i=>k(i,80)).filter(Boolean),grants:(s.proposalGrants||[]).map(i=>k(i,64)).filter(Boolean)}:null;return a&&a.engine?{pick:o,why:k(s.why,400)||"the evaluator's pick",confidence:F(s.confidence),proposal:null}:{pick:"",why:a?`the evaluator picked ${o}, which no engine can run right now`:k(s.why,400)||"none fits",confidence:F(s.confidence),proposal:r}}function he(e,l,{evaluation:t=null,minFit:s=q}={}){const o=l||[];if(t&&t.pick){const i=o.find(c=>c.agentId===t.pick&&c.engine);if(i)return{kind:"recruit",agentId:i.agentId,engine:i.engine,fit:i.fit,why:t.why,by:"evaluator"}}if(t&&!t.pick)return{kind:"none",why:t.why,proposal:t.proposal||P(e),by:"evaluator"};const a=o.find(i=>i.engine&&i.covers!==!1);return a&&a.fit>=s?{kind:"recruit",agentId:a.agentId,engine:a.engine,fit:a.fit,why:`best fit (${Math.round(a.fit*100)}%): ${(a.reasons||[])[0]||"meets the needs"}`,by:"fit"}:{kind:"none",why:o.length?a?`the best fit is ${Math.round(a.fit*100)}%, under the ${Math.round(s*100)}% floor: ${(a.reasons||[]).find(i=>/missing/.test(i))||a.reasons?.[0]||""}`:o.some(i=>i.engine)?`no applicant has a skill the job names (${(e?.needs?.skills||[]).join(", ")})`:`${o.length} applied but no engine clears for any of them: ${(o[0].reasons||[]).find(i=>/engine|pinned|roster/.test(i))||"see the applications"}`:"no one in the pool applies to jobs",proposal:P(e),by:"fit"}}function P(e){const l=e?.needs||{};return{name:k(e?.title,60)||"New agent",purpose:`Does jobs like "${k(e?.title,80)}".`,skills:[...l.skills||[]],grants:(l.grants||[]).length?[...l.grants]:["none"]}}function ge(e,l,{by:t="evaluator"}={}){const s=e||P(l),o=new Set(["data","web","history","mcp",...A]),a=(s.grants||[]).map(r=>String(r).toLowerCase()).filter(r=>o.has(r)||/^mcp:/.test(r));return X({name:s.name,purpose:s.purpose,skills:s.skills,prompt:`You are ${s.name}. ${s.purpose||""}
|
|
29
3
|
|
|
30
|
-
|
|
31
|
-
import { cardOverride, DEFAULT_MIN_CALLS } from './model-ledger.js';
|
|
32
|
-
import { normalizeEngineSpec, engineKeyOf, describeEngine } from './engine.js';
|
|
33
|
-
import { engineOf, agentFromForm, normalizeAgent, resolveTeam } from './agent.js';
|
|
34
|
-
import { applyAll, jobToRole } from './job.js';
|
|
35
|
-
import { WORK_GRANTS } from './team.js';
|
|
36
|
-
import { defineSchema, describeSchema, coerce } from './structured.js';
|
|
37
|
-
import { normalizeBudget } from './budget.js';
|
|
38
|
-
|
|
39
|
-
export const MIN_FIT = 0.5;
|
|
40
|
-
export const TOP_APPLICANTS = 5;
|
|
41
|
-
export const MAX_BRIEF_IN_PROMPT = 1500;
|
|
42
|
-
const REACH_RANK = { device: 0, trusted: 1, any: 2 };
|
|
43
|
-
|
|
44
|
-
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
45
|
-
const r3 = (v) => (v == null || !Number.isFinite(v) ? null : Math.round(v * 1000) / 1000);
|
|
46
|
-
const clip = (s, n) => String(s || '').trim().slice(0, n);
|
|
47
|
-
const lower = (xs) => (Array.isArray(xs) ? xs : []).map((x) => String(x).toLowerCase());
|
|
48
|
-
|
|
49
|
-
// ── The evaluator's answer ────────────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
export const RECRUIT_SCHEMA = defineSchema({
|
|
52
|
-
name: 'recruit',
|
|
53
|
-
purpose: 'which applicant gets the job, or that none fits and what agent should exist',
|
|
54
|
-
fields: {
|
|
55
|
-
// Not `required`: an emptied required field reads as "nothing" and would drop the proposal that rides beside it.
|
|
56
|
-
pick: { type: 'string', max: 64, describe: 'the id of the ONE applicant to recruit, or "" when none fits' },
|
|
57
|
-
why: { type: 'string', required: true, max: 400, describe: 'one or two sentences a person reads on the job page' },
|
|
58
|
-
confidence: { type: 'number', describe: '0 to 1' },
|
|
59
|
-
proposalName: { type: 'string', max: 60, describe: 'when none fits: the agent that should exist' },
|
|
60
|
-
proposalPurpose: { type: 'string', max: 300 },
|
|
61
|
-
proposalSkills: { type: 'string[]', maxItems: 12 },
|
|
62
|
-
proposalGrants: { type: 'string[]', maxItems: 8, describe: 'from: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr' },
|
|
63
|
-
},
|
|
64
|
-
nothing: { pick: '', why: 'none fits' },
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// ── Engine rows: what the host can run, as the recruiter reads it ─────────────────────────
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* One row from a host's candidate (a router model — `inferCandidate`'s shape: `id`, `model`,
|
|
71
|
-
* `reach`, `capabilities`, `quality`, `latencyMs`, `costPer1k`, `available`, `classUsed`;
|
|
72
|
-
* or anything carrying an `engine` ref) with its engine CARD applied: observed quality,
|
|
73
|
-
* latency, cost and availability replace the guess where there is enough history, and a
|
|
74
|
-
* withdrawn capability is gone. A bridge agent (`classUsed 'A'` / `kind 'bridge'`) is a
|
|
75
|
-
* harness — `claude/opus` is the harness `claude` asked to run `opus` — keyed the way the
|
|
76
|
-
* desktop's appointer and the engine ledger key it.
|
|
77
|
-
*/
|
|
78
|
-
export function engineRow(candidate, { card = null, minCalls = DEFAULT_MIN_CALLS, jobKind = null } = {}) {
|
|
79
|
-
if (!isRecord(candidate)) return null;
|
|
80
|
-
const c = candidate;
|
|
81
|
-
let engine = c.engine ? normalizeEngine(c.engine) : null;
|
|
82
|
-
if (!engine) {
|
|
83
|
-
const name = String(c.model || c.id || '');
|
|
84
|
-
if (!name) return null;
|
|
85
|
-
if (c.kind === 'bridge' || c.kind === 'harness' || c.classUsed === 'A') {
|
|
86
|
-
const slash = name.indexOf('/');
|
|
87
|
-
engine = slash > 0 ? { kind: 'harness', id: name.slice(0, slash), model: name.slice(slash + 1) } : { kind: 'harness', id: name };
|
|
88
|
-
} else engine = { kind: 'model', id: name };
|
|
89
|
-
}
|
|
90
|
-
const key = engineKey(engine);
|
|
91
|
-
if (!key) return null;
|
|
92
|
-
const { override, observed } = cardOverride(card, { minCalls, jobKind });
|
|
93
|
-
const withdrawn = new Set(card?.capabilities?.withdrawn || []);
|
|
94
|
-
const capabilities = [...new Set(lower(c.capabilities))].filter((x) => !withdrawn.has(x));
|
|
95
|
-
if (withdrawn.size && lower(c.capabilities).some((x) => withdrawn.has(x))) observed.push('capabilities');
|
|
96
|
-
const num = (v) => (v == null || v === '' || !Number.isFinite(Number(v)) ? null : Number(v));
|
|
97
|
-
const reach = REACH_RANK[c.reach] != null ? c.reach : 'any';
|
|
98
|
-
// A model on this machine costs nothing per token — the router's own rule (`costOf`), kept
|
|
99
|
-
// here for a host that has no guess to offer; an unknown cost anywhere else stays unknown
|
|
100
|
-
// and orders as the dearest, so "we did not price it" never reads as "free".
|
|
101
|
-
const costPer1k = override.costPer1k ?? num(c.costPer1k) ?? (reach === 'device' && engine.kind === 'model' ? 0 : null);
|
|
102
|
-
return {
|
|
103
|
-
key,
|
|
104
|
-
engine,
|
|
105
|
-
label: clip(c.label || c.name || engineName(engine), 120),
|
|
106
|
-
reach,
|
|
107
|
-
capabilities,
|
|
108
|
-
quality: override.quality ?? num(c.quality),
|
|
109
|
-
latencyMs: override.latencyMs ?? num(c.latencyMs),
|
|
110
|
-
costPer1k,
|
|
111
|
-
costPerTask: num(card?.cost?.perTask),
|
|
112
|
-
availability: num(card?.availability?.rate),
|
|
113
|
-
available: override.available ?? (c.available !== false && c.usable !== false),
|
|
114
|
-
observed,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
const engineName = (e) => `${e.id}${e.model && e.model !== e.id ? `/${e.model}` : ''}`;
|
|
118
|
-
|
|
119
|
-
/** Rows from a host's candidates and the cards it holds (by key). */
|
|
120
|
-
export function engineRows(candidates, { cards = {}, minCalls, jobKind } = {}) {
|
|
121
|
-
const out = []; const seen = new Set();
|
|
122
|
-
for (const c of Array.isArray(candidates) ? candidates : []) {
|
|
123
|
-
const row = engineRow(c, { card: null, minCalls, jobKind });
|
|
124
|
-
if (!row || seen.has(row.key)) continue;
|
|
125
|
-
seen.add(row.key);
|
|
126
|
-
out.push(cards[row.key] ? engineRow(c, { card: cards[row.key], minCalls, jobKind }) : row);
|
|
127
|
-
}
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── What the job needs of an engine ───────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Requirements eliminate; they are never traded for cost or speed. A work grant (`shell`,
|
|
135
|
-
* `fs:write`, `scm:*`) can only be exercised by a harness — a chat model has no shell. Tools
|
|
136
|
-
* or any grant beyond `none` mean the turn may call tools. Reach is the project's privacy
|
|
137
|
-
* ceiling, typed, never learned.
|
|
138
|
-
*/
|
|
139
|
-
export function needForJob(job, { reach = 'any' } = {}) {
|
|
140
|
-
const grants = lower(job?.needs?.grants).filter((g) => g !== 'none');
|
|
141
|
-
const tools = lower(job?.needs?.tools);
|
|
142
|
-
const harness = grants.some((g) => WORK_GRANTS.includes(g));
|
|
143
|
-
const capabilities = [];
|
|
144
|
-
const why = [];
|
|
145
|
-
if (tools.length || grants.length) { capabilities.push('tools'); why.push('the job uses tools'); }
|
|
146
|
-
if (harness) why.push(`a work grant (${grants.filter((g) => WORK_GRANTS.includes(g)).join(', ')}) needs an agent tool`);
|
|
147
|
-
const r = REACH_RANK[reach] != null ? reach : 'any';
|
|
148
|
-
if (r !== 'any') why.push(`reach ≤ ${r} (the project's privacy setting)`);
|
|
149
|
-
return { capabilities, harness, reach: r, why };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── Routing: the engine for this agent on this job ────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
const meets = (row, need, policy) => {
|
|
155
|
-
const why = [];
|
|
156
|
-
if (row.available === false) why.push('unavailable right now');
|
|
157
|
-
if (REACH_RANK[row.reach] > REACH_RANK[need.reach]) why.push(`reach ${row.reach} exceeds ${need.reach}`);
|
|
158
|
-
if (need.harness && row.engine.kind !== 'harness') why.push('not an agent tool');
|
|
159
|
-
const missing = need.capabilities.filter((c) => !row.capabilities.includes(c));
|
|
160
|
-
// A harness brings its own tools; the capability list of a bridge agent is the host's guess.
|
|
161
|
-
if (missing.length && !(row.engine.kind === 'harness' && missing.every((c) => c === 'tools'))) why.push(`lacks ${missing.join(', ')}`);
|
|
162
|
-
if (policy) {
|
|
163
|
-
const matches = (refs) => (refs || []).some((k) => k === row.key || k === `${row.engine.kind}:${row.engine.id}` || k === row.engine.id || k === row.engine.model);
|
|
164
|
-
if (policy.allow?.length && !matches(policy.allow)) why.push('not on the policy\'s allow list');
|
|
165
|
-
if (policy.deny?.length && matches(policy.deny)) why.push('on the policy\'s deny list');
|
|
166
|
-
if (policy.floor?.quality != null && row.quality != null && row.quality < policy.floor.quality) why.push(`quality ${row.quality} under the floor ${policy.floor.quality}`);
|
|
167
|
-
if (policy.floor?.availability != null && row.availability != null && row.availability < policy.floor.availability) why.push(`availability ${row.availability} under the floor ${policy.floor.availability}`);
|
|
168
|
-
if (policy.ceiling?.costPerTask != null && row.costPerTask != null && row.costPerTask > policy.ceiling.costPerTask) why.push(`$${row.costPerTask}/task over the ceiling`);
|
|
169
|
-
if (policy.ceiling?.latencyMs != null && row.latencyMs != null && row.latencyMs > policy.ceiling.latencyMs) why.push(`${row.latencyMs} ms over the ceiling`);
|
|
170
|
-
}
|
|
171
|
-
return why;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const ownRating = (summary, key) => (summary?.byEngine || []).find((r) => r.key === key)?.rating?.avg ?? null;
|
|
175
|
-
|
|
176
|
-
/** Order the rows that clear by the policy's preference; the agent's own record on an engine breaks ties. */
|
|
177
|
-
function orderByPolicy(rows, prefer, summary) {
|
|
178
|
-
const q = (r) => r.quality ?? 0.5;
|
|
179
|
-
const cost = (r) => r.costPerTask ?? r.costPer1k ?? null;
|
|
180
|
-
const maxCost = Math.max(...rows.map((r) => cost(r) ?? 0), 0) || 1;
|
|
181
|
-
const maxLat = Math.max(...rows.map((r) => r.latencyMs ?? 0), 0) || 1;
|
|
182
|
-
const own = (r) => { const v = ownRating(summary, r.key); return v == null ? 0 : v - 0.5; };
|
|
183
|
-
const score = (r) => {
|
|
184
|
-
switch (prefer) {
|
|
185
|
-
case 'cheapest-that-clears': return -((cost(r) ?? maxCost) / maxCost) + q(r) * 0.01;
|
|
186
|
-
case 'best-quality': return q(r) - ((cost(r) ?? maxCost) / maxCost) * 0.01;
|
|
187
|
-
case 'fastest': return -((r.latencyMs ?? maxLat) / maxLat) + q(r) * 0.01;
|
|
188
|
-
default: return q(r) * 0.5 + (1 - (cost(r) ?? maxCost) / maxCost) * 0.25 + (1 - (r.latencyMs ?? maxLat) / maxLat) * 0.25;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
return rows.map((r) => ({ row: r, score: score(r) + own(r) * 0.05 })).sort((a, b) => b.score - a.score).map((x) => x.row);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const rowLine = (r) => `${r.key} (quality ${r.quality ?? '?'}${r.observed.includes('quality') ? ' observed' : ''}${r.costPerTask != null ? `, $${r.costPerTask}/task` : r.costPer1k != null ? `, $${r.costPer1k}/1k` : ''}${r.latencyMs != null ? `, ${r.latencyMs} ms` : ''})`;
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* The engine this agent would run this job on, and why — `{ engine, key, reasons,
|
|
198
|
-
* alternatives, exploration, clears }`; `clears: false` (engine null) when nothing does, with
|
|
199
|
-
* the reasons. `rows` are the host's `engineRows`; an empty roster trusts a fixed spec and
|
|
200
|
-
* refuses `auto` (nothing to pick from). `explore` takes one tier cheaper than the policy's
|
|
201
|
-
* pick when a cheaper row clears — the project loop's bounded exploration (§13.4); never for
|
|
202
|
-
* a harness.
|
|
203
|
-
*/
|
|
204
|
-
export function routeFor(agent, job, { rows = [], summary = null, need = null, reach = 'any', chatModel = null, explore = false } = {}) {
|
|
205
|
-
const n = need || needForJob(job, { reach });
|
|
206
|
-
const spec = engineOf(agent, { chatModel });
|
|
207
|
-
const list = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
208
|
-
const none = (reasons) => ({ engine: null, key: null, reasons, alternatives: [], exploration: false, clears: false });
|
|
209
|
-
if (spec.kind !== 'auto') {
|
|
210
|
-
const key = engineKeyOf(spec);
|
|
211
|
-
const exact = list.find((r) => r.key === key);
|
|
212
|
-
// A harness card without a model matches any row of that harness; a model spec without a
|
|
213
|
-
// provider matches the row that runs that model anywhere.
|
|
214
|
-
const near = exact || list.find((r) => r.engine.kind === spec.kind && (spec.kind === 'harness' ? r.engine.id === spec.harnessId && !spec.model : (r.engine.id === spec.model || r.engine.model === spec.model) && !spec.providerId));
|
|
215
|
-
if (!list.length) return { engine: normalizeEngine({ kind: spec.kind, id: spec.kind === 'harness' ? spec.harnessId : (spec.providerId || spec.model), model: spec.model }), key, reasons: [`${describeEngine(spec)} — pinned by the agent; no roster to check it against`], alternatives: [], exploration: false, clears: true };
|
|
216
|
-
if (!near) return none([`${describeEngine(spec)} is pinned by the agent but is not installed or configured here`]);
|
|
217
|
-
const why = meets(near, n, null);
|
|
218
|
-
if (why.length) return none([`${describeEngine(spec)} is pinned by the agent but ${why.join('; ')}`]);
|
|
219
|
-
return { engine: near.engine, key: near.key, reasons: ['pinned by the agent', ...n.why], alternatives: [], exploration: false, clears: true };
|
|
220
|
-
}
|
|
221
|
-
if (!list.length) return none(['engine is auto and the roster is empty']);
|
|
222
|
-
const policy = spec.policy || {};
|
|
223
|
-
const rejected = [];
|
|
224
|
-
const cleared = list.filter((r) => { const why = meets(r, n, policy); if (why.length) rejected.push(`${r.key}: ${why.join('; ')}`); return !why.length; });
|
|
225
|
-
if (!cleared.length) return none([`no engine clears ${n.harness ? 'an agent tool with ' : ''}${n.capabilities.join(', ') || 'the requirements'}${n.reach !== 'any' ? ` within reach ${n.reach}` : ''}${policy.prefer ? ` under ${policy.prefer}` : ''}`, ...rejected.slice(0, 4)]);
|
|
226
|
-
const ordered = orderByPolicy(cleared, policy.prefer || 'balanced', summary);
|
|
227
|
-
let pick = ordered[0];
|
|
228
|
-
let exploration = false;
|
|
229
|
-
const cost = (r) => r.costPerTask ?? r.costPer1k ?? null;
|
|
230
|
-
if (explore && pick.engine.kind !== 'harness') {
|
|
231
|
-
const cheaper = ordered.filter((r) => r.engine.kind !== 'harness' && cost(r) != null && cost(pick) != null && cost(r) < cost(pick)).sort((a, b) => cost(b) - cost(a));
|
|
232
|
-
if (cheaper.length) { exploration = true; pick = cheaper[0]; }
|
|
233
|
-
}
|
|
234
|
-
const reasons = [
|
|
235
|
-
exploration ? `exploration: one tier cheaper than the policy's pick (${ordered[0].key})` : `${(policy.prefer || 'balanced').replace(/-/g, ' ')}: ${rowLine(pick)}`,
|
|
236
|
-
...n.why,
|
|
237
|
-
];
|
|
238
|
-
const own = ownRating(summary, pick.key);
|
|
239
|
-
if (own != null) reasons.push(`this agent rated ${Math.round(own * 100)}% on it before`);
|
|
240
|
-
if (policy.floor?.quality != null || policy.ceiling?.costPerTask != null || policy.ceiling?.latencyMs != null) reasons.push(`${cleared.length} of ${list.length} engines clear the policy`);
|
|
241
|
-
return { engine: pick.engine, key: pick.key, reasons, alternatives: ordered.filter((r) => r !== pick).slice(0, 4).map((r) => r.engine), exploration, clears: true };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── Applying: the whole pool at once ──────────────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
/** `qualityOf` / `costOf` for `adjustSummary`, read from the rows: what each engine is worth. */
|
|
247
|
-
export function engineWorth(rows) {
|
|
248
|
-
const byKey = new Map((rows || []).filter(Boolean).map((r) => [r.key, r]));
|
|
249
|
-
return {
|
|
250
|
-
qualityOf: (key) => byKey.get(key)?.quality ?? null,
|
|
251
|
-
costOf: (key) => { const r = byKey.get(key); return r ? (r.costPerTask ?? null) : null; },
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Every eligible agent applies: `fit` (needs → adjusted record → size) plus the engine it
|
|
257
|
-
* would run on. `summaries` are scorecard cards by agent id (`summarize()`); `rows` are
|
|
258
|
-
* `engineRows`. Best first, recruitable (an engine clears) before not. Each application is
|
|
259
|
-
* `{ agentId, engine?, fit, reasons, pitch, at, covers }` — job.js's shape, the record's, plus
|
|
260
|
-
* `covers` (has a skill the job names) for `decide`; the record drops it.
|
|
261
|
-
*/
|
|
262
|
-
export function applications(job, pool, { summaries = {}, rows = [], reach = 'any', chatModel = null, adjust = true, now = Date.now() } = {}) {
|
|
263
|
-
const need = needForJob(job, { reach });
|
|
264
|
-
const worth = engineWorth(rows);
|
|
265
|
-
const fitFn = (j, agent, summary) => {
|
|
266
|
-
const f = fit(j, { ...agent, tools: agent.tools || agent.grants }, summary, adjust ? worth : { adjust: false });
|
|
267
|
-
const route = routeFor(agent, j, { rows, summary, need, chatModel });
|
|
268
|
-
return { score: f.score, reasons: [...f.reasons, ...route.reasons].slice(0, 8), ...(route.clears ? { engine: route.engine } : {}), covers: coversSkills(j, agent) };
|
|
269
|
-
};
|
|
270
|
-
// `covers` — has at least one skill the job names (or the job names none) — rides on the
|
|
271
|
-
// live application for `decide`; the record keeps job.js's shape and drops it.
|
|
272
|
-
const covered = new Map((pool || []).map((a) => [a?.id, coversSkills(job, a)]));
|
|
273
|
-
return applyAll(job, pool, fitFn, { cards: summaries, now }).map((a) => ({ ...a, covers: covered.get(a.agentId) !== false }));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** A job that names skills is not given to an agent with none of them on fit alone: tools and grants it never asked for count for nothing. */
|
|
277
|
-
function coversSkills(job, agent) {
|
|
278
|
-
const want = lower(job?.needs?.skills);
|
|
279
|
-
if (!want.length) return true;
|
|
280
|
-
const has = new Set(lower(agent?.skills));
|
|
281
|
-
return want.some((s) => has.has(s));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ── Evaluating: one structured call, or none ──────────────────────────────────────────────
|
|
285
|
-
|
|
286
|
-
/** The evaluator's instruction: the job, the top applicants with their fit, engine and reasons, the shape to answer in. */
|
|
287
|
-
export function evaluatorPrompt(job, apps, pool = [], { rows = [], top = TOP_APPLICANTS } = {}) {
|
|
288
|
-
const byId = new Map((pool || []).map((a) => [a.id, a]));
|
|
289
|
-
const rowOf = new Map((rows || []).filter(Boolean).map((r) => [r.key, r]));
|
|
290
|
-
const needs = job?.needs || {};
|
|
291
|
-
const lines = (apps || []).slice(0, top).map((a) => {
|
|
292
|
-
const agent = byId.get(a.agentId) || {};
|
|
293
|
-
const key = a.engine ? engineKey(a.engine) : null;
|
|
294
|
-
const row = key ? rowOf.get(key) : null;
|
|
295
|
-
return `- ${a.agentId} (fit ${Math.round((a.fit || 0) * 100)}%)${agent.purpose ? ` — ${clip(agent.purpose, 160)}` : ''}; skills: ${(agent.skills || []).join(', ') || 'none'}; grants: ${(agent.grants || []).join(', ') || 'none'}; ${key ? `engine: ${row ? rowLine(row) : key}` : 'NOT RECRUITABLE NOW — no engine clears'}; ${(a.reasons || []).slice(0, 4).join('; ')}`;
|
|
296
|
-
});
|
|
297
|
-
return [
|
|
298
|
-
`You are the evaluator for the job "${clip(job?.title, 200)}" on project ${job?.projectId || '?'}. Recruit ONE applicant, or say none fits.`,
|
|
299
|
-
`Brief: ${clip(job?.brief, MAX_BRIEF_IN_PROMPT)}`,
|
|
300
|
-
`Needs — skills: ${(needs.skills || []).join(', ') || 'none named'}; tools: ${(needs.tools || []).join(', ') || 'none named'}; grants: ${(needs.grants || []).join(', ') || 'none'}${job?.budget ? `; budget: ${Object.entries(job.budget).map(([k, v]) => `${v} ${k}`).join(', ')}` : ''}.`,
|
|
301
|
-
'',
|
|
302
|
-
'Applicants, best computed fit first (fit = the skills, tools and grants the job names, then the attested record, then size):',
|
|
303
|
-
...(lines.length ? lines : ['- (no one applied)']),
|
|
304
|
-
'',
|
|
305
|
-
'Rules: prefer the applicant that has what the job names and a record of clearing work like it on the engine shown; never pick one marked NOT RECRUITABLE NOW; a lower fit is right only when its reasons show the higher one lacks something the brief needs. When no applicant has the skills the job names, answer pick "" and propose the agent that should exist.',
|
|
306
|
-
'',
|
|
307
|
-
describeSchema(RECRUIT_SCHEMA),
|
|
308
|
-
].join('\n');
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* The evaluator's answer, read through the schema and checked against the applications: a
|
|
313
|
-
* pick must be a recruitable applicant, else it is "none". Returns `{ pick, why, confidence,
|
|
314
|
-
* proposal }` or null when the text is unreadable.
|
|
315
|
-
*/
|
|
316
|
-
export function parseEvaluation(text, apps = []) {
|
|
317
|
-
const got = coerce(text, RECRUIT_SCHEMA);
|
|
318
|
-
if (!got) return null;
|
|
319
|
-
const v = got.value;
|
|
320
|
-
const pick = String(v.pick || '').trim();
|
|
321
|
-
const app = pick ? (apps || []).find((a) => a.agentId === pick) : null;
|
|
322
|
-
const proposal = v.proposalName ? { name: clip(v.proposalName, 60), purpose: clip(v.proposalPurpose, 300), skills: (v.proposalSkills || []).map((s) => clip(s, 80)).filter(Boolean), grants: (v.proposalGrants || []).map((g) => clip(g, 64)).filter(Boolean) } : null;
|
|
323
|
-
if (app && app.engine) return { pick, why: clip(v.why, 400) || 'the evaluator\'s pick', confidence: r3(v.confidence), proposal: null };
|
|
324
|
-
return { pick: '', why: app ? `the evaluator picked ${pick}, which no engine can run right now` : clip(v.why, 400) || 'none fits', confidence: r3(v.confidence), proposal };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* The decision: the evaluator's when one was made; else the best recruitable fit at or above
|
|
329
|
-
* `minFit`; else none, with the agent the job's needs describe as the proposal. Returns
|
|
330
|
-
* `{ kind: 'recruit', agentId, engine, fit, why, by }` or `{ kind: 'none', why, proposal, by }`.
|
|
331
|
-
*/
|
|
332
|
-
export function decide(job, apps, { evaluation = null, minFit = MIN_FIT } = {}) {
|
|
333
|
-
const list = apps || [];
|
|
334
|
-
if (evaluation && evaluation.pick) {
|
|
335
|
-
const app = list.find((a) => a.agentId === evaluation.pick && a.engine);
|
|
336
|
-
if (app) return { kind: 'recruit', agentId: app.agentId, engine: app.engine, fit: app.fit, why: evaluation.why, by: 'evaluator' };
|
|
337
|
-
}
|
|
338
|
-
if (evaluation && !evaluation.pick) return { kind: 'none', why: evaluation.why, proposal: evaluation.proposal || proposalFromNeeds(job), by: 'evaluator' };
|
|
339
|
-
const best = list.find((a) => a.engine && a.covers !== false);
|
|
340
|
-
if (best && best.fit >= minFit) return { kind: 'recruit', agentId: best.agentId, engine: best.engine, fit: best.fit, why: `best fit (${Math.round(best.fit * 100)}%): ${(best.reasons || [])[0] || 'meets the needs'}`, by: 'fit' };
|
|
341
|
-
const why = !list.length ? 'no one in the pool applies to jobs'
|
|
342
|
-
: !best ? (list.some((a) => a.engine) ? `no applicant has a skill the job names (${(job?.needs?.skills || []).join(', ')})` : `${list.length} applied but no engine clears for any of them: ${(list[0].reasons || []).find((r) => /engine|pinned|roster/.test(r)) || 'see the applications'}`)
|
|
343
|
-
: `the best fit is ${Math.round(best.fit * 100)}%, under the ${Math.round(minFit * 100)}% floor: ${(best.reasons || []).find((r) => /missing/.test(r)) || best.reasons?.[0] || ''}`;
|
|
344
|
-
return { kind: 'none', why, proposal: proposalFromNeeds(job), by: 'fit' };
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/** The agent a job's needs describe — what to propose when no one fits. */
|
|
348
|
-
export function proposalFromNeeds(job) {
|
|
349
|
-
const needs = job?.needs || {};
|
|
350
|
-
return { name: clip(job?.title, 60) || 'New agent', purpose: `Does jobs like "${clip(job?.title, 80)}".`, skills: [...(needs.skills || [])], grants: (needs.grants || []).length ? [...needs.grants] : ['none'] };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* A proposal as an agent card for a person to approve — validated by the pool's own form
|
|
355
|
-
* (agentFromForm), engine `auto`, `createdBy: 'evaluator'`, its origin the job. Nothing
|
|
356
|
-
* joins the pool without a decision (D-A2): this returns the card, it does not store it.
|
|
357
|
-
*/
|
|
358
|
-
export function proposalToAgent(proposal, job, { by = 'evaluator' } = {}) {
|
|
359
|
-
const p = proposal || proposalFromNeeds(job);
|
|
360
|
-
const known = new Set(['data', 'web', 'history', 'mcp', ...WORK_GRANTS]);
|
|
361
|
-
const grants = (p.grants || []).map((g) => String(g).toLowerCase()).filter((g) => known.has(g) || /^mcp:/.test(g));
|
|
362
|
-
return agentFromForm({
|
|
363
|
-
name: p.name, purpose: p.purpose, skills: p.skills,
|
|
364
|
-
prompt: `You are ${p.name}. ${p.purpose || ''}\n\nYou were proposed for the job "${clip(job?.title, 200)}" because no agent in the pool fit it. Do work like it well; ask on the thread when the brief is unclear.`,
|
|
365
|
-
grants: grants.length ? grants : ['none'], engine: { kind: 'auto', prefer: 'balanced' }, appliesTo: ['jobs'], createdBy: by,
|
|
366
|
-
origin: { kind: 'proposal', projectId: job?.projectId, jobId: job?.id },
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// ── Landing it on the record ──────────────────────────────────────────────────────────────
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* The budget a recruit gets: the job's own when it has one, else an equal share of what the
|
|
374
|
-
* project has left (its budget minus its spend) across the jobs still to be recruited — so
|
|
375
|
-
* one unbudgeted job cannot take the whole project. `record` is the project record
|
|
376
|
-
* (project.js `foldProject`).
|
|
377
|
-
*/
|
|
378
|
-
export function carveBudget(job, record = null) {
|
|
379
|
-
if (job?.budget && Object.keys(job.budget).length) return normalizeBudget(job.budget);
|
|
380
|
-
const cap = record?.page?.budget || {};
|
|
381
|
-
const spent = record?.spend || {};
|
|
382
|
-
const waiting = Math.max(1, (record?.jobs || []).filter((j) => ['open', 'evaluating'].includes(j.status)).length);
|
|
383
|
-
const out = {};
|
|
384
|
-
for (const k of ['tokens', 'calls', 'ms', 'usd']) {
|
|
385
|
-
if (!(Number(cap[k]) > 0)) continue;
|
|
386
|
-
const left = Math.max(0, Number(cap[k]) - (Number(spent[k]) || 0));
|
|
387
|
-
if (left > 0) out[k] = k === 'usd' ? Math.round((left / waiting) * 100) / 100 : Math.max(1, Math.floor(left / waiting));
|
|
388
|
-
}
|
|
389
|
-
return Object.keys(out).length ? out : null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* The events that record a recruiting pass on the project (project.js fold): the job moves
|
|
394
|
-
* to `evaluating` with its applications, then to `recruited` with the pair, the budget and
|
|
395
|
-
* the why — or back to `open`, with the proposal as a decision a person reads.
|
|
396
|
-
*/
|
|
397
|
-
export function recruitEvents(job, apps, decision, { by = 'evaluator', at = Date.now(), record = null } = {}) {
|
|
398
|
-
const events = [{ type: 'job.updated', at, job: { id: job.id, status: 'evaluating', applications: (apps || []).map(({ covers: _c, ...a }) => a) }, by }];
|
|
399
|
-
if (decision?.kind === 'recruit') {
|
|
400
|
-
const budget = carveBudget(job, record);
|
|
401
|
-
events.push({ type: 'job.updated', at, job: { id: job.id, status: 'recruited', recruited: { agentId: decision.agentId, engine: decision.engine, ...(budget ? { budget } : {}), by: decision.by || by, at, why: clip(decision.why, 600) } }, by });
|
|
402
|
-
} else {
|
|
403
|
-
events.push({ type: 'job.updated', at, job: { id: job.id, status: 'open' }, by });
|
|
404
|
-
const p = decision?.proposal;
|
|
405
|
-
events.push({ type: 'project.decision', at, by, kind: 'proposal', text: `No one in the pool fits "${clip(job.title, 120)}": ${clip(decision?.why, 400)}${p ? ` Proposed: ${p.name}${p.skills?.length ? ` — skills ${p.skills.join(', ')}` : ''}${p.grants?.length ? `; grants ${p.grants.join(', ')}` : ''}.` : ''}`, refs: [`job:${job.id}`] });
|
|
406
|
-
}
|
|
407
|
-
return events;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* One pass, end to end: apply → (evaluate) → decide → the events. `ask(prompt) → text |
|
|
412
|
-
* null` is the host's structured call; absent or failing, the fit decides. Returns
|
|
413
|
-
* `{ applications, evaluation, decision, events, prompt }`.
|
|
414
|
-
*/
|
|
415
|
-
export async function recruitJob(job, pool, { summaries = {}, rows = [], reach = 'any', chatModel = null, record = null, ask = null, minFit = MIN_FIT, by = 'evaluator', now = Date.now() } = {}) {
|
|
416
|
-
const apps = applications(job, pool, { summaries, rows, reach, chatModel, now });
|
|
417
|
-
const prompt = evaluatorPrompt(job, apps, pool, { rows });
|
|
418
|
-
let evaluation = null;
|
|
419
|
-
if (ask && apps.some((a) => a.engine)) {
|
|
420
|
-
try { const text = await ask(prompt, RECRUIT_SCHEMA); evaluation = text == null ? null : (typeof text === 'string' ? parseEvaluation(text, apps) : parseEvaluation(JSON.stringify(text), apps)); } catch { evaluation = null; }
|
|
421
|
-
}
|
|
422
|
-
const decision = decide(job, apps, { evaluation, minFit });
|
|
423
|
-
return { applications: apps, evaluation, decision, events: recruitEvents(job, apps, decision, { by: decision.by === 'evaluator' ? by : 'fit', at: now, record }), prompt };
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ── A run's job board (§15.2): the host's `recruit` hook, shared ─────────────────────────
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* What a host hands the runner as `recruit`: one pass over the pool for a sub-task's job,
|
|
430
|
-
* the pick made into a ROLE the run can appoint (job.js `jobToRole`, resolved through the
|
|
431
|
-
* pool so it carries the agent's prompt, grants, skills and a model target — agent.js
|
|
432
|
-
* `resolveTeam`). Returns `{ role, agentId, engine, why, fit, applications, events }` or,
|
|
433
|
-
* when no one is recruited, `{ why, proposal, applications, events }` with `proposal` the
|
|
434
|
-
* agent card a person may approve (the runner posts it). `create` is that card once
|
|
435
|
-
* approved — the host has persisted it to its pool before calling — and it applies with
|
|
436
|
-
* the rest. `events` are the project-record events of the pass (recruit.js `recruitEvents`)
|
|
437
|
-
* for a host that has a project to land them on; the run's own record has its own.
|
|
438
|
-
*/
|
|
439
|
-
export async function recruitForRun(job, pool, { rows = [], summaries = {}, reach = 'any', chatModel = null, targetFor = null, ask = null, minFit = MIN_FIT, create = null, now = Date.now() } = {}) {
|
|
440
|
-
const list = (Array.isArray(pool) ? pool : []).filter((a) => a && a.id);
|
|
441
|
-
if (create && isRecord(create)) {
|
|
442
|
-
try { const made = normalizeAgent(create); if (!list.some((a) => a.id === made.id)) list.push(made); } catch { /* an unusable card applies as nothing */ }
|
|
443
|
-
}
|
|
444
|
-
const pass = await recruitJob(job, list, { summaries, rows, reach, chatModel, ask, minFit, now });
|
|
445
|
-
const d = pass.decision;
|
|
446
|
-
if (d.kind !== 'recruit') {
|
|
447
|
-
const card = proposalToAgent(d.proposal || proposalFromNeeds(job), job, { by: d.by || 'evaluator' });
|
|
448
|
-
return { why: d.why, proposal: card?.ok === false ? null : (card?.agent || card), applications: pass.applications, events: pass.events };
|
|
449
|
-
}
|
|
450
|
-
const agent = list.find((a) => a.id === d.agentId);
|
|
451
|
-
const base = jobToRole(job, agent);
|
|
452
|
-
let role = { ...base, agent: agent.id, engine: d.engine };
|
|
453
|
-
try {
|
|
454
|
-
role = resolveTeam({ name: 'recruit', roles: [role], budget: { tokens: 1 } }, list, { chatModel, targetFor }).roles[0];
|
|
455
|
-
} catch { /* unresolved: the runner's appointer decides from the engine */ }
|
|
456
|
-
return { role, agentId: d.agentId, engine: d.engine, why: d.why, fit: d.fit ?? null, applications: pass.applications, events: pass.events };
|
|
457
|
-
}
|
|
4
|
+
You were proposed for the job "${k(l?.title,200)}" because no agent in the pool fit it. Do work like it well; ask on the thread when the brief is unclear.`,grants:a.length?a:["none"],engine:{kind:"auto",prefer:"balanced"},appliesTo:["jobs"],createdBy:t,origin:{kind:"proposal",projectId:l?.projectId,jobId:l?.id}})}function fe(e,l=null){if(e?.budget&&Object.keys(e.budget).length)return ie(e.budget);const t=l?.page?.budget||{},s=l?.spend||{},o=Math.max(1,(l?.jobs||[]).filter(r=>["open","evaluating"].includes(r.status)).length),a={};for(const r of["tokens","calls","ms","usd"]){if(!(Number(t[r])>0))continue;const i=Math.max(0,Number(t[r])-(Number(s[r])||0));i>0&&(a[r]=r==="usd"?Math.round(i/o*100)/100:Math.max(1,Math.floor(i/o)))}return Object.keys(a).length?a:null}function ye(e,l,t,{by:s="evaluator",at:o=Date.now(),record:a=null}={}){const r=[{type:"job.updated",at:o,job:{id:e.id,status:"evaluating",applications:(l||[]).map(({covers:i,...c})=>c)},by:s}];if(t?.kind==="recruit"){const i=fe(e,a);r.push({type:"job.updated",at:o,job:{id:e.id,status:"recruited",recruited:{agentId:t.agentId,engine:t.engine,...i?{budget:i}:{},by:t.by||s,at:o,why:k(t.why,600)}},by:s})}else{r.push({type:"job.updated",at:o,job:{id:e.id,status:"open"},by:s});const i=t?.proposal;r.push({type:"project.decision",at:o,by:s,kind:"proposal",text:`No one in the pool fits "${k(e.title,120)}": ${k(t?.why,400)}${i?` Proposed: ${i.name}${i.skills?.length?` — skills ${i.skills.join(", ")}`:""}${i.grants?.length?`; grants ${i.grants.join(", ")}`:""}.`:""}`,refs:[`job:${e.id}`]})}return r}async function me(e,l,{summaries:t={},rows:s=[],reach:o="any",chatModel:a=null,record:r=null,ask:i=null,minFit:c=q,by:n="evaluator",now:h=Date.now()}={}){const p=pe(e,l,{summaries:t,rows:s,reach:o,chatModel:a,now:h}),u=de(e,p,l,{rows:s});let g=null;if(i&&p.some(f=>f.engine))try{const f=await i(u,S);g=f==null?null:W(typeof f=="string"?f:JSON.stringify(f),p)}catch{g=null}const d=he(e,p,{evaluation:g,minFit:c});return{applications:p,evaluation:g,decision:d,events:ye(e,p,d,{by:d.by==="evaluator"?n:"fit",at:h,record:r}),prompt:u}}async function Te(e,l,{rows:t=[],summaries:s={},reach:o="any",chatModel:a=null,targetFor:r=null,ask:i=null,minFit:c=q,create:n=null,now:h=Date.now()}={}){const p=(Array.isArray(l)?l:[]).filter(b=>b&&b.id);if(n&&B(n))try{const b=Q(n);p.some(w=>w.id===b.id)||p.push(b)}catch{}const u=await me(e,p,{summaries:s,rows:t,reach:o,chatModel:a,ask:i,minFit:c,now:h}),g=u.decision;if(g.kind!=="recruit"){const b=ge(g.proposal||P(e),e,{by:g.by||"evaluator"});return{why:g.why,proposal:b?.ok===!1?null:b?.agent||b,applications:u.applications,events:u.events}}const d=p.find(b=>b.id===g.agentId);let y={...ee(e,d),agent:d.id,engine:g.engine};try{y=V({name:"recruit",roles:[y],budget:{tokens:1}},p,{chatModel:a,targetFor:r}).roles[0]}catch{}return{role:y,agentId:g.agentId,engine:g.engine,why:g.why,fit:g.fit??null,applications:u.applications,events:u.events}}export{ae as MAX_BRIEF_IN_PROMPT,q as MIN_FIT,S as RECRUIT_SCHEMA,oe as TOP_APPLICANTS,pe as applications,fe as carveBudget,he as decide,_ as engineRow,Ne as engineRows,ue as engineWorth,de as evaluatorPrompt,L as needForJob,W as parseEvaluation,P as proposalFromNeeds,ge as proposalToAgent,ye as recruitEvents,Te as recruitForRun,me as recruitJob,ce as routeFor};
|
package/redaction-tokens.js
CHANGED
|
@@ -1,61 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// It is its own file for the reason `distance.js` is: three unrelated places need this
|
|
4
|
-
// question answered — the subject resolver, the wikilink parser, and the extension's note
|
|
5
|
-
// index — and the first two live in `entity.js`, which has since grown entity resolution,
|
|
6
|
-
// merge suggestions and a Levenshtein dependency. Importing 25 KB of that into a notes page
|
|
7
|
-
// to ask "is this string a placeholder" is the same 120 KB mistake in a smaller coat.
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A REDACTION PLACEHOLDER IS NOT A SUBJECT — and this is the sharpest edge in the module.
|
|
11
|
-
*
|
|
12
|
-
* `@chatpanel/pii` writes `[[PERSON_1]]`, `[[EMAIL_2]]`, `[[LOCATION_1]]`, which is
|
|
13
|
-
* character-for-character the `[[wikilink]]` grammar. So a redacted transcript reads as a
|
|
14
|
-
* document full of links to pages that do not exist, and every one of them earned a "wanted
|
|
15
|
-
* page" brief. The name of a person we deliberately did not learn was being filed as a thing
|
|
16
|
-
* we know about.
|
|
17
|
-
*
|
|
18
|
-
* The tempting fix is to treat the token as a pseudonymous identity — it IS stable, and
|
|
19
|
-
* within one conversation `PERSON_1` really does mean one person. It must not become a
|
|
20
|
-
* subject anyway, because **the vault is scoped to a conversation**: `PERSON_1` in Monday's
|
|
21
|
-
* chat and `PERSON_1` in Friday's are different people, and a global subject would merge
|
|
22
|
-
* strangers under one page and attribute one person's decisions to another. That is the same
|
|
23
|
-
* reason `aliasMap` refuses a bare token two people could claim — here it is guaranteed
|
|
24
|
-
* rather than possible.
|
|
25
|
-
*
|
|
26
|
-
* So the placeholder is dropped from subject candidacy, and `curate.js` counts what was
|
|
27
|
-
* dropped so the loss is REPORTED rather than silent. Never resolved against a vault into
|
|
28
|
-
* anything derived and persisted, either: that would put the PII back on disk in a second
|
|
29
|
-
* place, which is the one thing redaction exists to prevent.
|
|
30
|
-
*
|
|
31
|
-
* The pattern is duplicated from `@chatpanel/pii` rather than imported — this package ships
|
|
32
|
-
* zero dependencies so the bridge can vendor it file by file, the same constraint that makes
|
|
33
|
-
* the entitlement JWK live in two clients. `CLAUDE.md` lists `[[TYPE_n]]` as a wire contract
|
|
34
|
-
* that only changes additively, and the extension (which vendors both) carries the drift
|
|
35
|
-
* guard that fails when these two stop agreeing.
|
|
36
|
-
*/
|
|
37
|
-
export const REDACTION_TOKEN_TYPES = Object.freeze([
|
|
38
|
-
'PERSON', 'ORG', 'LOCATION', 'ADDRESS', 'EMAIL', 'PHONE', 'ID', 'SSN', 'IBAN',
|
|
39
|
-
'CREDITCARD', 'CARD', 'POST', 'FAC', 'GROUP', 'NRP', 'ENTITY', 'KEY', 'SECRET',
|
|
40
|
-
'TERM', 'PII', 'OTHER',
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
const BARE_TOKEN_RE = /^([A-Z][A-Z0-9]*)_\d+$/;
|
|
44
|
-
const unwrap = (v) => String(v ?? '').trim().replace(/^\[{1,2}|\]{1,2}$/g, '');
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Is this BARE string one of the redaction placeholders?
|
|
48
|
-
*
|
|
49
|
-
* Matched against the type vocabulary rather than the `[A-Z]+_\d+` shape, and everywhere —
|
|
50
|
-
* inside brackets too. The shape alone eats real subjects: `[[Q3_2026]]` and `[[PHASE_2]]`
|
|
51
|
-
* are links people genuinely write, and silently dropping them would trade one invisible bug
|
|
52
|
-
* for another. A custom dictionary type is the accepted gap: it is user-chosen, so filing it
|
|
53
|
-
* is a name the user picked, not a stranger's identity.
|
|
54
|
-
*
|
|
55
|
-
* Bracket-tolerant, because a wikilink parser has already stripped them by the time we ask,
|
|
56
|
-
* and a model echoing a placeholder into JSON routinely mangles them.
|
|
57
|
-
*/
|
|
58
|
-
export function isRedactionToken(value) {
|
|
59
|
-
const m = BARE_TOKEN_RE.exec(unwrap(value));
|
|
60
|
-
return !!m && REDACTION_TOKEN_TYPES.includes(m[1]);
|
|
61
|
-
}
|
|
1
|
+
const e=Object.freeze(["PERSON","ORG","LOCATION","ADDRESS","EMAIL","PHONE","ID","SSN","IBAN","CREDITCARD","CARD","POST","FAC","GROUP","NRP","ENTITY","KEY","SECRET","TERM","PII","OTHER"]),T=/^([A-Z][A-Z0-9]*)_\d+$/,n=E=>String(E??"").trim().replace(/^\[{1,2}|\]{1,2}$/g,"");function t(E){const R=T.exec(n(E));return!!R&&e.includes(R[1])}export{e as REDACTION_TOKEN_TYPES,t as isRedactionToken};
|
package/ref.js
CHANGED
|
@@ -1,55 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// An event records "note n_88, lines 10-40, hash H" and never the text. Content that
|
|
4
|
-
// actually entered a model request is written once into a content-addressed blob store
|
|
5
|
-
// keyed by hash, so the same excerpt across a hundred turns costs one copy.
|
|
6
|
-
//
|
|
7
|
-
// Replay resolves a Ref by hash: match => exact reconstruction; blob absent or
|
|
8
|
-
// crypto-shredded => VERIFIED_BUT_UNAVAILABLE. It never silently substitutes today's
|
|
9
|
-
// version of the note, because that would make replay quietly wrong instead of loudly
|
|
10
|
-
// incomplete.
|
|
11
|
-
|
|
12
|
-
// `memory` and `brief` joined when the derived layer landed: a brief's claim cites the
|
|
13
|
-
// record it came from, and a claim the USER stated cites the memory they stated it in.
|
|
14
|
-
// Additive — no existing ref changes meaning.
|
|
15
|
-
export const REF_KINDS = Object.freeze(['note', 'meeting', 'chat', 'page', 'result', 'blob', 'memory', 'brief']);
|
|
16
|
-
|
|
17
|
-
export const RESOLUTION = Object.freeze({
|
|
18
|
-
EXACT: 'exact', // blob present, hash matches
|
|
19
|
-
UNAVAILABLE: 'verified-but-unavailable', // we know what it was; we no longer hold it
|
|
20
|
-
DRIFTED: 'drifted', // source still exists but its hash changed
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/** Build a Ref. `hash` is the content hash AT CAPTURE TIME — that is the whole point. */
|
|
24
|
-
export function makeRef({ kind, id, hash, range = null, stored = false }) {
|
|
25
|
-
if (!REF_KINDS.includes(kind)) throw new TypeError(`ref: unknown kind ${kind}`);
|
|
26
|
-
if (typeof id !== 'string' || !id) throw new TypeError('ref: id required');
|
|
27
|
-
if (typeof hash !== 'string' || !hash) throw new TypeError('ref: hash required');
|
|
28
|
-
const ref = { kind, id, hash };
|
|
29
|
-
if (range) {
|
|
30
|
-
if (!Number.isInteger(range.from) || !Number.isInteger(range.to) || range.to < range.from) {
|
|
31
|
-
throw new TypeError('ref: range must be {from,to} integers with to >= from');
|
|
32
|
-
}
|
|
33
|
-
ref.range = { from: range.from, to: range.to };
|
|
34
|
-
}
|
|
35
|
-
if (stored) ref.stored = true;
|
|
36
|
-
return Object.freeze(ref);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function isRef(v) {
|
|
40
|
-
return !!v && typeof v === 'object'
|
|
41
|
-
&& REF_KINDS.includes(v.kind)
|
|
42
|
-
&& typeof v.id === 'string' && v.id.length > 0
|
|
43
|
-
&& typeof v.hash === 'string' && v.hash.length > 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Classify what a replay can say about a Ref, given what the blob store holds now.
|
|
48
|
-
* `lookup(ref) -> { hash } | null`. Pure: the caller owns the store.
|
|
49
|
-
*/
|
|
50
|
-
export function resolveRef(ref, lookup) {
|
|
51
|
-
const got = lookup(ref);
|
|
52
|
-
if (!got) return { resolution: RESOLUTION.UNAVAILABLE, ref };
|
|
53
|
-
if (got.hash !== ref.hash) return { resolution: RESOLUTION.DRIFTED, ref, actualHash: got.hash };
|
|
54
|
-
return { resolution: RESOLUTION.EXACT, ref, value: got.value };
|
|
55
|
-
}
|
|
1
|
+
const f=Object.freeze(["note","meeting","chat","page","result","blob","memory","brief"]),i=Object.freeze({EXACT:"exact",UNAVAILABLE:"verified-but-unavailable",DRIFTED:"drifted"});function u({kind:e,id:o,hash:r,range:t=null,stored:s=!1}){if(!f.includes(e))throw new TypeError(`ref: unknown kind ${e}`);if(typeof o!="string"||!o)throw new TypeError("ref: id required");if(typeof r!="string"||!r)throw new TypeError("ref: hash required");const n={kind:e,id:o,hash:r};if(t){if(!Number.isInteger(t.from)||!Number.isInteger(t.to)||t.to<t.from)throw new TypeError("ref: range must be {from,to} integers with to >= from");n.range={from:t.from,to:t.to}}return s&&(n.stored=!0),Object.freeze(n)}function l(e){return!!e&&typeof e=="object"&&f.includes(e.kind)&&typeof e.id=="string"&&e.id.length>0&&typeof e.hash=="string"&&e.hash.length>0}function h(e,o){const r=o(e);return r?r.hash!==e.hash?{resolution:i.DRIFTED,ref:e,actualHash:r.hash}:{resolution:i.EXACT,ref:e,value:r.value}:{resolution:i.UNAVAILABLE,ref:e}}export{f as REF_KINDS,i as RESOLUTION,l as isRef,u as makeRef,h as resolveRef};
|