@ai-ecoverse/slicc-agent 4.2.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +9 -1
  2. package/dist/client.js +5 -2
  3. package/dist/client.js.map +2 -2
  4. package/dist/control.js +3 -2
  5. package/dist/control.js.map +2 -2
  6. package/dist/host.js +21 -9
  7. package/dist/host.js.map +3 -3
  8. package/dist/main.js +24 -10
  9. package/dist/main.js.map +2 -2
  10. package/dist/readonly.js +9 -2
  11. package/dist/readonly.js.map +2 -2
  12. package/dist/secrets/legacy.js +112 -0
  13. package/dist/secrets/legacy.js.map +7 -0
  14. package/dist/services.js.map +2 -2
  15. package/dist/settings.js +8 -2
  16. package/dist/settings.js.map +2 -2
  17. package/dist/spectrum/agent-port.js +31 -8
  18. package/dist/spectrum/agent-port.js.map +2 -2
  19. package/dist/spectrum/index.js +6 -0
  20. package/dist/spectrum/index.js.map +3 -3
  21. package/dist/spectrum/messages.js +18 -4
  22. package/dist/spectrum/messages.js.map +2 -2
  23. package/dist/spectrum/notices-port.js +42 -0
  24. package/dist/spectrum/notices-port.js.map +7 -0
  25. package/dist/titles/cluster.js +61 -0
  26. package/dist/titles/cluster.js.map +7 -0
  27. package/dist/titles/generate.js +33 -13
  28. package/dist/titles/generate.js.map +2 -2
  29. package/dist/titles/labels.js +218 -0
  30. package/dist/titles/labels.js.map +7 -0
  31. package/dist/titles/shown.js +12 -0
  32. package/dist/titles/shown.js.map +7 -0
  33. package/dist/types/client.d.ts +1 -0
  34. package/dist/types/control.d.ts +2 -1
  35. package/dist/types/host.d.ts +2 -0
  36. package/dist/types/secrets/legacy.d.ts +36 -0
  37. package/dist/types/services.d.ts +4 -0
  38. package/dist/types/settings.d.ts +2 -0
  39. package/dist/types/spectrum/agent-port.d.ts +1 -0
  40. package/dist/types/spectrum/index.d.ts +3 -0
  41. package/dist/types/spectrum/messages.d.ts +2 -0
  42. package/dist/types/spectrum/notices-port.d.ts +15 -0
  43. package/dist/types/titles/cluster.d.ts +15 -0
  44. package/dist/types/titles/generate.d.ts +13 -1
  45. package/dist/types/titles/labels.d.ts +33 -0
  46. package/dist/types/titles/shown.d.ts +2 -0
  47. package/package.json +4 -4
