@lensmcp/protocol-types 1.25.32 → 1.25.34
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/index.d.ts +9 -0
- package/index.js +1 -1
- package/lib/ai-call-groups.d.ts +56 -0
- package/lib/ai-call-groups.js +2 -0
- package/lib/ai-call-row.d.ts +252 -0
- package/lib/ai-call-row.js +3 -0
- package/lib/ai-call.d.ts +146 -0
- package/lib/ai-call.js +19 -0
- package/lib/ai-context.d.ts +113 -0
- package/lib/ai-context.js +6 -0
- package/lib/ai-origins.d.ts +72 -0
- package/lib/ai-origins.js +10 -0
- package/lib/ai-playground.d.ts +76 -0
- package/lib/ai-playground.js +1 -0
- package/lib/dashboard.d.ts +10 -3
- package/lib/event-loop.d.ts +94 -0
- package/lib/event-loop.js +1 -1
- package/lib/team-design.d.ts +228 -0
- package/lib/team-design.js +1 -0
- package/lib/team-library.d.ts +283 -0
- package/lib/team-library.js +1 -0
- package/lib/team.d.ts +209 -0
- package/lib/team.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { AiCall, AiRole } from './ai-call.js';
|
|
2
|
+
/**
|
|
3
|
+
* What an AI call's tokens went to (the AI call's Context tab, and each kept call's row): everything the model was
|
|
4
|
+
* given, part by part in the order it got them — the system prompt, the tool definitions, the answer's schema, the
|
|
5
|
+
* context the app put in, the earlier turns, the tool calls and their results, this turn — each with its tokens, the
|
|
6
|
+
* sections it is made of, and how far the provider's prompt cache reached into it; then what the answer's tokens went
|
|
7
|
+
* to, and what stands out (`findings`).
|
|
8
|
+
*
|
|
9
|
+
* A provider counts a call's tokens, not its parts: each part is estimated from its text ({@link estimateTokens}), and
|
|
10
|
+
* the estimates are scaled so they add up to the provider's count. The estimate was fitted on FoodGuard's kept calls
|
|
11
|
+
* (OpenAI, English prompts with Hebrew conversations): its total is within 0.5 % of the provider's on the median call.
|
|
12
|
+
*
|
|
13
|
+
* Pure: run on the call the panel already read.
|
|
14
|
+
*/
|
|
15
|
+
export type AiPartKind = 'system' | 'tools' | 'schema' | 'context' | 'history' | 'tool' | 'turn' | 'carried' | 'unkept';
|
|
16
|
+
/** The bar's order — what a prompt is built from first. */
|
|
17
|
+
export declare const AI_PART_KINDS: readonly AiPartKind[];
|
|
18
|
+
export declare const AI_PART_LABEL: Record<AiPartKind, string>;
|
|
19
|
+
export declare const AI_PART_SAID: Record<AiPartKind, string>;
|
|
20
|
+
export interface AiSection {
|
|
21
|
+
title: string;
|
|
22
|
+
/** A tagged section's tag (`memory`, `rules`…) and attributes (`id`, `note`, `source`, `version`…). */
|
|
23
|
+
tag?: string;
|
|
24
|
+
attrs?: Record<string, string>;
|
|
25
|
+
chars: number;
|
|
26
|
+
/** Estimated, scaled to its part's tokens. */
|
|
27
|
+
tokens: number;
|
|
28
|
+
/** Its span in its part's text — none for a JSON key. */
|
|
29
|
+
at?: number;
|
|
30
|
+
end?: number;
|
|
31
|
+
children?: AiSection[];
|
|
32
|
+
}
|
|
33
|
+
export type AiPartRef = {
|
|
34
|
+
system: number;
|
|
35
|
+
} | {
|
|
36
|
+
turn: number;
|
|
37
|
+
} | {
|
|
38
|
+
tools: true;
|
|
39
|
+
} | {
|
|
40
|
+
schema: true;
|
|
41
|
+
} | {
|
|
42
|
+
carried: true;
|
|
43
|
+
} | {
|
|
44
|
+
unkept: true;
|
|
45
|
+
};
|
|
46
|
+
export interface AiPart {
|
|
47
|
+
/** Its place in what the model got, from 0 — the order the prompt cache reads in. */
|
|
48
|
+
i: number;
|
|
49
|
+
kind: AiPartKind;
|
|
50
|
+
/** «System prompt · instructions», «user», «assistant», «tool · fill_question», «Context · developer message»… */
|
|
51
|
+
label: string;
|
|
52
|
+
role?: AiRole | 'tool';
|
|
53
|
+
ref: AiPartRef;
|
|
54
|
+
chars: number;
|
|
55
|
+
tokens: number;
|
|
56
|
+
/** Of its tokens, those inside the cached prefix. */
|
|
57
|
+
cached: number;
|
|
58
|
+
sections: AiSection[];
|
|
59
|
+
}
|
|
60
|
+
export interface AiFinding {
|
|
61
|
+
tone: 'bad' | 'warn' | 'info';
|
|
62
|
+
text: string;
|
|
63
|
+
}
|
|
64
|
+
export interface AiContext {
|
|
65
|
+
parts: AiPart[];
|
|
66
|
+
byKind: Array<{
|
|
67
|
+
kind: AiPartKind;
|
|
68
|
+
tokens: number;
|
|
69
|
+
cached: number;
|
|
70
|
+
parts: number;
|
|
71
|
+
}>;
|
|
72
|
+
/** The input tokens: the provider's count (`counted`), else the estimate. */
|
|
73
|
+
input: number;
|
|
74
|
+
counted: boolean;
|
|
75
|
+
cached: number;
|
|
76
|
+
written?: number;
|
|
77
|
+
/** The estimate of what is in the request, before it was scaled to the provider's count. */
|
|
78
|
+
estimated: number;
|
|
79
|
+
output: {
|
|
80
|
+
total: number;
|
|
81
|
+
reasoning: number;
|
|
82
|
+
answer: number;
|
|
83
|
+
tools: number;
|
|
84
|
+
counted: boolean;
|
|
85
|
+
};
|
|
86
|
+
findings: AiFinding[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* About how many tokens a text is, for an o200k-style tokenizer: a word 1 (a long one 1 more per 4 letters), a run of
|
|
90
|
+
* digits 1 per 3, a letter of a non-Latin script 0.66, punctuation 0.75 a character, a line break or an indent 1.
|
|
91
|
+
*/
|
|
92
|
+
export declare function estimateTokens(text: string): number;
|
|
93
|
+
/**
|
|
94
|
+
* What a text is made of: its `--- TITLE ---` blocks, else its headings (the shallowest level that repeats), else its
|
|
95
|
+
* `<tag>` blocks, else a JSON object's keys — one level, and a block that is JSON split by its keys in turn.
|
|
96
|
+
*/
|
|
97
|
+
export declare function sectionsOf(text: string): AiSection[];
|
|
98
|
+
/** What a prompt's tags say: the kinds of section an app's prompt is made of, by how often they change. */
|
|
99
|
+
export declare const STABLE_TAGS: ReadonlySet<string>;
|
|
100
|
+
export declare const CHANGING_TAGS: ReadonlySet<string>;
|
|
101
|
+
/**
|
|
102
|
+
* A prompt built of tagged sections — `<memory id="case" note="…" source="db:case_memory">…</memory>` — split by
|
|
103
|
+
* them, each with its tag and attributes, a list's items as its children (one level), and the untagged text between
|
|
104
|
+
* them kept as its own sections (its tokens are the prompt's too). A tag counts at the start of a line, closed by
|
|
105
|
+
* its own closing tag; anything else is text.
|
|
106
|
+
*/
|
|
107
|
+
export declare function taggedSections(text: string, from?: number, to?: number, depth?: number): AiSection[];
|
|
108
|
+
/** What the call's tokens went to — see the file's comment. `sections: false`: the parts only (a row's split). */
|
|
109
|
+
export declare function aiContextOf(call: AiCall, opts?: {
|
|
110
|
+
sections?: boolean;
|
|
111
|
+
}): AiContext;
|
|
112
|
+
/** A row's share of each kind — what a kept call keeps of its split, to add up over an agent, a run, a session. */
|
|
113
|
+
export declare function aiSplitOf(ctx: AiContext): Partial<Record<AiPartKind, number>>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";var P=Object.defineProperty;var $=(t,e)=>P(t,"name",{value:e,configurable:!0});var G=Object.defineProperty,g=$((t,e)=>G(t,"name",{value:e,configurable:!0}),"g");export const AI_PART_KINDS=["system","tools","schema","context","history","tool","turn","carried","unkept"],AI_PART_LABEL={system:"System prompt",tools:"Tool definitions",schema:"Answer schema",context:"Context",history:"Earlier turns",tool:"Tool calls & results",turn:"This turn",carried:"Held by the provider",unkept:"Not kept"},AI_PART_SAID={system:"The instructions every call of this agent starts with",tools:"The tools it was offered, as defined \u2014 sent with every call, called or not",schema:"The JSON schema its answer had to fit",context:"What the app put in beside the conversation (developer messages)",history:"The conversation before this turn \u2014 what was said, and answered",tool:"The tools it called earlier in this run, what it sent them and what came back",turn:"What this call asks \u2014 the latest message",carried:"The conversation of the response it continues (previous_response_id): counted by the provider, not in this request",unkept:"The end of the request, which the capture did not keep (a lens before 1.25.32 kept its first 64 KB): counted by the provider, not known here"};const R=/[\p{L}\p{M}]/u,W=/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u,D=/(\s*)([A-Za-z]+|[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+|[\p{L}\p{M}]+|[0-9]+|[^\s\p{L}\p{M}0-9]+)|(\s+)/gu,B=4,I=.7;export function estimateTokens(t){let e=0;for(const o of t.matchAll(D)){const s=o[1],n=o[2];if(o[3]!==void 0){/\n| {2,}/.test(o[3])&&(e+=1);continue}if(s&&/\n| {2,}/.test(s)&&(e+=1),!n)continue;const r=n.charCodeAt(0);r>=65&&r<=90||r>=97&&r<=122?e+=1+(n.length>8?(n.length-8)/4:0):r>=48&&r<=57?e+=Math.ceil(n.length/3):W.test(n)?e+=[...n].length*.9:R.test(n)?e+=[...n].length*.66:e+=n.length*.75}return e}$(estimateTokens,"estimateTokens"),g(estimateTokens,"estimateTokens");const Z=/^[ \t]*-{3,}[ \t]*(.+?)[ \t]*-{3,}[ \t]*$/gm,U=/^(#{1,4})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm,_=60;function C(t,e){const o=[],s=t.slice(0,e[0].at);return s.trim().length>40&&o.push({title:"(opening)",chars:s.length,tokens:0,at:0,end:e[0].at}),e.forEach((n,r)=>{const h=r+1<e.length?e[r+1].at:t.length;o.push({title:n.title.slice(0,120),chars:h-n.at,tokens:0,at:n.at,end:h})}),o}$(C,"P"),g(C,"cutAt");function M(t){const e=t.trim();if(!e.startsWith("{")&&!e.startsWith("["))return[];let o;try{o=JSON.parse(e)}catch{return[]}const s=o&&typeof o=="object"&&!Array.isArray(o)?o.properties:void 0,n=s&&typeof s=="object"&&!Array.isArray(s)?s:o,r=Array.isArray(n)?n.map((h,u)=>[`[${u}]`,h]):n&&typeof n=="object"?Object.entries(n):[];return r.length<2?[]:r.slice(0,_).map(([h,u])=>{const c=JSON.stringify(u,null,2)??"";return{title:h,chars:c.length,tokens:estimateTokens(c)}})}$(M,"M"),g(M,"jsonSections");export function sectionsOf(t){const e=taggedSections(t);return e.length?e:j(t)}$(sectionsOf,"sectionsOf"),g(sectionsOf,"sectionsOf");function j(t){if(t.length<200)return[];let e=g((s,n,r=()=>!0)=>[...t.matchAll(s)].filter(r).map(h=>({at:h.index,title:n(h)})),"pick")(Z,s=>s[1]);if(e.length<2){const s=[...t.matchAll(U)],n=[1,2,3,4].find(r=>s.filter(h=>h[1].length===r).length>=2);e=n?s.filter(r=>r[1].length===n).map(r=>({at:r.index,title:r[2]})):[]}if(e.length<2)return M(t);const o=C(t,e.slice(0,_));for(const s of o){const n=t.slice(s.at,s.end);s.tokens=estimateTokens(n);const r=n.indexOf(`
|
|
2
|
+
`),h=r>0?M(n.slice(r+1)):[];h.length&&(s.children=h)}return o}$(j,"N"),g(j,"plainSections");export const STABLE_TAGS=new Set(["instructions","rules","output","examples"]),CHANGING_TAGS=new Set(["memory","status","data","task"]);const F=/<([a-z][a-z0-9_-]{0,40})((?:\s+[A-Za-z_][\w.:-]*\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*>/g,Q=/([A-Za-z_][\w.:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g,X=g(t=>t.replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&"),"unescape");function H(t){const e={};for(const o of t.matchAll(Q))e[o[1]]=X(o[2]??o[3]??"");return e}$(H,"q"),g(H,"attrsOf");function L(t,e,o,s){const n=`<${e}`,r=`</${e}>`;let h=1,u=o;for(;u<s;){const c=t.indexOf(r,u);if(c<0||c>=s)return-1;let f=t.indexOf(n,u);for(;f>=0&&f<c&&!/[\s>]/.test(t[f+n.length]??"");)f=t.indexOf(n,f+1);if(f>=0&&f<c){h++,u=f+n.length;continue}if(--h===0)return c;u=c+r.length}return-1}$(L,"z"),g(L,"closeOf");const V=g((t,e)=>{for(let o=e-1;o>=0;o--){const s=t[o];if(s===`
|
|
3
|
+
`)return!0;if(s!==" "&&s!==" ")return!1}return!0},"atLineStart");export function taggedSections(t,e=0,o=t.length,s=0){const n=[],r=new RegExp(F.source,"g");let h=e;for(;h<o&&n.length<_;){r.lastIndex=h;const l=r.exec(t);if(!l||l.index>=o)break;const p=l[1],k=l.index+l[0].length,x=V(t,l.index)?L(t,p,k,o):-1;if(x<0){h=k;continue}const i=x+p.length+3,d=H(l[2]??""),b={title:p,tag:p,...Object.keys(d).length?{attrs:d}:{},chars:i-l.index,tokens:estimateTokens(t.slice(l.index,i)),at:l.index,end:i};if(s<1){const w=taggedSections(t,k,x,s+1).filter(a=>a.tag),v=w.length?w:j(t.slice(k,x)).map(a=>a.at===void 0?a:{...a,at:a.at+k,end:a.end+k});v.length&&(b.children=v)}n.push(b),h=i}if(!n.length)return[];const u=[];let c=e;const f=g((l,p)=>{const k=t.slice(l,p);k.trim().length>20&&u.push({title:"(untagged)",chars:p-l,tokens:estimateTokens(k),at:l,end:p})},"gap");for(const l of n)f(c,l.at),u.push(l),c=l.end;f(c,o);const y=new Map;for(const l of u){if(!l.tag)continue;const p=l.attrs?.id?`${l.tag} \xB7 ${l.attrs.id}`:l.tag,k=(y.get(p)??0)+1;y.set(p,k),l.title=k>1?`${p} #${k}`:p}return u}$(taggedSections,"taggedSections"),g(taggedSections,"taggedSections");function N(t,e){const o=t.reduce((s,n)=>s+n.tokens,0);for(const s of t)s.tokens=o>0?Math.round(s.tokens/o*e):0,s.children&&N(s.children,s.tokens)}$(N,"I"),g(N,"scaleSections");const Y={user:"user",assistant:"assistant",developer:"Context \xB7 developer message",system:"system message"};function E(t,e,o){if(t.kind==="reasoning")return t.text?{kind:"history",label:"reasoning \xB7 sent back",role:"assistant",ref:{turn:e},text:t.text,estimate:estimateTokens(t.text)+B}:void 0;if(t.kind==="tool"){const s=`${t.name}(${t.arguments})${t.output!==void 0?`
|
|
4
|
+
${t.output}`:""}`;return{kind:"tool",label:`tool \xB7 ${t.tool}${t.output!==void 0?" \u2014 called, and its result":" \u2014 called"}`,role:"tool",ref:{turn:e},text:s,estimate:estimateTokens(s)+B*(t.output!==void 0?2:1)}}return{kind:o,label:Y[t.role],role:t.role,ref:{turn:e},text:t.text,estimate:estimateTokens(t.text)+B}}$(E,"Z"),g(E,"turnDraft");function J(t){const e=t.turns;let o=e.length;for(;o>0&&e[o-1].kind==="message"&&e[o-1].role!=="assistant";)o--;return e.map((s,n)=>s.kind==="tool"?"tool":s.kind==="reasoning"?"history":s.role==="developer"||s.role==="system"?"context":n>=o&&s.role==="user"?"turn":"history")}$(J,"F"),g(J,"turnKinds");function K(t){const e=t.system.map((c,f)=>({kind:"system",label:c.source==="instructions"?"System prompt \xB7 instructions":`System prompt \xB7 ${c.source}`,ref:{system:f},text:c.text,estimate:estimateTokens(c.text)+B})),o=t.tools.map(c=>JSON.stringify({name:c.name,description:c.description,parameters:c.parameters})).join(`
|
|
5
|
+
`),s=t.tools.length?[{kind:"tools",label:`${t.tools.length} tool${t.tools.length===1?"":"s"} offered`,ref:{tools:!0},text:o,estimate:estimateTokens(o)*I}]:[],n=t.answer.format?.schema!==void 0?JSON.stringify(t.answer.format.schema):"",r=n?[{kind:"schema",label:`Answer schema${t.answer.format?.name?` \xB7 ${t.answer.format.name}`:""}`,ref:{schema:!0},text:n,estimate:estimateTokens(n)*I}]:[],h=J(t),u=t.turns.map((c,f)=>E(c,f,h[f])).filter(c=>!!c);for(const c of u){const f=t.turns[c.ref.turn];f.kind==="message"&&f.attachments?.length&&(c.label+=` \xB7 ${f.attachments.join(", ")}`)}return t.provider==="anthropic"?[...s,...e,...r,...u]:[...e,...s,...r,...u]}$(K,"U"),g(K,"draftsOf");export function aiContextOf(t,e={}){const o=e.sections!==!1,s=K(t),n=Math.round(s.reduce((a,m)=>a+m.estimate,0)),r=t.usage,h=r?.input!==void 0&&r.input>0,u=h?r.input:n,c=t.settings.some(a=>a.name==="continues response"),f=t.gaps.some(a=>/^The (request was kept to its first|prompt was not kept)/.test(a)),y=h&&(c||f)&&u>n*1.05?u-n:0,l=c?y:0,p=c?0:y,k=n>0?(u-y)/n:0,x=s.map((a,m)=>({i:m,kind:a.kind,label:a.label,...a.role?{role:a.role}:{},ref:a.ref,chars:a.text.length,tokens:Math.round(a.estimate*k),cached:0,sections:!o||a.kind==="tools"?[]:sectionsOf(a.text)}));l&&x.unshift({i:-1,kind:"carried",label:"The conversation it continues",ref:{carried:!0},chars:0,tokens:l,cached:0,sections:[]}),p&&x.push({i:-1,kind:"unkept",label:"The rest of the request \u2014 not kept",ref:{unkept:!0},chars:0,tokens:p,cached:0,sections:[]});const i=u-x.reduce((a,m)=>a+m.tokens,0);i&&x.length&&(x.reduce((a,m)=>m.tokens>a.tokens?m:a).tokens+=i);for(const a of x)a.kind==="tools"&&o&&(a.sections=t.tools.map(m=>({title:m.server?`${m.server} \u203A ${m.tool}`:m.tool,chars:0,tokens:estimateTokens(JSON.stringify({d:m.description,p:m.parameters}))}))),N(a.sections,a.tokens);x.forEach((a,m)=>a.i=m);const d=Math.min(r?.cached??0,u);let b=d;for(const a of x)a.cached=Math.min(a.tokens,b),b-=a.cached;const w=AI_PART_KINDS.map(a=>{const m=x.filter(A=>A.kind===a);return{kind:a,tokens:m.reduce((A,T)=>A+T.tokens,0),cached:m.reduce((A,T)=>A+T.cached,0),parts:m.length}}).filter(a=>a.parts),v=q(t);return{parts:x,byKind:w,input:u,counted:h,cached:d,...r?.written?{written:r.written}:{},estimated:n,output:v,findings:z(t,x,u,d,v)}}$(aiContextOf,"aiContextOf"),g(aiContextOf,"aiContextOf");function q(t){const e=t.answer,o=e.json!==void 0?JSON.stringify(e.json):e.text??e.refusal??"",s=e.toolCalls.map(p=>`${p.name}(${p.arguments})`).join(`
|
|
6
|
+
`),n=estimateTokens(o),r=estimateTokens(s)+e.toolCalls.length*B,h=t.usage,u=h?.output!==void 0,c=u?h.output:Math.round(n+r),f=Math.min(h?.reasoning??0,c),y=c-f,l=n+r>0?Math.round(r/(n+r)*y):0;return{total:c,reasoning:f,answer:y-l,tools:l,counted:u}}$(q,"X"),g(q,"outputOf");const S=g(t=>Math.round(t).toLocaleString("en-US"),"fmt"),O=g((t,e)=>`${e>0?Math.round(t/e*100):0}%`,"pct");function z(t,e,o,s,n){const r=[],h=t.answer;if(h.status==="incomplete"||/max_output_tokens|length|max_tokens/.test(h.finish??"")){const i=t.settings.find(d=>/max output|max_tokens|max_completion/.test(d.name));r.push({tone:"bad",text:`It stopped at its output cap${i?` (${i.name} ${i.value})`:""} \u2014 the answer is cut.`})}t.gaps.length&&r.push({tone:"warn",text:"Part of the call was not kept \u2014 the split covers what was."});const u=t.turns.reduce((i,d)=>i+(d.kind==="message"?d.attachments?.length??0:0),0);u&&r.push({tone:"info",text:`${u} image${u===1?"":"s"} or file${u===1?"":"s"} went with its messages: their tokens are counted by the provider but not read from the text, so the split spreads them over the parts.`});const c=t.usage;if(c?.input!==void 0&&o>=1024){if(!s)r.push({tone:"warn",text:`None of its ${S(o)} input tokens came from the prompt cache${c.written?` \u2014 ${S(c.written)} were written to it, for a later call that opens the same way`:""}.`});else if(s<o*.9){const w=e.find(v=>v.cached<v.tokens);w&&r.push({tone:"info",text:`${O(s,o)} cached \u2014 the cache stopped in \xAB${w.label}\xBB: from there on the prompt differed from the last call that opened the same way.`})}const i=e.findIndex(w=>w.kind==="context"),d=i>=0?e.slice(i+1).filter(w=>w.kind==="history"||w.kind==="tool"):[],b=d.reduce((w,v)=>w+v.tokens,0);i>=0&&d.length>=2&&e[i].cached<e[i].tokens&&b>0&&r.push({tone:"info",text:`The context the app put in sits before ${d.length} earlier turns (${S(b)} tokens): when it changes between calls, nothing after it can be read from the cache.`})}const f=e.flatMap(i=>i.sections.filter(d=>d.tag).map(d=>({p:i,x:d}))),y=f.findIndex(({x:i})=>CHANGING_TAGS.has(i.tag)),l=y>=0?f.slice(y+1).filter(({x:i})=>STABLE_TAGS.has(i.tag)):[];if(y>=0&&l.length){const i=f[y].x,d=l.reduce((b,{x:w})=>b+w.tokens,0);r.push({tone:"info",text:`\xAB${i.title}\xBB changes from call to call and comes before ${l.length} section${l.length===1?"":"s"} that should not (\xAB${l[0].x.title}\xBB\u2026, ${S(d)} tokens): put the stable ones first, or they cannot be read from the cache.`})}const p=e.filter(i=>i.kind!=="carried").reduce((i,d)=>!i||d.tokens>i.tokens?d:i,void 0);if(p&&o>2e3&&p.tokens>o*.5){const i=p.sections.reduce((d,b)=>!d||b.tokens>d.tokens?b:d,void 0);r.push({tone:"info",text:`\xAB${p.label}\xBB is ${O(p.tokens,o)} of the input${i&&i.tokens>p.tokens*.4?` \u2014 most of it \xAB${i.title}\xBB (${S(i.tokens)} tokens)`:""}.`})}const k=e.find(i=>i.kind==="tools"),x=h.toolCalls.length;return k&&k.tokens>o*.2&&!x&&!t.turns.some(i=>i.kind==="tool")&&r.push({tone:"info",text:`${t.tools.length} tool definitions are ${O(k.tokens,o)} of the input, and it called none of them.`}),n.total>=200&&n.reasoning>n.total*.7&&r.push({tone:"info",text:`${O(n.reasoning,n.total)} of its output was reasoning (${S(n.reasoning)} tokens).`}),r}$(z,"Q"),g(z,"findingsOf");export function aiSplitOf(t){return Object.fromEntries(t.byKind.filter(e=>e.tokens>0).map(e=>[e.kind,e.tokens]))}$(aiSplitOf,"aiSplitOf"),g(aiSplitOf,"aiSplitOf");
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AiCall } from './ai-call.js';
|
|
2
|
+
import type { AiCallRow } from './ai-call-row.js';
|
|
3
|
+
import { type AiPart } from './ai-context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Where each part of what a model was given came from (the AI call's Context tab): found by comparing the call with
|
|
6
|
+
* the earlier calls the lens kept of its run, thread or flow. A part — or a section of one, a `--- THE CASE MEMORY ---`
|
|
7
|
+
* block of a developer message — is an earlier call's answer handed on whole (`answer`), what a tool an earlier call
|
|
8
|
+
* asked for returned (`tool-output`), or holds passages of an earlier answer (`quoted`); else it is set beside the
|
|
9
|
+
* previous call of the same agent: a turn of the conversation it already had (`carried`), and the system prompt, the
|
|
10
|
+
* tools, the schema and the app's context `unchanged`, `changed` or `new` since — what the provider's prompt cache
|
|
11
|
+
* reads, and what each turn the app changed.
|
|
12
|
+
*
|
|
13
|
+
* Texts are compared normalized — trimmed, whitespace collapsed, JSON escapes read (`\n`, `\"`), a JSON text by its
|
|
14
|
+
* parsed value. Passages are found with a rolling hash over each text, once: a call of 350 KB against 30 earlier ones
|
|
15
|
+
* is a few passes over its characters, not one per passage.
|
|
16
|
+
*
|
|
17
|
+
* Pure: the server runs it on the call and the earlier calls it read.
|
|
18
|
+
*/
|
|
19
|
+
export type AiOriginHow = 'answer' | 'tool-output' | 'quoted' | 'carried' | 'unchanged' | 'changed' | 'new';
|
|
20
|
+
export interface AiOrigin {
|
|
21
|
+
/** The part's index in aiContextOf(call).parts. */
|
|
22
|
+
part: number;
|
|
23
|
+
/** A section of that part, by its title — absent when the origin is the whole part's. */
|
|
24
|
+
section?: string;
|
|
25
|
+
how: AiOriginHow;
|
|
26
|
+
/** The earlier call it came from, or was compared with. */
|
|
27
|
+
from?: {
|
|
28
|
+
id: string;
|
|
29
|
+
agent: string;
|
|
30
|
+
step?: number;
|
|
31
|
+
at: number;
|
|
32
|
+
service: string;
|
|
33
|
+
};
|
|
34
|
+
/** In words, for a person: «the answer of Document digest, 2 calls earlier», «unchanged since the previous call of this thread», «changed since the previous call — 1,240 → 1,610 chars», «returned by tool read_message (called in step 3)», «quotes 3 passages of Critical Limits Analyst's answer». */
|
|
35
|
+
said: string;
|
|
36
|
+
/** Beside an answer, a tool's output or a quote: how the part (or section) compares with `previous` — what changed is still marked. */
|
|
37
|
+
since?: 'unchanged' | 'changed' | 'new';
|
|
38
|
+
/** `quoted`: how many passages of that answer it holds. */
|
|
39
|
+
passages?: number;
|
|
40
|
+
/** It changed (`changed`, or `since: 'changed'`): its size in characters then and now. */
|
|
41
|
+
chars?: {
|
|
42
|
+
before: number;
|
|
43
|
+
after: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export interface AiOrigins {
|
|
47
|
+
/** How many earlier calls it was compared with. */
|
|
48
|
+
compared: number;
|
|
49
|
+
/** The call it was diffed against: the previous call of the same agent in the same thread (else run, else flow). */
|
|
50
|
+
previous?: {
|
|
51
|
+
id: string;
|
|
52
|
+
at: number;
|
|
53
|
+
by?: AiPreviousScope;
|
|
54
|
+
};
|
|
55
|
+
items: AiOrigin[];
|
|
56
|
+
}
|
|
57
|
+
/** What the previous call shares with this one: its thread, else its run, else its flow, else its request. */
|
|
58
|
+
export type AiPreviousScope = 'thread' | 'run' | 'flow' | 'request';
|
|
59
|
+
/** A text as compared: JSON escapes read, whitespace collapsed, trimmed. */
|
|
60
|
+
export declare function normText(s: string): string;
|
|
61
|
+
/** A part's text, as {@link aiContextOf} measured it — what `ai_call_context { texts }` shows of it. */
|
|
62
|
+
export declare const aiPartText: (call: AiCall, part: AiPart) => string;
|
|
63
|
+
/** Each of a part's sections' texts, in its order (a tool's definition for the tools' part). */
|
|
64
|
+
export declare const aiSectionTexts: (call: AiCall, part: AiPart) => string[];
|
|
65
|
+
/**
|
|
66
|
+
* Where each part of `call` came from — see the file's comment. `earlier`: the calls of its run, thread, flow or
|
|
67
|
+
* request made before it (any order; a later one, or itself, is left out).
|
|
68
|
+
*/
|
|
69
|
+
export declare function aiOriginsOf(call: AiCall, row: AiCallRow, earlier: Array<{
|
|
70
|
+
row: AiCallRow;
|
|
71
|
+
call: AiCall;
|
|
72
|
+
}>): AiOrigins;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";var It=Object.defineProperty;var v=(t,e)=>It(t,"name",{value:e,configurable:!0});var Mt=Object.defineProperty,c=v((t,e)=>Mt(t,"name",{value:e,configurable:!0}),"a");import{aiContextOf as at}from"./ai-context.js";const ct=8,lt=24,ft=32,Nt=80,ut=400,Tt=200,Ct=1e6,Jt=12;export function normText(t){return(t.includes("\\")?t.replace(/\\[nrt]/g," ").replace(/\\(["\\/])/g,"$1"):t).replace(/\s\s+|[^\S ]/g," ").trim()}v(normText,"normText"),c(normText,"normText");function S(t){const e=t.trim();if(e.startsWith("{")&&e.endsWith("}")||e.startsWith("[")&&e.endsWith("]"))try{return JSON.stringify(JSON.parse(e))}catch{}return normText(e)}v(S,"I"),c(S,"canonical");const W=ft,G=16777619,qt=(()=>{let t=1;for(let e=1;e<W;e++)t=Math.imul(t,G);return t})(),dt=1<<18,ht=dt-1;function Q(t){let e=0;for(let s=0;s<W;s++)e=Math.imul(e,G)+t.charCodeAt(s)|0;return e}v(Q,"ue"),c(Q,"headHash");class Wt{static{v(this,"Ne")}static{c(this,"PassageIndex")}constructor(){this.by=new Map,this.bits=new Uint32Array(dt>>>5),this.n=0}add(e){const s=Q(e.text),r=this.by.get(s);r?r.push(e):this.by.set(s,[e]),this.bits[(s&ht)>>>5]|=1<<(s&31),this.n++}get size(){return this.n}find(e,s){if(!this.n||e.length<W)return;const r=new Set,n=this.bits;let a=Q(e);for(let u=0;;u++){const h=(n[(a&ht)>>>5]&1<<(a&31))!==0?this.by.get(a):void 0;if(h)for(const w of h)!r.has(w)&&e.startsWith(w.text,u)&&(r.add(w),s(w));if(u+W>=e.length)break;a=Math.imul(a-Math.imul(e.charCodeAt(u),qt)|0,G)+e.charCodeAt(u+W)|0}}}const pt=c(t=>t.replace(/^(?:mcp__)?[A-Za-z0-9-]+(?:_[A-Za-z0-9-]+)*__/,""),"toolName");function C(t,e,s,r=0){if(!(r>Jt)){if(typeof t=="string")e(t,s);else if(Array.isArray(t))for(const n of t)C(n,e,s,r+1);else if(t&&typeof t=="object")for(const[n,a]of Object.entries(t))C(a,e,n,r+1)}}v(C,"T"),c(C,"strings");const V=c(t=>{const e=t.trim();if(!(!e.startsWith("{")&&!e.startsWith("[")))try{return JSON.parse(e)}catch{return}},"parseJson");function gt(t){const e=t.answer,s=[],r=c((n,a)=>{const u=S(n);u.length>=ct&&s.push({key:u,text:normText(n),...a?{what:a}:{}})},"add");e.text!==void 0&&r(e.text),e.json!==void 0&&(e.text===void 0&&r(JSON.stringify(e.json)),C(e.json,(n,a)=>{n.trim().length>=lt&&r(n,a?`its \xAB${a}\xBB`:void 0)}));for(const n of e.toolCalls){const a=n.tool||pt(n.name);r(n.arguments,`its call of ${a}`),C(V(n.arguments),(u,h)=>{u.trim().length>=lt&&r(u,h?`${a}\u2019s \xAB${h}\xBB`:`its call of ${a}`)})}return s}v(gt,"_e"),c(gt,"answerKeys");function wt(t){const e=[],s=new Set,r=c((h,w,x)=>{if(e.length>=Tt||s.has(h))return;s.add(h);const O={text:h,e:t,...w?{what:w}:{},...x?{parent:x}:{}};return e.push(O),O},"push"),n=c((h,w,x,O)=>{const[j,...K]=O;if(j)for(const J of h.split(j)){const N=normText(J);if(N.length<Nt||N===x?.text)continue;const R=r(N,w,x)??x;N.length>ut&&n(J,w,R,K)}},"pieces"),a=c((h,w)=>{const x=normText(h);if(x.length<ft)return;const O=r(x,w);x.length>ut&&n(h,w,O,[/\n\s*\n/,/\n/,/(?<=[.!?:;])\s+/])},"text"),u=t.call.answer;u.json!==void 0?C(u.json,(h,w)=>a(h,w?`its \xAB${w}\xBB`:void 0)):u.text!==void 0&&a(u.text);for(const h of u.toolCalls){const w=h.tool||pt(h.name),x=V(h.arguments);x!==void 0?C(x,(O,j)=>a(O,j?`${w}\u2019s \xAB${j}\xBB`:`its call of ${w}`)):a(h.arguments,`its call of ${w}`)}return e}v(wt,"je"),c(wt,"passagesOf");function mt(t){const e=[];let s=Ct;const r=c(n=>{if(s<=0||!n)return;const a=n.length>s?n.slice(0,s):n;s-=a.length,e.push(a)},"take");for(const n of t.system)r(n.text);for(const n of t.turns)n.kind==="tool"?(r(n.arguments),r(n.output??"")):r(n.text??"");return e}v(mt,"Te"),c(mt,"inputTexts");function _(t,e){const s=e.ref;if("system"in s)return t.system[s.system]?.text??"";if("tools"in s)return t.tools.map(r=>JSON.stringify({name:r.name,description:r.description,parameters:r.parameters})).join(`
|
|
2
|
+
`);if("schema"in s)return t.answer.format?.schema!==void 0?JSON.stringify(t.answer.format.schema):"";if("turn"in s){const r=t.turns[s.turn];return r?r.kind==="tool"?`${r.name}(${r.arguments})${r.output!==void 0?`
|
|
3
|
+
${r.output}`:""}`:r.text??"":""}return""}v(_,"K"),c(_,"partText");export const aiPartText=c((t,e)=>_(t,e),"aiPartText"),aiSectionTexts=c((t,e)=>z(t,e,_(t,e)),"aiSectionTexts");function $t(t){const e=V(t);if(e===void 0)return[];const s=e&&typeof e=="object"&&!Array.isArray(e)?e.properties:void 0,r=s&&typeof s=="object"&&!Array.isArray(s)?s:e;return(Array.isArray(r)?r:r&&typeof r=="object"?Object.values(r):[]).map(n=>typeof n=="string"?n:JSON.stringify(n,null,2)??"")}v($t,"Ee"),c($t,"jsonMemberTexts");function z(t,e,s){if(e.kind==="tools")return t.tools.map(n=>JSON.stringify({d:n.description,p:n.parameters}));let r;return e.sections.map((n,a)=>n.at!==void 0&&n.end!==void 0?s.slice(n.at,n.end):(r??=$t(s),r[a]??""))}v(z,"B"),c(z,"sectionTexts");function E(t){const e=new Map;return t.sections.map(s=>{const r=e.get(s.title)??0;return e.set(s.title,r+1),`${s.title}#${r}`})}v(E,"F"),c(E,"titled");function X(t){return t?t.kind==="tool"?`tool
|
|
4
|
+
${t.name}
|
|
5
|
+
${S(t.arguments)}
|
|
6
|
+
${S(t.output??"")}`:t.kind==="reasoning"?`reasoning
|
|
7
|
+
${S(t.text??"")}`:`${t.role}
|
|
8
|
+
${S(t.text)}`:""}v(X,"le"),c(X,"turnKey");const _t=new Set(["system","tools","schema","context"]),vt=new Set(["history","tool","turn"]);function Y(t){const e=new Map,s=new Map;for(const r of t){if(!_t.has(r.kind))continue;const n=`${r.kind}:${r.role??""}`,a=e.get(n)??0;e.set(n,a+1),s.set(r,`${n}#${a}`)}return s}v(Y,"pe"),c(Y,"structureKeys");const xt=c(t=>t.toLocaleString("en-US"),"fmt"),D=c(t=>t.k===1?"the call before":`${t.k} calls earlier`,"ago"),P=c(t=>({id:t.row.id,agent:t.row.agent.name,...t.row.step!==void 0?{step:t.row.step}:{},at:t.row.at,service:t.row.service}),"fromOf"),yt={answer:6,"tool-output":6,quoted:5,carried:4,changed:3,unchanged:2,new:1};function kt(t,e){const s=e.filter(n=>n.row.agent.key===t.agent.key),r=[["thread",t.thread?n=>n.thread?.key===t.thread.key:void 0],["run",n=>n.run.key===t.run.key],["flow",t.flowId?n=>n.flowId===t.flowId:void 0],["request",t.requestId?n=>n.requestId===t.requestId:void 0]];for(const[n,a]of r)if(a){for(let u=s.length-1;u>=0;u--)if(a(s[u].row))return{e:s[u],by:n}}}v(kt,"We"),c(kt,"previousOf");export function aiOriginsOf(t,e,s){const r=s.filter(o=>o.row.id!==e.id&&o.row.at<=e.at).sort((o,i)=>o.row.at-i.row.at||o.row.id.localeCompare(i.row.id)),n=r.map((o,i)=>({row:o.row,call:o.call,k:r.length-i})),a=kt(e,n),u={compared:n.length,...a?{previous:{id:a.e.row.id,at:a.e.row.at,by:a.by}}:{},items:[]};if(!n.length)return u;const h=new Map,w=new Wt;for(const o of n){for(const i of gt(o.call)){const d=h.get(i.key);d?d.push({...i,e:o}):h.set(i.key,[{...i,e:o}])}for(const i of wt(o))w.add(i)}const x=new Map,O=new Map,j=c((o,i)=>{const d=`${o.k}
|
|
9
|
+
${i}`;let m=O.get(d);if(m===void 0){let l=x.get(o);l||x.set(o,l=mt(o.call).map(normText)),m=l.some(f=>f.includes(i)),O.set(d,m)}return m},"given"),K=c((o,...i)=>{for(const d of i){if(d===void 0)continue;const m=S(d),l=m.length>=ct?h.get(m):void 0;for(let f=(l?.length??0)-1;f>=0;f--){const y=l[f];if(o||!j(y.e,y.text))return{e:y.e,...y.what?{what:y.what}:{}}}}},"answerOf"),J=c((o,i=!1)=>{if(!w.size)return;const d=new Set;if(w.find(normText(o),f=>d.add(f)),!i)for(const f of d)j(f.e,f.text)&&d.delete(f);if(!d.size)return;const m=new Map;for(const f of d){let y=f.parent;for(;y&&!d.has(y);)y=y.parent;if(y)continue;const I=m.get(f.e)??{e:f.e,n:0,what:new Set};I.n++,f.what&&I.what.add(f.what),m.set(f.e,I)}let l;for(const f of m.values())(!l||f.n>l.n||f.n===l.n&&f.e.k<l.e.k)&&(l=f);return l},"quoteIn"),N=c(o=>({how:"answer",from:P(o.e),said:`the answer of ${o.e.row.agent.name}${o.what?` (${o.what})`:""}, ${D(o.e)}`}),"answered"),R=c(o=>({how:"quoted",from:P(o.e),passages:o.n,said:`quotes ${o.n===1?"a passage":`${o.n} passages`} of ${o.e.row.agent.name}\u2019s answer${o.what.size===1?` (${[...o.what][0]})`:""}, ${D(o.e)}`}),"quoted"),tt=at(t);let U,et=[];if(a){const o=at(a.e.call);U=new Map;for(const[i,d]of Y(o.parts))U.set(d,{part:i,text:_(a.e.call,i)});et=o.parts.filter(i=>vt.has(i.kind)&&"turn"in i.ref).map(i=>X(a.e.call.turns[i.ref.turn]))}const bt=c(o=>{const i=U?.get(o);if(i){if(!i.sections){const d=z(a.e.call,i.part,i.text);i.sections=new Map(E(i.part).map((m,l)=>[m,{text:d[l]??"",canon:S(d[l]??"")}]))}return i.sections}},"prevSections"),St=a?`the previous call of this ${a.by}`:"",B=a?P(a.e):void 0,nt=c((o,i)=>`changed since the previous call \u2014 ${xt(o)} \u2192 ${xt(i)} chars`,"changedSaid"),Ot=Y(tt.parts),ot=[...n].reverse();let rt=0;return tt.parts.forEach((o,i)=>{if(o.kind==="carried"){const p=e.previousResponseId??t.settings.find(g=>g.name==="continues response")?.value,$=p?ot.find(g=>(g.row.responseId??g.call.responseId)===p):void 0;$&&u.items.push({part:i,how:"carried",from:P($),said:`the conversation of ${$.row.agent.name}\u2019s response it continues, ${D($)} \u2014 held by the provider, not sent again`});return}const d=_(t,o),m="turn"in o.ref?t.turns[o.ref.turn]:void 0;let l,f;const y=Ot.get(o);if(a&&y){const p=U.get(y);p?S(p.text)===S(d)?l={part:i,how:"unchanged",from:B,said:`unchanged since ${St}`}:(l={part:i,how:"changed",from:B,said:nt(p.text.length,d.length),chars:{before:p.text.length,after:d.length}},f=bt(y)):l={part:i,how:"new",from:B,said:"new since the previous call"}}else if(a&&vt.has(o.kind)){const p=X(m),$=et.indexOf(p,rt);$>=0?(rt=$+1,l={part:i,how:"carried",from:B,said:"carried from the previous call"}):l={part:i,how:"new",from:B,said:"new since the previous call"}}const I=l&&l.how!=="carried"?l.how:l?"unchanged":void 0;let k;if(m?.kind==="tool"&&m.callId){const p=m.callId,$=ot.find(g=>g.row.callIds?.includes(p)||g.call.answer.toolCalls.some(b=>b.callId===p));if($){const g=$.row.step!==void 0&&$.row.run.key===e.run.key?`called in step ${$.row.step}`:`called by ${$.row.agent.name}, ${D($)}`;k={part:i,how:"tool-output",from:P($),said:`returned by tool ${m.tool} (${g})`}}}const Z=m?.kind==="message"&&m.role==="assistant";if(!k){const p=K(Z,m?.kind==="tool"?m.output:d);p&&(k={part:i,...N(p)})}const F=k?void 0:J(d,Z);F&&(k={part:i,...R(F)}),k&&I&&(k.since=I),k&&l?.chars&&(k.chars=l.chars);const T=k&&(!l||yt[k.how]>=yt[l.how])?k:l;if(T&&f&&T.how==="changed"){const p=new Set(E(o)),$=[...f.keys()].filter(A=>!p.has(A)).map(A=>A.replace(/#\d+$/,"")),g={changed:0,new:0},b=z(t,o,d);E(o).forEach((A,H)=>{const M=f.get(A);M?M.canon!==S(b[H]??"")&&g.changed++:g.new++});const q=[g.changed?`${g.changed} of ${o.sections.length} sections changed`:"",g.new?`${g.new} new`:"",$.length?`${$.length} gone (${$.slice(0,3).map(A=>`\xAB${A}\xBB`).join(", ")}${$.length>3?", \u2026":""})`:""].filter(Boolean);q.length&&(T.said+=`: ${q.join(", ")}`)}if(T&&u.items.push(T),!o.sections.length||T?.how==="answer"||T?.how==="tool-output")return;const At=z(t,o,d),jt=E(o),Bt=!!F;o.sections.forEach((p,$)=>{const g=At[$]??"";if(!g)return;let b;if(f){const L=f.get(jt[$]);L?L.canon===S(g)?b={part:i,section:p.title,how:"unchanged",from:B,said:"unchanged since the previous call"}:b={part:i,section:p.title,how:"changed",from:B,said:nt(L.text.length,g.length),chars:{before:L.text.length,after:g.length}}:b={part:i,section:p.title,how:"new",from:B,said:"new since the previous call"}}const q=p.at!==void 0&&p.title!=="(opening)"?g.indexOf(`
|
|
10
|
+
`):-1,A=K(Z,g,q>0?g.slice(q+1):void 0),H=A?void 0:Bt?J(g,Z):void 0,M=A?{part:i,section:p.title,...N(A)}:H?{part:i,section:p.title,...R(H)}:void 0,st=b?b.how:I!=="changed"?I:void 0;M&&st&&(M.since=st),M&&b?.chars&&(M.chars=b.chars);const it=M??b;it&&u.items.push(it)})}),u}v(aiOriginsOf,"aiOriginsOf"),c(aiOriginsOf,"aiOriginsOf");
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI calls page's playground (planning/ai-calls D1, D4): a kept call sent again — to another model, its prompt,
|
|
3
|
+
* input or tools changed — on the lens's own keys, paid from the click allowance. A tool the model calls gets the
|
|
4
|
+
* result recorded in the original run (or one a person typed); nothing reaches the running app.
|
|
5
|
+
*/
|
|
6
|
+
/** A tool's result the playground gives back: recorded in the call's run, or typed by a person. */
|
|
7
|
+
export interface AiToolResult {
|
|
8
|
+
name: string;
|
|
9
|
+
/** The arguments it answers, as sent (JSON); absent on a person's: it answers any call of that tool. */
|
|
10
|
+
arguments?: string;
|
|
11
|
+
output: string;
|
|
12
|
+
}
|
|
13
|
+
/** `GET /api/ai/playground?id=` — what the playground can do with one call. */
|
|
14
|
+
export interface AiPlaygroundInfo {
|
|
15
|
+
/** The models the lens's keys can send to, the call's own first; the cost of a step estimated from the call's tokens. */
|
|
16
|
+
models: Array<{
|
|
17
|
+
model: string;
|
|
18
|
+
provider: 'openai' | 'anthropic';
|
|
19
|
+
estimateUsd?: number;
|
|
20
|
+
own?: true;
|
|
21
|
+
}>;
|
|
22
|
+
/** The call's request as it was sent (the Responses API body), to start from. */
|
|
23
|
+
body?: Record<string, unknown>;
|
|
24
|
+
/** Every tool result its run recorded, oldest first. */
|
|
25
|
+
recorded: Array<AiToolResult & {
|
|
26
|
+
at: number;
|
|
27
|
+
}>;
|
|
28
|
+
/** Today's spend on clicks, and the allowance. */
|
|
29
|
+
spentTodayUsd: number;
|
|
30
|
+
capUsd: number;
|
|
31
|
+
/** Why the playground cannot send this call. */
|
|
32
|
+
unavailable?: string;
|
|
33
|
+
}
|
|
34
|
+
/** `POST /api/ai/playground` — one run of the playground, on one model. */
|
|
35
|
+
export interface AiPlaygroundRun {
|
|
36
|
+
id: string;
|
|
37
|
+
model: string;
|
|
38
|
+
/** The request as edited (the Responses API body); absent: as it was sent. */
|
|
39
|
+
body?: Record<string, unknown>;
|
|
40
|
+
/** Results a person typed: theirs before the recorded ones. */
|
|
41
|
+
results?: AiToolResult[];
|
|
42
|
+
/** Steps of the tool loop at most: 1–8 (default 6). */
|
|
43
|
+
maxSteps?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AiPlaygroundStep {
|
|
46
|
+
/** The provider's API, its request and its answer as sent and received — read like a kept call. */
|
|
47
|
+
api: 'responses' | 'messages';
|
|
48
|
+
sent: Record<string, unknown>;
|
|
49
|
+
response: Record<string, unknown>;
|
|
50
|
+
model: string;
|
|
51
|
+
usage: {
|
|
52
|
+
input: number;
|
|
53
|
+
output: number;
|
|
54
|
+
cacheRead?: number;
|
|
55
|
+
};
|
|
56
|
+
costUsd?: number;
|
|
57
|
+
latencyMs: number;
|
|
58
|
+
/** The tools it called this step: each with the result given back, and where that came from. */
|
|
59
|
+
tools: Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
arguments: string;
|
|
62
|
+
output?: string;
|
|
63
|
+
from: 'recorded' | 'edited' | 'missing';
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
export interface AiPlaygroundAnswer {
|
|
67
|
+
model: string;
|
|
68
|
+
provider: 'openai' | 'anthropic';
|
|
69
|
+
/** The key that paid. */
|
|
70
|
+
keyLabel?: string;
|
|
71
|
+
steps: AiPlaygroundStep[];
|
|
72
|
+
/** It answered; a tool's result is missing (type one, run again); the step limit; a call failed. */
|
|
73
|
+
stopped: 'answered' | 'missing-result' | 'max-steps' | 'failed';
|
|
74
|
+
error?: string;
|
|
75
|
+
costUsd: number;
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";export{};
|
package/lib/dashboard.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* on-demand GETs.
|
|
12
12
|
*/
|
|
13
13
|
import type { HttpExchange } from './http-exchange.js';
|
|
14
|
+
import type { DashTeamDesignRun, TeamDesign } from './team-design.js';
|
|
14
15
|
import type { PageVisitNavType, PageVisitRaw, PageVisitReadyBy, PageVisitRequest, PageVisitVitals } from './page-visit.js';
|
|
15
16
|
/** Bumped only on a breaking change to the stream framing itself. */
|
|
16
17
|
export declare const DASHBOARD_PROTOCOL = 1;
|
|
@@ -2617,10 +2618,12 @@ export interface DashAiRun {
|
|
|
2617
2618
|
/**
|
|
2618
2619
|
* `db`: one database (`<store>/<database>`, the health check); `dbtable`: one table (`<store>/<database>/<schema>.<table>`);
|
|
2619
2620
|
* `kvstore` / `queuestore`: one Redis store's keys / BullMQ queues (`<store>`); `files`: the services' file access
|
|
2620
|
-
* (`files`); `external`: the external APIs the services call (`external`); `log`: one log line (its `seq`)
|
|
2621
|
+
* (`files`); `external`: the external APIs the services call (`external`); `log`: one log line (its `seq`);
|
|
2622
|
+
* `aiagent`: one AI agent of the app, by its key on the AI calls page (`agent:onboarding-chat-conductor`);
|
|
2623
|
+
* `team`: the project's agent team, designed (`team.design`) — by its repository (`project` when it has no id).
|
|
2621
2624
|
*/
|
|
2622
2625
|
target: {
|
|
2623
|
-
kind: 'sql' | 'route' | 'error' | 'flow' | 'ui' | 'kv' | 'queue' | 'db' | 'dbtable' | 'kvstore' | 'queuestore' | 'files' | 'external' | 'log' | 'page';
|
|
2626
|
+
kind: 'sql' | 'route' | 'error' | 'flow' | 'ui' | 'kv' | 'queue' | 'db' | 'dbtable' | 'kvstore' | 'queuestore' | 'files' | 'external' | 'log' | 'page' | 'aiagent' | 'team';
|
|
2624
2627
|
id: string;
|
|
2625
2628
|
label: string;
|
|
2626
2629
|
name?: string;
|
|
@@ -2706,6 +2709,10 @@ export interface DashAiRun {
|
|
|
2706
2709
|
/** The reviews it had run before it (a route's statements' SQL reviews), as each went. */
|
|
2707
2710
|
ranFirst?: DashAiRanFirst[];
|
|
2708
2711
|
suggestion?: DashAiSuggestion;
|
|
2712
|
+
/** A team design run's answer, as LensMCP checked it — a design is not a review: it has no suggestion. */
|
|
2713
|
+
design?: TeamDesign;
|
|
2714
|
+
/** The design was accepted on the Team page: when, the files it wrote, the goals it created. */
|
|
2715
|
+
accepted?: DashTeamDesignRun['accepted'];
|
|
2709
2716
|
impact?: DashAiImpact;
|
|
2710
2717
|
/** The instructions it sent: which guide, the workspace's rules and memories, the indicator set. */
|
|
2711
2718
|
prompt?: DashAiPrompt;
|
|
@@ -2898,7 +2905,7 @@ export interface DashSuggestion {
|
|
|
2898
2905
|
}
|
|
2899
2906
|
/** One thing a suggestion is about, or rests on — and how to read it. */
|
|
2900
2907
|
export interface DashSuggestionRef {
|
|
2901
|
-
kind: 'sql' | 'route' | 'flow' | 'page' | 'error' | 'queue' | 'kv' | 'db' | 'dbtable' | 'table' | 'external' | 'code' | 'run';
|
|
2908
|
+
kind: 'sql' | 'route' | 'flow' | 'page' | 'error' | 'queue' | 'kv' | 'db' | 'dbtable' | 'table' | 'external' | 'aiagent' | 'code' | 'run';
|
|
2902
2909
|
id: string;
|
|
2903
2910
|
label: string;
|
|
2904
2911
|
/** `target`: what was reviewed; `reviewed-with`: a review its review was shown; `named`: the finding names it; `related`: the catalogs join it to the target. */
|
package/lib/event-loop.d.ts
CHANGED
|
@@ -63,3 +63,97 @@ export interface EventLoopsView {
|
|
|
63
63
|
persistence: 'disk' | 'memory';
|
|
64
64
|
retentionDays: number;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* A loop's delay over one short window — `[end of the window (epoch ms), mean, p99, max (ms), utilization (0–1)]`.
|
|
68
|
+
* A Node pod measures one every 5 s (`LENSMCP_EVENT_LOOP_STEP_MS`) and carries every one since its last report in the
|
|
69
|
+
* next `event-loop-lag` event (`raw.samples`): the history costs the bus no event of its own.
|
|
70
|
+
*/
|
|
71
|
+
export type EventLoopSample = [at: number, meanMs: number, p99Ms: number, maxMs: number, utilization: number];
|
|
72
|
+
/**
|
|
73
|
+
* One bucket of a loop's samples — `[bucket start (epoch ms), mean, p99, max, utilization]`. max and p99 are the
|
|
74
|
+
* bucket's highest, so a spike survives any bucket width; mean and utilization are its average.
|
|
75
|
+
*/
|
|
76
|
+
export type EventLoopPoint = [at: number, meanMs: number, p99Ms: number, maxMs: number, utilization: number];
|
|
77
|
+
/** One loop — a process lifetime, so a restart or a hot swap starts a new series — over the asked window. */
|
|
78
|
+
export interface EventLoopSeries {
|
|
79
|
+
loop: EventLoopIdentity;
|
|
80
|
+
/** The first and the last sample the lens holds of this loop (all time, not only the window). */
|
|
81
|
+
firstAt: number;
|
|
82
|
+
lastAt: number;
|
|
83
|
+
points: EventLoopPoint[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A Node process's first minute — loading modules, wiring its container, opening its first connections. Stalls there are
|
|
87
|
+
* the price of a start, not of the work it serves: on FoodGuard (2026-10-28) 83% of the services' stalls and 800 of the
|
|
88
|
+
* 822 longer than a second came in it, so the Event loops charts read the two apart.
|
|
89
|
+
*/
|
|
90
|
+
export declare const LOOP_STARTUP_MS = 60000;
|
|
91
|
+
/**
|
|
92
|
+
* When a Node loop's process started: its id ends with the process's `performance.timeOrigin`
|
|
93
|
+
* (`node:<pid>:<thread>:<timeOrigin>`). A browser document's load is not a start in this sense — its stalls are what a
|
|
94
|
+
* user sees — so a browser loop, or an id without the time, has none.
|
|
95
|
+
*/
|
|
96
|
+
export declare function loopStartedAt(loopId: string): number | undefined;
|
|
97
|
+
/** Where in its process's life a moment of a Node loop fell: starting up (its first minute) or running, and how far in. */
|
|
98
|
+
export declare function loopPhaseAt(loopId: string, at: number): {
|
|
99
|
+
phase: 'startup' | 'running';
|
|
100
|
+
sinceStartMs: number;
|
|
101
|
+
} | undefined;
|
|
102
|
+
/** One part of a process's life over a window: its stalls, the longest, and the services that had the most. */
|
|
103
|
+
export interface EventLoopPhaseTotals {
|
|
104
|
+
stalls: number;
|
|
105
|
+
longestMs: number;
|
|
106
|
+
/** The services with the most stalls in it, at most five. */
|
|
107
|
+
services: Array<{
|
|
108
|
+
project: string;
|
|
109
|
+
stalls: number;
|
|
110
|
+
longestMs: number;
|
|
111
|
+
}>;
|
|
112
|
+
}
|
|
113
|
+
/** A window's stalls read apart: a Node process's first minute ({@link LOOP_STARTUP_MS}) and the work after it. */
|
|
114
|
+
export interface EventLoopPhaseWindow {
|
|
115
|
+
window: string;
|
|
116
|
+
from: number;
|
|
117
|
+
to: number;
|
|
118
|
+
running: EventLoopPhaseTotals;
|
|
119
|
+
startup: EventLoopPhaseTotals & {
|
|
120
|
+
/** Node processes that started in the window, and those of them with a stall in their first minute. */
|
|
121
|
+
starts: number;
|
|
122
|
+
stalledStarts: number;
|
|
123
|
+
};
|
|
124
|
+
/** The share of the window's stalls that were a start's, in percent; absent when it had none. */
|
|
125
|
+
startupSharePct?: number;
|
|
126
|
+
}
|
|
127
|
+
/** The stall incidents of one loop in one bucket, for a marker: what a click opens is read from `/api/event-loops`. */
|
|
128
|
+
export interface EventLoopStallMark {
|
|
129
|
+
loopId: string;
|
|
130
|
+
/** The loop's service, so a loop with stalls and no samples (a browser page) still gets its chart. */
|
|
131
|
+
project: string;
|
|
132
|
+
runtime: EventLoopIdentity['runtime'];
|
|
133
|
+
/** The bucket's start, as the points'. */
|
|
134
|
+
at: number;
|
|
135
|
+
count: number;
|
|
136
|
+
longestMs: number;
|
|
137
|
+
/** One of them was not responding when last observed. */
|
|
138
|
+
unresponsive: boolean;
|
|
139
|
+
/** These stalls began in the process's first {@link LOOP_STARTUP_MS}: a loop's bucket may hold one mark of each. */
|
|
140
|
+
startup?: true;
|
|
141
|
+
}
|
|
142
|
+
/** `/api/event-loops/history`: every loop's delay over a window, bucketed, with its stalls. */
|
|
143
|
+
export interface EventLoopHistory {
|
|
144
|
+
from: number;
|
|
145
|
+
to: number;
|
|
146
|
+
/** The bucket width. */
|
|
147
|
+
stepMs: number;
|
|
148
|
+
/** What the buckets were made of: the 5 s samples (kept `samplesHours`), or the 5-minute rollups (kept `retentionDays`). */
|
|
149
|
+
source: 'samples' | 'rollups';
|
|
150
|
+
series: EventLoopSeries[];
|
|
151
|
+
/** More loops had samples in the window than are listed: the most recently seen are. */
|
|
152
|
+
seriesTruncated?: boolean;
|
|
153
|
+
stalls: EventLoopStallMark[];
|
|
154
|
+
/** The start the stall marks were split at ({@link LOOP_STARTUP_MS}); absent from an older lens, whose marks are not. */
|
|
155
|
+
startupMs?: number;
|
|
156
|
+
persistence: 'disk' | 'memory';
|
|
157
|
+
samplesHours: number;
|
|
158
|
+
retentionDays: number;
|
|
159
|
+
}
|
package/lib/event-loop.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export{};
|
|
1
|
+
"use strict";var a=Object.defineProperty;var r=(t,e)=>a(t,"name",{value:e,configurable:!0});var i=Object.defineProperty,s=r((t,e)=>i(t,"name",{value:e,configurable:!0}),"e");export const LOOP_STARTUP_MS=6e4;export function loopStartedAt(t){if(!t.startsWith("node:"))return;const e=Number(t.slice(t.lastIndexOf(":")+1));return Number.isFinite(e)&&e>0?e:void 0}r(loopStartedAt,"loopStartedAt"),s(loopStartedAt,"loopStartedAt");export function loopPhaseAt(t,e){const n=loopStartedAt(t);if(n===void 0)return;const o=Math.max(0,Math.round(e-n));return{phase:o<6e4?"startup":"running",sinceStartMs:o}}r(loopPhaseAt,"loopPhaseAt"),s(loopPhaseAt,"loopPhaseAt");
|