@@ -0,0 +1,61 @@
1
+ import { answerText } from "../agent.js";
2
+ const OUTPUT_LENGTH = 600;
3
+ function blocks(entry) {
4
+ const content = entry.model?.[0]?.content;
5
+ return entry.kind === "pi.assistant" && Array.isArray(content) ? content : [];
6
+ }
7
+ function callIdsOf(views) {
8
+ const ids = /* @__PURE__ */ new Set();
9
+ for (const view of views)
10
+ for (const entry of view.entries)
11
+ for (const block of blocks(entry))
12
+ if (block.type === "toolCall" && block.id) ids.add(block.id);
13
+ return ids;
14
+ }
15
+ function movedOn(entry) {
16
+ if (entry.kind === "pi.user") return true;
17
+ if (entry.kind !== "pi.assistant") return false;
18
+ const said = entry.model?.[0];
19
+ return said?.stopReason !== "toolUse" || blocks(entry).some((block) => block.type === "text" && block.text?.trim());
20
+ }
21
+ function clusterOf(entries, callIds) {
22
+ const calls = /* @__PURE__ */ new Map();
23
+ const results = /* @__PURE__ */ new Map();
24
+ entries.forEach((entry, index) => {
25
+ const said = entry.model?.[0];
26
+ for (const block of blocks(entry))
27
+ if (block.type === "toolCall" && block.id)
28
+ calls.set(block.id, {
29
+ name: block.name ?? "tool",
30
+ input: JSON.stringify(block.arguments ?? {}),
31
+ at: said?.timestamp ?? 0
32
+ });
33
+ if (entry.kind === "pi.tool-result" && said?.toolCallId)
34
+ results.set(said.toolCallId, { output: answerText(said.content), index });
35
+ });
36
+ const runs = [];
37
+ let at = 0;
38
+ let last = -1;
39
+ for (const id of callIds) {
40
+ const call = calls.get(id);
41
+ if (!call) return void 0;
42
+ const result = results.get(id);
43
+ runs.push({ name: call.name, input: call.input, output: result?.output ?? "", done: !!result });
44
+ at = Math.max(at, call.at);
45
+ last = Math.max(last, result?.index ?? Number.POSITIVE_INFINITY);
46
+ }
47
+ const settled = entries.slice(last + 1).some(movedOn);
48
+ return { runs, at, settled };
49
+ }
50
+ function describe(runs) {
51
+ return runs.map(
52
+ (run) => `${run.name}: ${run.input}
53
+ => ${run.done ? run.output.slice(0, OUTPUT_LENGTH) : "(running)"}`
54
+ ).join("\n\n");
55
+ }
56
+ export {
57
+ callIdsOf,
58
+ clusterOf,
59
+ describe
60
+ };
61
+ //# sourceMappingURL=cluster.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/titles/cluster.ts"],
4
+ "sourcesContent": ["import type { ConversationView, EntryRecord } from '@earendil-works/pi-durable';\nimport { answerText } from '../agent.ts';\n\nconst OUTPUT_LENGTH = 600;\n\nexport type Run = { name: string; input: string; output: string; done: boolean };\n\nexport type Cluster = { runs: Run[]; at: number; settled: boolean };\n\ntype Block = { type?: string; id?: string; name?: string; arguments?: unknown; text?: string };\n\ntype Said = {\n content?: unknown;\n toolCallId?: string;\n timestamp?: number;\n stopReason?: string;\n};\n\nfunction blocks(entry: EntryRecord): Block[] {\n const content = (entry.model?.[0] as Said | undefined)?.content;\n return entry.kind === 'pi.assistant' && Array.isArray(content) ? (content as Block[]) : [];\n}\n\nexport function callIdsOf(views: readonly ConversationView[]): Set<string> {\n const ids = new Set<string>();\n for (const view of views)\n for (const entry of view.entries)\n for (const block of blocks(entry))\n if (block.type === 'toolCall' && block.id) ids.add(block.id);\n return ids;\n}\n\nfunction movedOn(entry: EntryRecord): boolean {\n if (entry.kind === 'pi.user') return true;\n if (entry.kind !== 'pi.assistant') return false;\n const said = entry.model?.[0] as Said | undefined;\n return (\n said?.stopReason !== 'toolUse' ||\n blocks(entry).some((block) => block.type === 'text' && block.text?.trim())\n );\n}\n\nexport function clusterOf(\n entries: readonly EntryRecord[],\n callIds: readonly string[]\n): Cluster | undefined {\n const calls = new Map<string, Pick<Run, 'name' | 'input'> & { at: number }>();\n const results = new Map<string, { output: string; index: number }>();\n entries.forEach((entry, index) => {\n const said = entry.model?.[0] as Said | undefined;\n for (const block of blocks(entry))\n if (block.type === 'toolCall' && block.id)\n calls.set(block.id, {\n name: block.name ?? 'tool',\n input: JSON.stringify(block.arguments ?? {}),\n at: said?.timestamp ?? 0,\n });\n if (entry.kind === 'pi.tool-result' && said?.toolCallId)\n results.set(said.toolCallId, { output: answerText(said.content), index });\n });\n const runs: Run[] = [];\n let at = 0;\n let last = -1;\n for (const id of callIds) {\n const call = calls.get(id);\n if (!call) return undefined;\n const result = results.get(id);\n runs.push({ name: call.name, input: call.input, output: result?.output ?? '', done: !!result });\n at = Math.max(at, call.at);\n last = Math.max(last, result?.index ?? Number.POSITIVE_INFINITY);\n }\n const settled = entries.slice(last + 1).some(movedOn);\n return { runs, at, settled };\n}\n\nexport function describe(runs: readonly Run[]): string {\n return runs\n .map(\n (run) =>\n `${run.name}: ${run.input}\\n=> ${run.done ? run.output.slice(0, OUTPUT_LENGTH) : '(running)'}`\n )\n .join('\\n\\n');\n}\n"],
5
+ "mappings": "AACA,SAAS,kBAAkB;AAE3B,MAAM,gBAAgB;AAetB,SAAS,OAAO,OAA6B;AAC3C,QAAM,UAAW,MAAM,QAAQ,CAAC,GAAwB;AACxD,SAAO,MAAM,SAAS,kBAAkB,MAAM,QAAQ,OAAO,IAAK,UAAsB,CAAC;AAC3F;AAEO,SAAS,UAAU,OAAiD;AACzE,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,QAAQ;AACjB,eAAW,SAAS,KAAK;AACvB,iBAAW,SAAS,OAAO,KAAK;AAC9B,YAAI,MAAM,SAAS,cAAc,MAAM,GAAI,KAAI,IAAI,MAAM,EAAE;AACjE,SAAO;AACT;AAEA,SAAS,QAAQ,OAA6B;AAC5C,MAAI,MAAM,SAAS,UAAW,QAAO;AACrC,MAAI,MAAM,SAAS,eAAgB,QAAO;AAC1C,QAAM,OAAO,MAAM,QAAQ,CAAC;AAC5B,SACE,MAAM,eAAe,aACrB,OAAO,KAAK,EAAE,KAAK,CAAC,UAAU,MAAM,SAAS,UAAU,MAAM,MAAM,KAAK,CAAC;AAE7E;AAEO,SAAS,UACd,SACA,SACqB;AACrB,QAAM,QAAQ,oBAAI,IAA0D;AAC5E,QAAM,UAAU,oBAAI,IAA+C;AACnE,UAAQ,QAAQ,CAAC,OAAO,UAAU;AAChC,UAAM,OAAO,MAAM,QAAQ,CAAC;AAC5B,eAAW,SAAS,OAAO,KAAK;AAC9B,UAAI,MAAM,SAAS,cAAc,MAAM;AACrC,cAAM,IAAI,MAAM,IAAI;AAAA,UAClB,MAAM,MAAM,QAAQ;AAAA,UACpB,OAAO,KAAK,UAAU,MAAM,aAAa,CAAC,CAAC;AAAA,UAC3C,IAAI,MAAM,aAAa;AAAA,QACzB,CAAC;AACL,QAAI,MAAM,SAAS,oBAAoB,MAAM;AAC3C,cAAQ,IAAI,KAAK,YAAY,EAAE,QAAQ,WAAW,KAAK,OAAO,GAAG,MAAM,CAAC;AAAA,EAC5E,CAAC;AACD,QAAM,OAAc,CAAC;AACrB,MAAI,KAAK;AACT,MAAI,OAAO;AACX,aAAW,MAAM,SAAS;AACxB,UAAM,OAAO,MAAM,IAAI,EAAE;AACzB,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,SAAS,QAAQ,IAAI,EAAE;AAC7B,SAAK,KAAK,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,OAAO,QAAQ,QAAQ,UAAU,IAAI,MAAM,CAAC,CAAC,OAAO,CAAC;AAC9F,SAAK,KAAK,IAAI,IAAI,KAAK,EAAE;AACzB,WAAO,KAAK,IAAI,MAAM,QAAQ,SAAS,OAAO,iBAAiB;AAAA,EACjE;AACA,QAAM,UAAU,QAAQ,MAAM,OAAO,CAAC,EAAE,KAAK,OAAO;AACpD,SAAO,EAAE,MAAM,IAAI,QAAQ;AAC7B;AAEO,SAAS,SAAS,MAA8B;AACrD,SAAO,KACJ;AAAA,IACC,CAAC,QACC,GAAG,IAAI,IAAI,KAAK,IAAI,KAAK;AAAA,KAAQ,IAAI,OAAO,IAAI,OAAO,MAAM,GAAG,aAAa,IAAI,WAAW;AAAA,EAChG,EACC,KAAK,MAAM;AAChB;",
6
+ "names": []
7
+ }
@@ -3,14 +3,21 @@ import { extractSettings } from "../memory/extract.js";
3
3
  import { redactSecrets } from "../memory/format.js";
4
4
  import { scrubText } from "../secrets/scrub.js";
5
5
  const TITLE_PROMPT = "Give this chat a title of three to six words that says what it is about, in the language the user writes in. Answer with the title only, without quotes.";
6
+ const LABEL_PROMPT = 'Label this group of tool runs in three to eight words, as an imperative that says what they do, such as "Check the failing build logs". Answer with the label only, without quotes.';
6
7
  const TRANSCRIPT_LENGTH = 4e3;
7
- function titlesOn(text) {
8
+ function settingOn(text, key) {
8
9
  try {
9
- return JSON.parse(text ?? "{}").titles !== false;
10
+ return JSON.parse(text ?? "{}")[key] !== false;
10
11
  } catch {
11
12
  return true;
12
13
  }
13
14
  }
15
+ function titlesOn(text) {
16
+ return settingOn(text, "titles");
17
+ }
18
+ function labelsOn(text) {
19
+ return settingOn(text, "toolLabels");
20
+ }
14
21
  function settingsText(env, home, dir = `${home}/.pi/agent`) {
15
22
  return async (context) => {
16
23
  const read = await env.readTextFile(`${dir}/settings.json`, context);
@@ -23,44 +30,57 @@ function textOf(entry) {
23
30
  function transcriptOf(entries) {
24
31
  return entries.filter((entry) => entry.kind === "pi.user" || entry.kind === "pi.assistant").map((entry) => `${entry.kind === "pi.user" ? "User" : "Agent"}: ${textOf(entry)}`).join("\n");
25
32
  }
26
- async function scrubTranscript(entries) {
27
- const raw = transcriptOf(entries);
33
+ async function scrubbed(raw) {
28
34
  const redacted = redactSecrets(raw).text;
29
- const scrubbed = await scrubText(redacted);
30
- return scrubbed.slice(0, TRANSCRIPT_LENGTH);
35
+ return (await scrubText(redacted)).slice(0, TRANSCRIPT_LENGTH);
31
36
  }
32
- function titler(models, settings) {
33
- return async (transcript, model, context) => {
37
+ function scrubTranscript(entries) {
38
+ return scrubbed(transcriptOf(entries));
39
+ }
40
+ function smallModel(models, settings, key, prompt, max) {
41
+ return async (transcript, model, context, admit = () => true) => {
34
42
  const text = await settings(context);
35
- if (!titlesOn(text)) return void 0;
43
+ if (!settingOn(text, key)) return void 0;
36
44
  const chosen = extractSettings(text).model;
37
45
  const [provider, ...rest] = model.split("/");
38
46
  const ref = chosen ?? (provider && rest.length ? { provider, modelId: rest.join("/") } : null);
39
47
  const found = ref ? models.getModel(ref.provider, ref.modelId) : void 0;
40
- if (!found || !transcript) return void 0;
48
+ if (!found || !transcript || !admit()) return void 0;
41
49
  const message = await models.completeSimple(
42
50
  found,
43
51
  {
44
52
  messages: [
45
53
  {
46
54
  role: "user",
47
- content: [{ type: "text", text: `${TITLE_PROMPT}
55
+ content: [{ type: "text", text: `${prompt}
48
56
 
49
57
  ${transcript}` }],
50
58
  timestamp: Date.now()
51
59
  }
52
60
  ]
53
61
  },
54
- { maxTokens: 40 }
62
+ { maxTokens: max }
55
63
  );
56
64
  if (message.stopReason === "error") return void 0;
57
- return message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
65
+ const said = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
66
+ return { text: said, answer: message };
58
67
  };
59
68
  }
69
+ function titler(models, settings) {
70
+ const ask = smallModel(models, settings, "titles", TITLE_PROMPT, 40);
71
+ return async (transcript, model, context) => (await ask(transcript, model, context))?.text;
72
+ }
73
+ function labeler(models, settings) {
74
+ return smallModel(models, settings, "toolLabels", LABEL_PROMPT, 40);
75
+ }
60
76
  export {
77
+ LABEL_PROMPT,
61
78
  TITLE_PROMPT,
62
79
  TRANSCRIPT_LENGTH,
80
+ labeler,
81
+ labelsOn,
63
82
  scrubTranscript,
83
+ scrubbed,
64
84
  settingsText,
65
85
  titler,
66
86
  titlesOn,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/titles/generate.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Message } from '@earendil-works/pi-ai';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport type { EntryRecord } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { answerText } from '../agent.ts';\nimport { extractSettings } from '../memory/extract.ts';\nimport { redactSecrets } from '../memory/format.ts';\nimport { scrubText } from '../secrets/scrub.ts';\n\nexport const TITLE_PROMPT =\n 'Give this chat a title of three to six words that says what it is about, in the language the user writes in. Answer with the title only, without quotes.';\n\nexport const TRANSCRIPT_LENGTH = 4000;\n\nexport function titlesOn(text: string | undefined): boolean {\n try {\n return (JSON.parse(text ?? '{}') as { titles?: unknown }).titles !== false;\n } catch {\n return true;\n }\n}\n\nexport function settingsText(\n env: Pick<ExecutionEnv, 'readTextFile'>,\n home: string,\n dir = `${home}/.pi/agent`\n): (context: Context) => Promise<string | undefined> {\n return async (context) => {\n const read = await env.readTextFile(`${dir}/settings.json`, context);\n return read.ok ? read.value : undefined;\n };\n}\n\nfunction textOf(entry: EntryRecord): string {\n return answerText(entry.model?.[0]?.content).trim();\n}\n\nexport function transcriptOf(entries: readonly EntryRecord[]): string {\n return entries\n .filter((entry) => entry.kind === 'pi.user' || entry.kind === 'pi.assistant')\n .map((entry) => `${entry.kind === 'pi.user' ? 'User' : 'Agent'}: ${textOf(entry)}`)\n .join('\\n');\n}\n\nexport async function scrubTranscript(entries: readonly EntryRecord[]): Promise<string> {\n const raw = transcriptOf(entries);\n const redacted = redactSecrets(raw).text;\n const scrubbed = await scrubText(redacted);\n return scrubbed.slice(0, TRANSCRIPT_LENGTH);\n}\n\nexport function titler(\n models: Models,\n settings: (context: Context) => Promise<string | undefined>\n) {\n return async (\n transcript: string,\n model: string,\n context: Context\n ): Promise<string | undefined> => {\n const text = await settings(context);\n if (!titlesOn(text)) return undefined;\n const chosen = extractSettings(text).model;\n const [provider, ...rest] = model.split('/');\n const ref = chosen ?? (provider && rest.length ? { provider, modelId: rest.join('/') } : null);\n const found = ref ? models.getModel(ref.provider, ref.modelId) : undefined;\n if (!found || !transcript) return undefined;\n const message = await models.completeSimple(\n found,\n {\n messages: [\n {\n role: 'user',\n content: [{ type: 'text', text: `${TITLE_PROMPT}\\n\\n${transcript}` }],\n timestamp: Date.now(),\n } as Message,\n ],\n },\n { maxTokens: 40 }\n );\n if (message.stopReason === 'error') return undefined;\n return message.content\n .map((part) => (part.type === 'text' ? part.text : ''))\n .join('')\n .trim();\n };\n}\n"],
5
- "mappings": "AAKA,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAEnB,MAAM,eACX;AAEK,MAAM,oBAAoB;AAE1B,SAAS,SAAS,MAAmC;AAC1D,MAAI;AACF,WAAQ,KAAK,MAAM,QAAQ,IAAI,EAA2B,WAAW;AAAA,EACvE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,aACd,KACA,MACA,MAAM,GAAG,IAAI,cACsC;AACnD,SAAO,OAAO,YAAY;AACxB,UAAM,OAAO,MAAM,IAAI,aAAa,GAAG,GAAG,kBAAkB,OAAO;AACnE,WAAO,KAAK,KAAK,KAAK,QAAQ;AAAA,EAChC;AACF;AAEA,SAAS,OAAO,OAA4B;AAC1C,SAAO,WAAW,MAAM,QAAQ,CAAC,GAAG,OAAO,EAAE,KAAK;AACpD;AAEO,SAAS,aAAa,SAAyC;AACpE,SAAO,QACJ,OAAO,CAAC,UAAU,MAAM,SAAS,aAAa,MAAM,SAAS,cAAc,EAC3E,IAAI,CAAC,UAAU,GAAG,MAAM,SAAS,YAAY,SAAS,OAAO,KAAK,OAAO,KAAK,CAAC,EAAE,EACjF,KAAK,IAAI;AACd;AAEA,eAAsB,gBAAgB,SAAkD;AACtF,QAAM,MAAM,aAAa,OAAO;AAChC,QAAM,WAAW,cAAc,GAAG,EAAE;AACpC,QAAM,WAAW,MAAM,UAAU,QAAQ;AACzC,SAAO,SAAS,MAAM,GAAG,iBAAiB;AAC5C;AAEO,SAAS,OACd,QACA,UACA;AACA,SAAO,OACL,YACA,OACA,YACgC;AAChC,UAAM,OAAO,MAAM,SAAS,OAAO;AACnC,QAAI,CAAC,SAAS,IAAI,EAAG,QAAO;AAC5B,UAAM,SAAS,gBAAgB,IAAI,EAAE;AACrC,UAAM,CAAC,UAAU,GAAG,IAAI,IAAI,MAAM,MAAM,GAAG;AAC3C,UAAM,MAAM,WAAW,YAAY,KAAK,SAAS,EAAE,UAAU,SAAS,KAAK,KAAK,GAAG,EAAE,IAAI;AACzF,UAAM,QAAQ,MAAM,OAAO,SAAS,IAAI,UAAU,IAAI,OAAO,IAAI;AACjE,QAAI,CAAC,SAAS,CAAC,WAAY,QAAO;AAClC,UAAM,UAAU,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA;AAAA,QACE,UAAU;AAAA,UACR;AAAA,YACE,MAAM;AAAA,YACN,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,GAAG,YAAY;AAAA;AAAA,EAAO,UAAU,GAAG,CAAC;AAAA,YACpE,WAAW,KAAK,IAAI;AAAA,UACtB;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,WAAW,GAAG;AAAA,IAClB;AACA,QAAI,QAAQ,eAAe,QAAS,QAAO;AAC3C,WAAO,QAAQ,QACZ,IAAI,CAAC,SAAU,KAAK,SAAS,SAAS,KAAK,OAAO,EAAG,EACrD,KAAK,EAAE,EACP,KAAK;AAAA,EACV;AACF;",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { AssistantMessage, Message } from '@earendil-works/pi-ai';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport type { EntryRecord } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { answerText } from '../agent.ts';\nimport { extractSettings } from '../memory/extract.ts';\nimport { redactSecrets } from '../memory/format.ts';\nimport { scrubText } from '../secrets/scrub.ts';\n\nexport const TITLE_PROMPT =\n 'Give this chat a title of three to six words that says what it is about, in the language the user writes in. Answer with the title only, without quotes.';\n\nexport const LABEL_PROMPT =\n 'Label this group of tool runs in three to eight words, as an imperative that says what they do, such as \"Check the failing build logs\". Answer with the label only, without quotes.';\n\nexport const TRANSCRIPT_LENGTH = 4000;\n\ntype Switch = 'titles' | 'toolLabels';\n\nfunction settingOn(text: string | undefined, key: Switch): boolean {\n try {\n return (JSON.parse(text ?? '{}') as Record<string, unknown>)[key] !== false;\n } catch {\n return true;\n }\n}\n\nexport function titlesOn(text: string | undefined): boolean {\n return settingOn(text, 'titles');\n}\n\nexport function labelsOn(text: string | undefined): boolean {\n return settingOn(text, 'toolLabels');\n}\n\nexport function settingsText(\n env: Pick<ExecutionEnv, 'readTextFile'>,\n home: string,\n dir = `${home}/.pi/agent`\n): (context: Context) => Promise<string | undefined> {\n return async (context) => {\n const read = await env.readTextFile(`${dir}/settings.json`, context);\n return read.ok ? read.value : undefined;\n };\n}\n\nfunction textOf(entry: EntryRecord): string {\n return answerText(entry.model?.[0]?.content).trim();\n}\n\nexport function transcriptOf(entries: readonly EntryRecord[]): string {\n return entries\n .filter((entry) => entry.kind === 'pi.user' || entry.kind === 'pi.assistant')\n .map((entry) => `${entry.kind === 'pi.user' ? 'User' : 'Agent'}: ${textOf(entry)}`)\n .join('\\n');\n}\n\nexport async function scrubbed(raw: string): Promise<string> {\n const redacted = redactSecrets(raw).text;\n return (await scrubText(redacted)).slice(0, TRANSCRIPT_LENGTH);\n}\n\nexport function scrubTranscript(entries: readonly EntryRecord[]): Promise<string> {\n return scrubbed(transcriptOf(entries));\n}\n\ntype Settings = (context: Context) => Promise<string | undefined>;\n\nexport type Asked = { text: string; answer: AssistantMessage };\n\nfunction smallModel(models: Models, settings: Settings, key: Switch, prompt: string, max: number) {\n return async (\n transcript: string,\n model: string,\n context: Context,\n admit: () => boolean = () => true\n ): Promise<Asked | undefined> => {\n const text = await settings(context);\n if (!settingOn(text, key)) return undefined;\n const chosen = extractSettings(text).model;\n const [provider, ...rest] = model.split('/');\n const ref = chosen ?? (provider && rest.length ? { provider, modelId: rest.join('/') } : null);\n const found = ref ? models.getModel(ref.provider, ref.modelId) : undefined;\n if (!found || !transcript || !admit()) return undefined;\n const message = await models.completeSimple(\n found,\n {\n messages: [\n {\n role: 'user',\n content: [{ type: 'text', text: `${prompt}\\n\\n${transcript}` }],\n timestamp: Date.now(),\n } as Message,\n ],\n },\n { maxTokens: max }\n );\n if (message.stopReason === 'error') return undefined;\n const said = message.content\n .map((part) => (part.type === 'text' ? part.text : ''))\n .join('')\n .trim();\n return { text: said, answer: message };\n };\n}\n\nexport function titler(models: Models, settings: Settings) {\n const ask = smallModel(models, settings, 'titles', TITLE_PROMPT, 40);\n return async (transcript: string, model: string, context: Context) =>\n (await ask(transcript, model, context))?.text;\n}\n\nexport function labeler(models: Models, settings: Settings) {\n return smallModel(models, settings, 'toolLabels', LABEL_PROMPT, 40);\n}\n\nexport type Labeler = ReturnType<typeof labeler>;\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,218 @@
1
+ import { replicatedState } from "@earendil-works/chord";
2
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
3
+ import {
4
+ defineDoc,
5
+ UsageDoc
6
+ } from "@earendil-works/pi-durable";
7
+ import { vaultHold } from "../secrets/view.js";
8
+ import { callIdsOf, clusterOf, describe } from "./cluster.js";
9
+ import { scrubbed } from "./generate.js";
10
+ import { labelKey, shownLabel } from "./shown.js";
11
+ const LabelsDoc = defineDoc({
12
+ kind: "slicc.labels",
13
+ version: 1,
14
+ scope: "session",
15
+ initial: () => ({ labels: {} })
16
+ });
17
+ const LABELS_KEPT = 500;
18
+ const LABELS_PER_MINUTE = 6;
19
+ const MINUTE = 6e4;
20
+ const LABEL_WORDS = 8;
21
+ const LABEL_LENGTH = 80;
22
+ const RESERVED = 2;
23
+ const POLL_MS = 1e3;
24
+ const SETTLE_MS = 6e5;
25
+ const LABEL_WAIT_MS = 3e4;
26
+ function visibleLabels(labels, sources, views) {
27
+ const pick = () => {
28
+ const ids = callIdsOf(views());
29
+ return Object.fromEntries(
30
+ Object.entries(labels.value).filter(
31
+ ([key]) => key.split(" ").every((id) => ids.has(id))
32
+ )
33
+ );
34
+ };
35
+ const state = replicatedState(pick());
36
+ let queued = false;
37
+ const update = () => {
38
+ if (queued) return;
39
+ queued = true;
40
+ queueMicrotask(() => {
41
+ queued = false;
42
+ const next = pick();
43
+ const before = state.value;
44
+ const keys = Object.keys(next);
45
+ if (keys.length !== Object.keys(before).length || keys.some((key) => before[key] !== next[key]))
46
+ state.replace(BACKGROUND_CONTEXT, next);
47
+ });
48
+ };
49
+ const offs = [labels, ...sources].map((source) => source.subscribe(update));
50
+ return {
51
+ state,
52
+ dispose() {
53
+ for (const off of offs) off();
54
+ }
55
+ };
56
+ }
57
+ function watchLabel(labels, waitMs = LABEL_WAIT_MS) {
58
+ return (_agentId, callIds) => {
59
+ const found = shownLabel(labels.value, callIds);
60
+ if (found !== void 0 || !callIds.length) return Promise.resolve(found ?? null);
61
+ return new Promise((resolve) => {
62
+ const done = (label) => {
63
+ clearTimeout(timer);
64
+ off();
65
+ resolve(label);
66
+ };
67
+ const timer = setTimeout(() => done(null), waitMs);
68
+ const off = labels.subscribe(() => {
69
+ const label = shownLabel(labels.value, callIds);
70
+ if (label !== void 0) done(label);
71
+ });
72
+ });
73
+ };
74
+ }
75
+ function cleanLabel(text) {
76
+ return text.replace(/\s+/g, " ").trim().replace(/^["'“”]+|["'“”]+$/g, "").replace(/\.+$/, "").trim().split(" ").slice(0, LABEL_WORDS).join(" ").slice(0, LABEL_LENGTH);
77
+ }
78
+ async function spend(tx, conversation, answer) {
79
+ const models = (await tx.doc(UsageDoc, conversation)).models;
80
+ const key = `${answer.provider}/${answer.model}`;
81
+ const usage = answer.usage;
82
+ const total = Object.hasOwn(models, key) ? models[key] : void 0;
83
+ if (!total) {
84
+ models[key] = JSON.parse(JSON.stringify(usage));
85
+ return;
86
+ }
87
+ for (const field of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"])
88
+ total[field] += usage[field];
89
+ for (const field of ["input", "output", "cacheRead", "cacheWrite", "total"])
90
+ total.cost[field] += usage.cost[field];
91
+ }
92
+ function modelName(model) {
93
+ return model ? `${model.provider}/${model.modelId}` : "";
94
+ }
95
+ async function attachLabels(options, context = BACKGROUND_CONTEXT) {
96
+ const { harness, agents } = options;
97
+ const now = options.now ?? Date.now;
98
+ const started = now();
99
+ const cap = options.perMinute ?? LABELS_PER_MINUTE;
100
+ const loaded = await harness.commit(
101
+ async (tx) => ({ ...(await tx.doc(LabelsDoc)).labels }),
102
+ context
103
+ );
104
+ const labels = replicatedState(loaded);
105
+ const flying = /* @__PURE__ */ new Map();
106
+ const waiting = /* @__PURE__ */ new Set();
107
+ const spent = /* @__PURE__ */ new Map();
108
+ const pollMs = options.pollMs ?? POLL_MS;
109
+ const rounds = Math.ceil((options.settleMs ?? SETTLE_MS) / pollMs);
110
+ const sleep = () => new Promise((resolve) => setTimeout(resolve, pollMs));
111
+ const used = (agentId) => {
112
+ const at = now();
113
+ const recent = (spent.get(agentId) ?? []).filter((time) => at - time < MINUTE);
114
+ spent.set(agentId, recent);
115
+ return recent;
116
+ };
117
+ const admit = (agentId, settle) => () => {
118
+ const recent = used(agentId);
119
+ if (recent.length >= (settle ? cap : cap - RESERVED)) return false;
120
+ recent.push(now());
121
+ return true;
122
+ };
123
+ const room = async (agentId) => {
124
+ for (let round = 0; round < rounds && used(agentId).length >= cap; round++) await sleep();
125
+ };
126
+ const keep = (key, label, conversation, answer) => harness.commit(async (tx) => {
127
+ const doc = await tx.doc(LabelsDoc);
128
+ doc.labels[key] = label;
129
+ const keys = Object.keys(doc.labels);
130
+ for (const old of keys.slice(0, Math.max(0, keys.length - LABELS_KEPT)))
131
+ delete doc.labels[old];
132
+ await spend(tx, conversation, answer);
133
+ return { ...doc.labels };
134
+ }, BACKGROUND_CONTEXT);
135
+ const read = async (agentId, callIds) => {
136
+ const conversation = await agents.conversation(agentId, BACKGROUND_CONTEXT);
137
+ if (!conversation) return void 0;
138
+ const cluster = clusterOf((await conversation.context(BACKGROUND_CONTEXT)).entries, callIds);
139
+ return cluster ? { conversation, cluster } : void 0;
140
+ };
141
+ const ask = async (agentId, key, found, settle) => {
142
+ if (vaultHold(options.vault?.value)) return null;
143
+ if (settle) await room(agentId);
144
+ const text = await scrubbed(describe(found.cluster.runs));
145
+ const model = modelName((await found.conversation.agent(BACKGROUND_CONTEXT)).model);
146
+ const asked = await options.label(text, model, BACKGROUND_CONTEXT, admit(agentId, settle));
147
+ if (!asked) return null;
148
+ const label = cleanLabel(asked.text);
149
+ labels.replace(BACKGROUND_CONTEXT, await keep(key, label, found.conversation.id, asked.answer));
150
+ return label || null;
151
+ };
152
+ const longer = (key) => [...flying.keys(), ...waiting, ...Object.keys(labels.value)].some(
153
+ (other) => other.startsWith(`${key} `)
154
+ );
155
+ const prefixOf = (callIds) => {
156
+ for (let size = callIds.length - 1; size > 0; size--) {
157
+ const part = callIds.slice(0, size);
158
+ const key = labelKey(part);
159
+ const shown = flying.get(key) ?? shownLabel(labels.value, part);
160
+ if (shown !== void 0) return Promise.resolve(shown);
161
+ if (waiting.has(key)) return Promise.resolve(null);
162
+ }
163
+ return void 0;
164
+ };
165
+ const settled = async (agentId, callIds, key) => {
166
+ for (let round = 0; round < rounds; round++) {
167
+ await sleep();
168
+ if (longer(key)) return void 0;
169
+ const found = await read(agentId, callIds);
170
+ if (found?.cluster.settled) return found;
171
+ }
172
+ return void 0;
173
+ };
174
+ const relabel = async (agentId, callIds, key) => {
175
+ const found = await settled(agentId, callIds, key);
176
+ return found ? ask(agentId, key, found, true) : void 0;
177
+ };
178
+ const make = async (agentId, callIds, key) => {
179
+ const found = await read(agentId, callIds);
180
+ if (!found || found.cluster.at < started) return null;
181
+ const prefix = prefixOf(callIds);
182
+ if (longer(key)) return await prefix ?? null;
183
+ if (found.cluster.settled) return ask(agentId, key, found, prefix !== void 0);
184
+ if (prefix !== void 0) {
185
+ const shown = await prefix;
186
+ return await relabel(agentId, callIds, key) ?? shown;
187
+ }
188
+ const first = await ask(agentId, key, found, false);
189
+ waiting.add(key);
190
+ void relabel(agentId, callIds, key).catch(() => void 0).finally(() => waiting.delete(key));
191
+ return first;
192
+ };
193
+ return {
194
+ labels,
195
+ label(agentId, callIds, _context) {
196
+ if (!callIds.length) return Promise.resolve(null);
197
+ const key = labelKey(callIds);
198
+ const found = shownLabel(labels.value, callIds);
199
+ if (found !== void 0) return Promise.resolve(found);
200
+ const known = flying.get(key);
201
+ if (known !== void 0) return known;
202
+ const made = make(agentId, callIds, key).catch(() => null).finally(() => flying.delete(key));
203
+ flying.set(key, made);
204
+ return made;
205
+ }
206
+ };
207
+ }
208
+ export {
209
+ LABELS_KEPT,
210
+ LABELS_PER_MINUTE,
211
+ LABEL_WAIT_MS,
212
+ LabelsDoc,
213
+ attachLabels,
214
+ cleanLabel,
215
+ visibleLabels,
216
+ watchLabel
217
+ };
218
+ //# sourceMappingURL=labels.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/titles/labels.ts"],
4
+ "sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { AssistantMessage, Usage } from '@earendil-works/pi-ai';\nimport {\n type ConversationId,\n type ConversationView,\n defineDoc,\n type Harness,\n type Tx,\n UsageDoc,\n} from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { VaultState } from '../secrets/vault.ts';\nimport { vaultHold } from '../secrets/view.ts';\nimport { callIdsOf, clusterOf, describe } from './cluster.ts';\nimport { type Labeler, scrubbed } from './generate.ts';\nimport { labelKey, shownLabel } from './shown.ts';\n\nexport type LabelsState = { labels: Record<string, string> };\n\nexport const LabelsDoc = defineDoc<LabelsState>({\n kind: 'slicc.labels',\n version: 1,\n scope: 'session',\n initial: () => ({ labels: {} }),\n});\n\nexport const LABELS_KEPT = 500;\nexport const LABELS_PER_MINUTE = 6;\nconst MINUTE = 60_000;\nconst LABEL_WORDS = 8;\nconst LABEL_LENGTH = 80;\nconst RESERVED = 2;\nconst POLL_MS = 1000;\nconst SETTLE_MS = 600_000;\n\nexport const LABEL_WAIT_MS = 30_000;\n\nexport function visibleLabels(\n labels: ReplicatedState<Record<string, string>>,\n sources: readonly ReplicatedState<unknown>[],\n views: () => readonly ConversationView[]\n) {\n const pick = () => {\n const ids = callIdsOf(views());\n return Object.fromEntries(\n Object.entries(labels.value as Record<string, string>).filter(([key]) =>\n key.split(' ').every((id) => ids.has(id))\n )\n );\n };\n const state = replicatedState<Record<string, string>>(pick());\n let queued = false;\n const update = () => {\n if (queued) return;\n queued = true;\n queueMicrotask(() => {\n queued = false;\n const next = pick();\n const before = state.value;\n const keys = Object.keys(next);\n if (\n keys.length !== Object.keys(before).length ||\n keys.some((key) => before[key] !== next[key])\n )\n state.replace(BACKGROUND_CONTEXT, next);\n });\n };\n const offs = [labels, ...sources].map((source) => source.subscribe(update));\n return {\n state,\n dispose() {\n for (const off of offs) off();\n },\n };\n}\n\nexport function watchLabel(\n labels: ReplicatedState<Record<string, string>>,\n waitMs = LABEL_WAIT_MS\n) {\n return (_agentId: string, callIds: readonly string[]): Promise<string | null> => {\n const found = shownLabel(labels.value, callIds);\n if (found !== undefined || !callIds.length) return Promise.resolve(found ?? null);\n return new Promise((resolve) => {\n const done = (label: string | null) => {\n clearTimeout(timer);\n off();\n resolve(label);\n };\n const timer = setTimeout(() => done(null), waitMs);\n const off = labels.subscribe(() => {\n const label = shownLabel(labels.value, callIds);\n if (label !== undefined) done(label);\n });\n });\n };\n}\n\nexport function cleanLabel(text: string): string {\n return text\n .replace(/\\s+/g, ' ')\n .trim()\n .replace(/^[\"'\u201C\u201D]+|[\"'\u201C\u201D]+$/g, '')\n .replace(/\\.+$/, '')\n .trim()\n .split(' ')\n .slice(0, LABEL_WORDS)\n .join(' ')\n .slice(0, LABEL_LENGTH);\n}\n\ntype Counted = Record<'input' | 'output' | 'cacheRead' | 'cacheWrite' | 'totalTokens', number> & {\n cost: Record<'input' | 'output' | 'cacheRead' | 'cacheWrite' | 'total', number>;\n};\n\nasync function spend(tx: Tx, conversation: ConversationId, answer: AssistantMessage) {\n const models = (await tx.doc(UsageDoc, conversation)).models as unknown as Record<\n string,\n Counted\n >;\n const key = `${answer.provider}/${answer.model}`;\n const usage = answer.usage as Usage;\n const total = Object.hasOwn(models, key) ? models[key] : undefined;\n if (!total) {\n models[key] = JSON.parse(JSON.stringify(usage)) as Counted;\n return;\n }\n for (const field of ['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens'] as const)\n total[field] += usage[field];\n for (const field of ['input', 'output', 'cacheRead', 'cacheWrite', 'total'] as const)\n total.cost[field] += usage.cost[field];\n}\n\nexport interface LabelsAttach {\n harness: Pick<Harness, 'commit'>;\n agents: Pick<Agents, 'conversation'>;\n label: Labeler;\n vault?: Pick<ReplicatedState<VaultState>, 'value'>;\n now?: () => number;\n perMinute?: number;\n pollMs?: number;\n settleMs?: number;\n}\n\nexport interface LabelsRuntime {\n labels: ReplicatedState<Record<string, string>>;\n label(agentId: string, callIds: readonly string[], context: Context): Promise<string | null>;\n}\n\nfunction modelName(model: { provider: string; modelId: string } | undefined): string {\n return model ? `${model.provider}/${model.modelId}` : '';\n}\n\nexport async function attachLabels(\n options: LabelsAttach,\n context: Context = BACKGROUND_CONTEXT\n): Promise<LabelsRuntime> {\n const { harness, agents } = options;\n const now = options.now ?? Date.now;\n const started = now();\n const cap = options.perMinute ?? LABELS_PER_MINUTE;\n const loaded = await harness.commit(\n async (tx) => ({ ...(await tx.doc(LabelsDoc)).labels }),\n context\n );\n const labels = replicatedState<Record<string, string>>(loaded);\n const flying = new Map<string, Promise<string | null>>();\n const waiting = new Set<string>();\n const spent = new Map<string, number[]>();\n const pollMs = options.pollMs ?? POLL_MS;\n const rounds = Math.ceil((options.settleMs ?? SETTLE_MS) / pollMs);\n const sleep = () => new Promise((resolve) => setTimeout(resolve, pollMs));\n const used = (agentId: string) => {\n const at = now();\n const recent = (spent.get(agentId) ?? []).filter((time) => at - time < MINUTE);\n spent.set(agentId, recent);\n return recent;\n };\n const admit = (agentId: string, settle: boolean) => () => {\n const recent = used(agentId);\n if (recent.length >= (settle ? cap : cap - RESERVED)) return false;\n recent.push(now());\n return true;\n };\n const room = async (agentId: string) => {\n for (let round = 0; round < rounds && used(agentId).length >= cap; round++) await sleep();\n };\n const keep = (\n key: string,\n label: string,\n conversation: ConversationId,\n answer: AssistantMessage\n ) =>\n harness.commit(async (tx) => {\n const doc = await tx.doc(LabelsDoc);\n doc.labels[key] = label;\n const keys = Object.keys(doc.labels);\n for (const old of keys.slice(0, Math.max(0, keys.length - LABELS_KEPT)))\n delete doc.labels[old];\n await spend(tx, conversation, answer);\n return { ...doc.labels };\n }, BACKGROUND_CONTEXT);\n const read = async (agentId: string, callIds: readonly string[]) => {\n const conversation = await agents.conversation(agentId, BACKGROUND_CONTEXT);\n if (!conversation) return undefined;\n const cluster = clusterOf((await conversation.context(BACKGROUND_CONTEXT)).entries, callIds);\n return cluster ? { conversation, cluster } : undefined;\n };\n type Read = NonNullable<Awaited<ReturnType<typeof read>>>;\n const ask = async (agentId: string, key: string, found: Read, settle: boolean) => {\n if (vaultHold(options.vault?.value)) return null;\n if (settle) await room(agentId);\n const text = await scrubbed(describe(found.cluster.runs));\n const model = modelName((await found.conversation.agent(BACKGROUND_CONTEXT)).model);\n const asked = await options.label(text, model, BACKGROUND_CONTEXT, admit(agentId, settle));\n if (!asked) return null;\n const label = cleanLabel(asked.text);\n labels.replace(BACKGROUND_CONTEXT, await keep(key, label, found.conversation.id, asked.answer));\n return label || null;\n };\n const longer = (key: string) =>\n [...flying.keys(), ...waiting, ...Object.keys(labels.value)].some((other) =>\n other.startsWith(`${key} `)\n );\n const prefixOf = (callIds: readonly string[]) => {\n for (let size = callIds.length - 1; size > 0; size--) {\n const part = callIds.slice(0, size);\n const key = labelKey(part);\n const shown = flying.get(key) ?? shownLabel(labels.value, part);\n if (shown !== undefined) return Promise.resolve(shown);\n if (waiting.has(key)) return Promise.resolve(null);\n }\n return undefined;\n };\n const settled = async (agentId: string, callIds: readonly string[], key: string) => {\n for (let round = 0; round < rounds; round++) {\n await sleep();\n if (longer(key)) return undefined;\n const found = await read(agentId, callIds);\n if (found?.cluster.settled) return found;\n }\n return undefined;\n };\n const relabel = async (agentId: string, callIds: readonly string[], key: string) => {\n const found = await settled(agentId, callIds, key);\n return found ? ask(agentId, key, found, true) : undefined;\n };\n const make = async (agentId: string, callIds: readonly string[], key: string) => {\n const found = await read(agentId, callIds);\n if (!found || found.cluster.at < started) return null;\n const prefix = prefixOf(callIds);\n if (longer(key)) return (await prefix) ?? null;\n if (found.cluster.settled) return ask(agentId, key, found, prefix !== undefined);\n if (prefix !== undefined) {\n const shown = await prefix;\n return (await relabel(agentId, callIds, key)) ?? shown;\n }\n const first = await ask(agentId, key, found, false);\n waiting.add(key);\n void relabel(agentId, callIds, key)\n .catch(() => undefined)\n .finally(() => waiting.delete(key));\n return first;\n };\n return {\n labels,\n label(agentId, callIds, _context) {\n if (!callIds.length) return Promise.resolve(null);\n const key = labelKey(callIds);\n const found = shownLabel(labels.value, callIds);\n if (found !== undefined) return Promise.resolve(found);\n const known = flying.get(key);\n if (known !== undefined) return known;\n const made = make(agentId, callIds, key)\n .catch(() => null)\n .finally(() => flying.delete(key));\n flying.set(key, made);\n return made;\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ function labelKey(callIds) {
2
+ return callIds.join(" ");
3
+ }
4
+ function shownLabel(labels, callIds) {
5
+ const key = labelKey(callIds);
6
+ return labels && Object.hasOwn(labels, key) ? labels[key] || null : void 0;
7
+ }
8
+ export {
9
+ labelKey,
10
+ shownLabel
11
+ };
12
+ //# sourceMappingURL=shown.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/titles/shown.ts"],
4
+ "sourcesContent": ["export function labelKey(callIds: readonly string[]): string {\n return callIds.join(' ');\n}\n\nexport function shownLabel(\n labels: Readonly<Record<string, string>> | undefined,\n callIds: readonly string[]\n): string | null | undefined {\n const key = labelKey(callIds);\n return labels && Object.hasOwn(labels, key) ? labels[key] || null : undefined;\n}\n"],
5
+ "mappings": "AAAO,SAAS,SAAS,SAAoC;AAC3D,SAAO,QAAQ,KAAK,GAAG;AACzB;AAEO,SAAS,WACd,QACA,SAC2B;AAC3B,QAAM,MAAM,SAAS,OAAO;AAC5B,SAAO,UAAU,OAAO,OAAO,QAAQ,GAAG,IAAI,OAAO,GAAG,KAAK,OAAO;AACtE;",
6
+ "names": []
7
+ }
@@ -25,6 +25,7 @@ export interface AgentConnection {
25
25
  readonly changes?: ReplicatedState<ChangesView>;
26
26
  readonly monitor?: ReplicatedState<MonitorFeed>;
27
27
  readonly vault?: ReplicatedState<VaultState>;
28
+ readonly labels?: ReplicatedState<Record<string, string>>;
28
29
  readonly settings: AgentSettings;
29
30
  readonly remote?: AgentRemote;
30
31
  readonly pages?: AgentPages;
@@ -11,6 +11,7 @@ import type { SecretName } from './secrets/names.ts';
11
11
  import type { AgentControl } from './services.ts';
12
12
  import type { SkillsRuntime } from './skills/index.ts';
13
13
  import type { SprinklesRuntime } from './sprinkles/index.ts';
14
+ import type { LabelsRuntime } from './titles/labels.ts';
14
15
  import { type Uploads } from './uploads.ts';
15
16
  export interface HostLicks {
16
17
  licks: Licks;
@@ -22,4 +23,4 @@ export interface HostScoops {
22
23
  }
23
24
  export declare function readable(out: string): string;
24
25
  export declare function submissionId(value: string): SubmissionId | undefined;
25
- export declare function createAgentControl(harness: Harness, target: Conversation | Cones, deliveries?: Deliveries, licks?: HostLicks, scoops?: HostScoops, skills?: Pick<SkillsRuntime, 'expand' | 'commandsFor'>, sprinkles?: Pick<SprinklesRuntime, 'send' | 'call'>, memory?: Pick<MemoryRuntime, 'save' | 'remove'>, freezer?: Pick<FreezerRuntime, 'freeze' | 'newChat' | 'thaw' | 'discard'>, changes?: Pick<ChangesRuntime, 'open' | 'accept' | 'revert'>, monitor?: Pick<Monitor, 'open' | 'close'>, secrets?: () => readonly SecretName[], uploads?: Uploads): AgentControl;
26
+ export declare function createAgentControl(harness: Harness, target: Conversation | Cones, deliveries?: Deliveries, licks?: HostLicks, scoops?: HostScoops, skills?: Pick<SkillsRuntime, 'expand' | 'commandsFor'>, sprinkles?: Pick<SprinklesRuntime, 'send' | 'call'>, memory?: Pick<MemoryRuntime, 'save' | 'remove'>, freezer?: Pick<FreezerRuntime, 'freeze' | 'newChat' | 'thaw' | 'discard'>, changes?: Pick<ChangesRuntime, 'open' | 'accept' | 'revert'>, monitor?: Pick<Monitor, 'open' | 'close'>, secrets?: () => readonly SecretName[], uploads?: Uploads, labels?: Pick<LabelsRuntime, 'label'>): AgentControl;
@@ -14,6 +14,7 @@ import { AgentSettings } from './services.ts';
14
14
  import type { SkillsRuntime } from './skills/index.ts';
15
15
  import type { SprinklesRuntime } from './sprinkles/index.ts';
16
16
  import { type Thumbnail } from './thumbs.ts';
17
+ import { type LabelsRuntime } from './titles/labels.ts';
17
18
  import type { Uploads } from './uploads.ts';
18
19
  import type { PortEndpoint } from './wire.ts';
19
20
  export interface ConnectOptions {
@@ -51,4 +52,5 @@ export declare function hostAgent(agent: Agent, options?: {
51
52
  secrets?: HostSecrets;
52
53
  uploads?: Uploads;
53
54
  thumbnail?: Thumbnail;
55
+ labels?: LabelsRuntime;
54
56
  }): Promise<AgentHost>;
@@ -0,0 +1,36 @@
1
+ import { type Context, type ReplicatedState } from '@earendil-works/chord';
2
+ import { type Harness } from '@earendil-works/pi-durable';
3
+ import type { VaultRecords } from './records.ts';
4
+ import type { Vault } from './vault.ts';
5
+ export declare const LEGACY_DATABASE = "slicc-agent-credentials";
6
+ export type MovedAccount = {
7
+ kind: 'model';
8
+ provider: string;
9
+ label: string;
10
+ } | {
11
+ kind: 'mcp';
12
+ server: string;
13
+ label: string;
14
+ };
15
+ export type MovedState = {
16
+ accounts: MovedAccount[];
17
+ dismissed: boolean;
18
+ };
19
+ export declare const MovedDoc: import("@earendil-works/pi-durable").SessionDocToken<MovedState>;
20
+ export declare function movedAccount(key: string): MovedAccount;
21
+ export declare const WIPE_WAIT_MS = 3000;
22
+ export interface Wiped {
23
+ now: MovedAccount[];
24
+ later: Promise<MovedAccount[]>;
25
+ }
26
+ export declare function wipeLegacy(factory?: IDBFactory | undefined, wait?: number): Promise<Wiped>;
27
+ export interface KeysMoved {
28
+ readonly state: ReplicatedState<string[]>;
29
+ dismiss(context: Context): Promise<void>;
30
+ }
31
+ type Records = Pick<VaultRecords, 'state' | 'model' | 'mcp' | 'refresh'>;
32
+ export declare function keysMoved(harness: Pick<Harness, 'commit'>, sources: {
33
+ records: Records;
34
+ vault: Pick<Vault, 'connected'>;
35
+ }, wiped: Wiped, context?: Context): Promise<KeysMoved>;
36
+ export {};
@@ -87,6 +87,7 @@ export interface AgentControl {
87
87
  monitorClose(lease: string, context: Context): Promise<void>;
88
88
  image(ref: ImageRef, context: Context): Promise<StoredImage>;
89
89
  agentCommands(agentId: string, context: Context): Promise<Command[]>;
90
+ toolLabel(agentId: string, callIds: string[], context: Context): Promise<string | null>;
90
91
  }
91
92
  export interface ImageRef {
92
93
  entry: number;
@@ -247,6 +248,7 @@ export interface AgentTranscript {
247
248
  readonly changes: ReplicatedState<ChangesView>;
248
249
  readonly monitor: ReplicatedState<MonitorFeed>;
249
250
  readonly vault: ReplicatedState<VaultState>;
251
+ readonly labels: ReplicatedState<Record<string, string>>;
250
252
  }
251
253
  export interface PrivateRevision {
252
254
  revision: number;
@@ -310,11 +312,13 @@ export interface SettingsState {
310
312
  models: ModelChoice[];
311
313
  accounts: AccountState[];
312
314
  budget?: BudgetState | null;
315
+ moved?: string[];
313
316
  }
314
317
  export interface AgentSettings {
315
318
  readonly state: ReplicatedState<SettingsState>;
316
319
  connect(providerId: string, secret: string | null, fields: Record<string, string> | null, context: Context): Promise<void>;
317
320
  disconnect(providerId: string, context: Context): Promise<void>;
318
321
  signIn(providerId: string, context: Context): Promise<SignIn | McpSignIn | null>;
322
+ dismissMoved(context: Context): Promise<void>;
319
323
  }
320
324
  export declare const AgentSettings: import("@earendil-works/chord").Service<AgentSettings>;
@@ -2,6 +2,7 @@ import type { CredentialStore, Provider } from '@earendil-works/pi-ai';
2
2
  import { type MutableModels } from '@earendil-works/pi-ai/models';
3
3
  import { type Budget } from './adobe.ts';
4
4
  import type { McpAccounts } from './mcp/index.ts';
5
+ import type { KeysMoved } from './secrets/legacy.ts';
5
6
  import type { VaultRecords } from './secrets/records.ts';
6
7
  import type { PlainStore } from './secrets/store.ts';
7
8
  import type { AccountField, AgentSettings } from './services.ts';
@@ -37,4 +38,5 @@ export declare function createAgentSettings(models: MutableModels, sources: Sett
37
38
  usage?: UsageReader;
38
39
  every?: number;
39
40
  mcp?: McpAccounts;
41
+ moved?: KeysMoved;
40
42
  }): Promise<AgentSettings>;
@@ -38,6 +38,7 @@ export declare class AgentAdapter extends Emitter<AgentAdapterEvents> implements
38
38
  createScoop(parentId: string, name: string): Agent;
39
39
  drop(agentId: string): Promise<void>;
40
40
  createCone(name: string): Promise<Agent>;
41
+ toolLabel(agentId: string, callIds: readonly string[]): Promise<string | null>;
41
42
  frozen(): readonly FrozenCone[];
42
43
  thaw(id: string): Agent | null;
43
44
  discard(id: string): void;
@@ -4,6 +4,7 @@ import { AgentAdapter } from './agent-port.ts';
4
4
  import { ChangesAdapter } from './changes-port.ts';
5
5
  import { MemoryAdapter } from './memory-port.ts';
6
6
  import { MonitorAdapter } from './monitor-port.ts';
7
+ import { NoticesAdapter } from './notices-port.ts';
7
8
  import { PageAdapter, type PageOptions } from './page-port.ts';
8
9
  import { type Login, SettingsAdapter } from './settings-port.ts';
9
10
  import { SprinkleAdapter } from './sprinkle-port.ts';
@@ -13,6 +14,7 @@ export { ChangesAdapter, type ChangesEvents } from './changes-port.ts';
13
14
  export { MemoryAdapter } from './memory-port.ts';
14
15
  export { assistant, errorAction, errorPart, isBusy, queued, textOf, toMessages, } from './messages.ts';
15
16
  export { MonitorAdapter, type MonitorAdapterOptions, type MonitorInput, monitorSnapshot, } from './monitor-port.ts';
17
+ export { KEYS_MOVED, movedNotice, NoticesAdapter } from './notices-port.ts';
16
18
  export { PageAdapter, type PageDocument, type PageOptions, type PageRun, type PageTarget, } from './page-port.ts';
17
19
  export { type Login, SETTINGS_KEY, SettingsAdapter } from './settings-port.ts';
18
20
  export { SprinkleAdapter } from './sprinkle-port.ts';
@@ -32,5 +34,6 @@ export declare function createAgentModel(connection: AgentConnection, options?:
32
34
  memory: MemoryAdapter;
33
35
  changes: ChangesAdapter;
34
36
  monitor: MonitorAdapter;
37
+ notices: NoticesAdapter;
35
38
  page: PageAdapter | null;
36
39
  };