@pasko70/pibo 3.2.6 → 3.3.0

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 (37) hide show
  1. package/dist/agent-runtime/history.js +5 -16
  2. package/dist/agent-runtimes/codex-native/adapter.js +44 -14
  3. package/dist/agent-runtimes/codex-native/process.js +1 -1
  4. package/dist/agent-runtimes/codex-native/protocol-version.js +6 -6
  5. package/dist/agent-runtimes/omp/process.js +23 -3
  6. package/dist/agent-runtimes/omp/protocol-version.js +2 -0
  7. package/dist/agent-runtimes/pi/adapter.js +1 -1
  8. package/dist/agent-runtimes/pi/model-catalog.js +2 -2
  9. package/dist/agent-runtimes/pi/runtime.js +6 -3
  10. package/dist/apps/chat/chat-api-routes.js +1 -1
  11. package/dist/apps/chat/chat-request-normalizers.js +7 -2
  12. package/dist/apps/chat/data/read-state-service.js +13 -11
  13. package/dist/apps/chat/session-metadata.js +30 -0
  14. package/dist/apps/chat/trace.js +15 -2
  15. package/dist/apps/chat/web-app.js +125 -17
  16. package/dist/apps/chat-ui/assets/{dist-CLxh9uSM.js → dist-BbCjOLiw.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-B9gnHZqL.js → dist-Bd9gWCbk.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-CR75V9VE.js → dist-C9a0_446.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-BUupvbV9.js → dist-CcAkJ8iv.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-DXdSiTyu.js → dist-Kc3zbvQi.js} +1 -1
  21. package/dist/apps/chat-ui/assets/index-DWJnUye8.css +1 -0
  22. package/dist/apps/chat-ui/assets/index-exohEOUE.js +228 -0
  23. package/dist/apps/chat-ui/index.html +2 -2
  24. package/dist/core/session-router.js +12 -6
  25. package/dist/debug/agents.js +9 -3
  26. package/dist/plugins/builtin.js +2 -0
  27. package/dist/plugins/registry.js +1 -0
  28. package/dist/providers/openai-gpt56.js +41 -3
  29. package/dist/subagents/context.js +2 -2
  30. package/dist/subagents/observation-query.js +49 -6
  31. package/dist/subagents/observation-text-regex.js +124 -0
  32. package/dist/subagents/tool.js +3 -2
  33. package/dist/tools/hashline.js +64 -0
  34. package/npm-shrinkwrap.json +781 -280
  35. package/package.json +6 -5
  36. package/dist/apps/chat-ui/assets/index-BMkUGVRS.js +0 -228
  37. package/dist/apps/chat-ui/assets/index-BwT5grLv.css +0 -1
@@ -11,11 +11,11 @@
11
11
  <link rel="manifest" href="/apps/chat/manifest.webmanifest" />
12
12
  <link rel="apple-touch-icon" href="/apps/chat/assets/pwa-images/ios/180.png" />
13
13
  <title>Pibo Web Chat</title>
14
- <script type="module" crossorigin src="/apps/chat/assets/index-BMkUGVRS.js"></script>
14
+ <script type="module" crossorigin src="/apps/chat/assets/index-exohEOUE.js"></script>
15
15
  <link rel="modulepreload" crossorigin href="/apps/chat/assets/dist-CqM4On9D.js">
16
16
  <link rel="modulepreload" crossorigin href="/apps/chat/assets/dist-DLJCGrGQ.js">
17
17
  <link rel="modulepreload" crossorigin href="/apps/chat/assets/dist-S3zJGntM.js">
18
- <link rel="stylesheet" crossorigin href="/apps/chat/assets/index-BwT5grLv.css">
18
+ <link rel="stylesheet" crossorigin href="/apps/chat/assets/index-DWJnUye8.css">
19
19
  </head>
20
20
  <body>
21
21
  <div id="root"></div>
@@ -2090,12 +2090,18 @@ export class PiboSessionRouter {
2090
2090
  for (const agentId of input.agentIds ?? [])
2091
2091
  this.requireManagedAgent(parentPiboSessionId, agentId);
2092
2092
  const query = preparePiboAgentObservationQuery(input);
2093
- const ordered = query.scanOrder === "asc"
2094
- ? this.agentObservations
2095
- : [...this.agentObservations].reverse();
2096
- return selectPiboAgentObservationPage(ordered
2097
- .filter((observation) => observation.managingParentId === parentPiboSessionId)
2098
- .map(({ managingParentId: _managingParentId, ...observation }) => observation), query, { evictedThrough: this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0 });
2093
+ const observations = this.agentObservations;
2094
+ function* ordered() {
2095
+ const start = query.scanOrder === "asc" ? 0 : observations.length - 1;
2096
+ const end = query.scanOrder === "asc" ? observations.length : -1;
2097
+ const step = query.scanOrder === "asc" ? 1 : -1;
2098
+ for (let index = start; index !== end; index += step) {
2099
+ const { managingParentId, ...observation } = observations[index];
2100
+ if (managingParentId === parentPiboSessionId)
2101
+ yield observation;
2102
+ }
2103
+ }
2104
+ return selectPiboAgentObservationPage(ordered(), query, { evictedThrough: this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0 });
2099
2105
  }
2100
2106
  async killManagedAgent(parentPiboSessionId, agentId) {
2101
2107
  const child = this.requireManagedAgent(parentPiboSessionId, agentId);
@@ -47,6 +47,7 @@ export async function runDebugAgentsCli(args) {
47
47
  since: parsed.since,
48
48
  until: parsed.until,
49
49
  textContains: parsed.textContains,
50
+ textRegex: parsed.textRegex,
50
51
  afterSequence: parsed.afterSequence,
51
52
  order: parsed.order,
52
53
  limit: parsed.limit,
@@ -282,7 +283,7 @@ Filters use exact values. The command inspects only direct pibo.subagents childr
282
283
  Usage:
283
284
  pibo debug agents ${parentPiboSessionId} observe [--tool-call-id id] [--agent-id ps_...] [--name name]
284
285
  [--thread-key key] [--event-type type] [--kind message|thinking|tool|error|lifecycle|event]
285
- [--role role] [--since iso] [--until iso] [--contains text] [--after-sequence n]
286
+ [--role role] [--since iso] [--until iso] [--contains text] [--regex pattern] [--after-sequence n]
286
287
  [--order asc|desc] [--limit 1..200] [--include-tools]
287
288
  [--tool-detail summary|full] [--details] [--json]
288
289
 
@@ -290,7 +291,9 @@ Default: the newest 20 completed assistant messages, with streaming deltas and t
290
291
  Use --include-tools for compact tool calls and terminal results. Explicit --event-type or --kind
291
292
  filters retain access to progress events. Repeat plural filters for OR within that field.
292
293
  Different fields combine with AND. With --after-sequence, pages always consume the oldest unseen rows;
293
- --order desc reverses only the returned page, so nextAfterSequence remains safe for polling.`);
294
+ --order desc reverses only the returned page, so nextAfterSequence remains safe for polling.
295
+ --regex uses case-sensitive bundled rg/Rust-regex syntax; inline flags change case or multiline behavior.
296
+ Regex rejects NUL text and literal or escaped NUL patterns and requires the optional rg platform binary.`);
294
297
  }
295
298
  function parseAgentDebugOptions(args) {
296
299
  const parsed = {
@@ -350,6 +353,8 @@ function parseAgentDebugOptions(args) {
350
353
  parsed.until = value;
351
354
  else if (arg === "--contains")
352
355
  parsed.textContains = value;
356
+ else if (arg === "--regex")
357
+ parsed.textRegex = value;
353
358
  else if (arg === "--after-sequence")
354
359
  parsed.afterSequence = parseNonNegativeInteger(value, arg);
355
360
  else if (arg === "--order") {
@@ -377,7 +382,8 @@ function validateAgentDebugOptions(command, parsed) {
377
382
  if (parsed.details || parsed.includeTools || parsed.toolCallIds.length > 0 || parsed.agentIds.length > 0
378
383
  || parsed.threadKeys.length > 0 || parsed.eventTypes.length > 0 || parsed.kinds.length > 0
379
384
  || parsed.roles.length > 0 || parsed.since !== undefined || parsed.until !== undefined
380
- || parsed.textContains !== undefined || parsed.afterSequence !== undefined || parsed.order !== undefined
385
+ || parsed.textContains !== undefined || parsed.textRegex !== undefined
386
+ || parsed.afterSequence !== undefined || parsed.order !== undefined
381
387
  || parsed.limit !== undefined || parsed.toolDetail !== undefined)
382
388
  throw new Error("Unsupported option for pibo debug agents list. Run the list command with --help.");
383
389
  return;
@@ -7,6 +7,7 @@ import { parsePiboThinkingLevel } from "../core/thinking.js";
7
7
  import { createWebSearchToolProfile } from "../tools/web-search.js";
8
8
  import { CODEX_BROWSER_TOOL_NAMES, createCodexBrowserToolProfiles } from "../tools/codex-browser.js";
9
9
  import { createRuntimeToolProfile } from "../tools/runtime/tool.js";
10
+ import { createHashlineToolProfile } from "../tools/hashline.js";
10
11
  import { loadModelCatalog } from "../apps/chat/model-catalog.js";
11
12
  import { piboCodexCompatPlugin } from "./codex-compat.js";
12
13
  import { piboCodexNativePlugin } from "./codex-native.js";
@@ -213,6 +214,7 @@ export const piboCorePlugin = definePiboPlugin({
213
214
  });
214
215
  api.registerTool(createWebSearchToolProfile());
215
216
  api.registerTool(createRuntimeToolProfile());
217
+ api.registerTool(createHashlineToolProfile());
216
218
  api.registerTools(createCodexBrowserToolProfiles());
217
219
  api.registerCapabilityPackage({
218
220
  name: "codex-browser-interface",
@@ -553,6 +553,7 @@ export class PiboPluginRegistry {
553
553
  yieldable: tool.yieldable !== false,
554
554
  hasDefinition: tool.definition !== undefined || tool.createDefinition !== undefined,
555
555
  portable: toolIsPortable(tool),
556
+ ...(tool.replacesBuiltinTools?.length ? { replacesBuiltinTools: [...tool.replacesBuiltinTools] } : {}),
556
557
  pluginId: tool.pluginId,
557
558
  pluginName: tool.pluginId ? this.pluginNames.get(tool.pluginId) : undefined,
558
559
  ...(tool.providerTool ? { providerTool: tool.providerTool } : {}),
@@ -9,6 +9,8 @@ export const OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
9
9
  const OPENAI_GPT_56_CONTEXT_WINDOW = 1_050_000;
10
10
  const OPENAI_CODEX_GPT_56_CONTEXT_WINDOW = 272_000;
11
11
  const GPT_56_MAX_TOKENS = 128_000;
12
+ const OPENAI_CODEX_GPT_6_ASTRA_CONTEXT_WINDOW = 272_000;
13
+ const GPT_6_ASTRA_MAX_TOKENS = 128_000;
12
14
  export const OPENAI_GPT_56_MODELS = [
13
15
  {
14
16
  id: "gpt-5.6",
@@ -31,6 +33,11 @@ export const OPENAI_GPT_56_MODELS = [
31
33
  cost: { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25 },
32
34
  },
33
35
  ];
36
+ export const OPENAI_CODEX_GPT_6_ASTRA_MODEL = {
37
+ id: "gpt-6-astra",
38
+ name: "GPT-6-Astra",
39
+ cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
40
+ };
34
41
  const OPENAI_GPT_56_MODEL_IDS = new Set(OPENAI_GPT_56_MODELS.map((model) => model.id));
35
42
  export function getBuiltInOpenAiModels() {
36
43
  return getBuiltInProviderModels(OPENAI_PROVIDER_ID);
@@ -60,7 +67,13 @@ export function buildOpenAiCodexGpt56Models(baseModels = getBuiltInOpenAiCodexMo
60
67
  modelCost: (model) => ({ ...model.cost, cacheWrite: 0 }),
61
68
  });
62
69
  }
63
- export function registerOpenAiGpt56Models(modelRegistry, options = {}) {
70
+ export function buildOpenAiCodexSupplementalModels(baseModels = getBuiltInOpenAiCodexModels()) {
71
+ const models = buildOpenAiCodexGpt56Models(baseModels);
72
+ if (models.some((model) => model.id === OPENAI_CODEX_GPT_6_ASTRA_MODEL.id))
73
+ return models;
74
+ return [...models, openAiCodexAstraModelToRegistryModel()];
75
+ }
76
+ export function registerOpenAiSupplementalModels(modelRegistry, options = {}) {
64
77
  const baseOpenAiModels = options.baseOpenAiModels ?? getBuiltInOpenAiModels();
65
78
  const openAiModels = buildOpenAiGpt56Models(baseOpenAiModels);
66
79
  const openAiAdded = countMissingGpt56Models(baseOpenAiModels, OPENAI_PROVIDER_ID);
@@ -71,8 +84,10 @@ export function registerOpenAiGpt56Models(modelRegistry, options = {}) {
71
84
  models: openAiModels,
72
85
  });
73
86
  const baseOpenAiCodexModels = options.baseOpenAiCodexModels ?? getBuiltInOpenAiCodexModels();
74
- const openAiCodexModels = buildOpenAiCodexGpt56Models(baseOpenAiCodexModels);
75
- const openAiCodexAdded = countMissingGpt56Models(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID);
87
+ const openAiCodexModels = buildOpenAiCodexSupplementalModels(baseOpenAiCodexModels);
88
+ const hasBuiltInAstra = baseOpenAiCodexModels.some((model) => model.provider === OPENAI_CODEX_PROVIDER_ID && model.id === OPENAI_CODEX_GPT_6_ASTRA_MODEL.id);
89
+ const openAiCodexAdded = countMissingGpt56Models(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID)
90
+ + (hasBuiltInAstra ? 0 : 1);
76
91
  modelRegistry.registerProvider(OPENAI_CODEX_PROVIDER_ID, {
77
92
  baseUrl: OPENAI_CODEX_BASE_URL,
78
93
  api: OPENAI_CODEX_RESPONSES_API,
@@ -85,6 +100,7 @@ export function registerOpenAiGpt56Models(modelRegistry, options = {}) {
85
100
  added: openAiAdded + openAiCodexAdded,
86
101
  };
87
102
  }
103
+ export const registerOpenAiGpt56Models = registerOpenAiSupplementalModels;
88
104
  export function findOpenAiGpt56Model(modelRegistry, model) {
89
105
  if (!model?.provider || !model.id)
90
106
  return undefined;
@@ -140,3 +156,25 @@ function openAiGpt56ModelToRegistryModel(model, options) {
140
156
  maxTokens: GPT_56_MAX_TOKENS,
141
157
  };
142
158
  }
159
+ function openAiCodexAstraModelToRegistryModel() {
160
+ return {
161
+ id: OPENAI_CODEX_GPT_6_ASTRA_MODEL.id,
162
+ name: OPENAI_CODEX_GPT_6_ASTRA_MODEL.name,
163
+ api: OPENAI_CODEX_RESPONSES_API,
164
+ provider: OPENAI_CODEX_PROVIDER_ID,
165
+ baseUrl: OPENAI_CODEX_BASE_URL,
166
+ reasoning: true,
167
+ // Codex also advertises "ultra", but Pibo's portable thinking-level contract ends at "max".
168
+ thinkingLevelMap: { off: null, minimal: null, xhigh: "xhigh", max: "max" },
169
+ input: ["text", "image"],
170
+ compat: {
171
+ supportsOpenAIGrammarTools: true,
172
+ supportsAdditionalTools: true,
173
+ supportsToolSearch: true,
174
+ },
175
+ cost: { ...OPENAI_CODEX_GPT_6_ASTRA_MODEL.cost, cacheWrite: 0 },
176
+ // Codex uses 272k by default and advertises 872k only as an optional maximum override.
177
+ contextWindow: OPENAI_CODEX_GPT_6_ASTRA_CONTEXT_WINDOW,
178
+ maxTokens: GPT_6_ASTRA_MAX_TOKENS,
179
+ };
180
+ }
@@ -33,14 +33,14 @@ export function getDelegatedAgentContextFile(subagents) {
33
33
  "",
34
34
  "pibo_run_wait({ runId, timeoutMs? }) # bounded wait only; expiry does not stop the child",
35
35
  "pibo_run_status({ runId }) # compact lifecycle state",
36
- "pibo_agents_observe({ requestIds?: [runId], afterSequence?, limit?, includeTools?, toolDetail?, ... })",
36
+ "pibo_agents_observe({ requestIds?: [runId], textContains?, textRegex?, afterSequence?, limit?, includeTools?, toolDetail?, ... })",
37
37
  "pibo_run_read({ runId }) # terminal result, including the complete final agent message",
38
38
  "pibo_run_cancel({ runId }) # explicit request cancellation",
39
39
  "pibo_agents_list_agents({}) # available definitions and persistent child instances",
40
40
  "pibo_agents_kill({ agentId }) # terminate one persistent child session subtree",
41
41
  "```",
42
42
  "",
43
- "Set `sessionName` on every send to a nonblank human-readable child title of at most 40 Unicode code points. Pibo trims surrounding whitespace and rejects missing, blank, non-string, or oversized names before creating a yielded run or child session. Reuse a stable `threadKey` to continue the same child Pibo Session; a new `sessionName` updates its title without changing identity. A wait timeout is only an orchestrator wake-up. Observe defaults to the newest 20 completed assistant messages with streaming deltas, duplicate tool progress events, and tools hidden. Set `includeTools: true` for compact tool call/result summaries, `toolDetail: \"full\"` only for bounded diagnostics, and `limit: 50` when a larger page is necessary. Use `afterSequence` from the prior result for polling.",
43
+ "Set `sessionName` on every send to a nonblank human-readable child title of at most 40 Unicode code points. Pibo trims surrounding whitespace and rejects missing, blank, non-string, or oversized names before creating a yielded run or child session. Reuse a stable `threadKey` to continue the same child Pibo Session; a new `sessionName` updates its title without changing identity. A wait timeout is only an orchestrator wake-up. Observe defaults to the newest 20 completed assistant messages with streaming deltas, duplicate tool progress events, and tools hidden. Use `textContains` for case-insensitive substring matching or `textRegex` for case-sensitive rg/Rust-regex matching; both must match when supplied together. Set `includeTools: true` for compact tool call/result summaries, `toolDetail: \"full\"` only for bounded diagnostics, and `limit: 50` when a larger page is necessary. Use `afterSequence` from the prior result for polling.",
44
44
  "",
45
45
  "Observe progress and decide whether to continue waiting, steer through a new message after the current turn, cancel the request, or kill the child session.",
46
46
  "",
@@ -1,4 +1,5 @@
1
1
  import { PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES, normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, normalizePiboAgentObservationToolDetail, parsePiboAgentObservationTimestamp, piboAgentObservationKind, piboAgentObservationToolSummary, } from "./observations.js";
2
+ import { PIBO_AGENT_TEXT_REGEX_BATCH_MAX_ITEMS, PIBO_AGENT_TEXT_REGEX_BATCH_TARGET_BYTES, matchPiboAgentObservationTextRegex, preparePiboAgentObservationTextRegex, } from "./observation-text-regex.js";
2
3
  export function preparePiboAgentObservationQuery(input = {}) {
3
4
  const order = normalizePiboAgentObservationOrder(input.order);
4
5
  const limit = normalizePiboAgentObservationLimit(input.limit);
@@ -21,6 +22,7 @@ export function preparePiboAgentObservationQuery(input = {}) {
21
22
  const kinds = input.kinds ? new Set(input.kinds) : undefined;
22
23
  const roles = input.roles ? new Set(input.roles) : undefined;
23
24
  const textContains = input.textContains?.toLowerCase();
25
+ const textRegex = preparePiboAgentObservationTextRegex(input.textRegex);
24
26
  const defaultMessageView = eventTypes === undefined && kinds === undefined;
25
27
  const explicitlySelectsTools = input.eventTypes?.some((eventType) => piboAgentObservationKind(eventType) === "tool") === true
26
28
  || input.kinds?.includes("tool") === true
@@ -53,6 +55,7 @@ export function preparePiboAgentObservationQuery(input = {}) {
53
55
  includeTools,
54
56
  toolDetail,
55
57
  ...(scanEventTypes ? { scanEventTypes } : {}),
58
+ ...(textRegex ? { textRegex } : {}),
56
59
  matches(observation) {
57
60
  if (requestIds && (!observation.requestId || !requestIds.has(observation.requestId)))
58
61
  return false;
@@ -89,12 +92,52 @@ export function preparePiboAgentObservationQuery(input = {}) {
89
92
  }
90
93
  export function selectPiboAgentObservationPage(observations, query, options = {}) {
91
94
  const matches = [];
92
- for (const observation of observations) {
93
- if (!query.matches(observation))
94
- continue;
95
- matches.push(observation);
96
- if (matches.length > query.limit)
97
- break;
95
+ if (query.textRegex) {
96
+ let candidates = [];
97
+ let candidateBytes = 0;
98
+ const matchCandidates = () => {
99
+ const regexMatches = matchPiboAgentObservationTextRegex(query.textRegex, candidates.map((observation) => observation.text ?? ""));
100
+ for (let index = 0; index < candidates.length; index += 1) {
101
+ if (!regexMatches[index])
102
+ continue;
103
+ matches.push(candidates[index]);
104
+ if (matches.length > query.limit) {
105
+ candidates = [];
106
+ candidateBytes = 0;
107
+ return true;
108
+ }
109
+ }
110
+ candidates = [];
111
+ candidateBytes = 0;
112
+ return false;
113
+ };
114
+ for (const observation of observations) {
115
+ if (!query.matches(observation))
116
+ continue;
117
+ const textBytes = Buffer.byteLength(observation.text ?? "", "utf8");
118
+ if (candidates.length > 0
119
+ && candidateBytes + textBytes > PIBO_AGENT_TEXT_REGEX_BATCH_TARGET_BYTES
120
+ && matchCandidates())
121
+ break;
122
+ candidates.push(observation);
123
+ candidateBytes += textBytes;
124
+ if (candidates.length >= PIBO_AGENT_TEXT_REGEX_BATCH_MAX_ITEMS
125
+ || candidateBytes >= PIBO_AGENT_TEXT_REGEX_BATCH_TARGET_BYTES) {
126
+ if (matchCandidates())
127
+ break;
128
+ }
129
+ }
130
+ if (matches.length <= query.limit && candidates.length > 0)
131
+ matchCandidates();
132
+ }
133
+ else {
134
+ for (const observation of observations) {
135
+ if (!query.matches(observation))
136
+ continue;
137
+ matches.push(observation);
138
+ if (matches.length > query.limit)
139
+ break;
140
+ }
98
141
  }
99
142
  const pageLimited = matches.length > query.limit;
100
143
  const selected = matches.slice(0, query.limit);
@@ -0,0 +1,124 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ export const PIBO_AGENT_TEXT_REGEX_BATCH_MAX_ITEMS = 128;
7
+ export const PIBO_AGENT_TEXT_REGEX_BATCH_TARGET_BYTES = 64 * 1024;
8
+ const PIBO_AGENT_TEXT_REGEX_MAX_OUTPUT_BYTES = 64 * 1024;
9
+ const requireFromHere = createRequire(import.meta.url);
10
+ function escapedNulIndex(pattern) {
11
+ for (let index = 0; index < pattern.length; index += 1) {
12
+ if (pattern[index] !== "\\")
13
+ continue;
14
+ let slashEnd = index;
15
+ while (pattern[slashEnd + 1] === "\\")
16
+ slashEnd += 1;
17
+ const slashCount = slashEnd - index + 1;
18
+ if (slashCount % 2 === 1) {
19
+ const remainder = pattern.slice(slashEnd + 1);
20
+ if (/^(?:x00|x\{0+\}|u\{0+\})/i.test(remainder))
21
+ return index;
22
+ }
23
+ index = slashEnd;
24
+ }
25
+ return undefined;
26
+ }
27
+ function assertSupportedNulPattern(pattern) {
28
+ if (pattern.includes("\0") || escapedNulIndex(pattern) !== undefined) {
29
+ throw new Error("Agent observation textRegex is invalid: matching NUL bytes is not supported.");
30
+ }
31
+ }
32
+ function resolvePiboAgentObservationRipgrepPath() {
33
+ const arch = process.env.npm_config_arch || process.arch;
34
+ const binaryName = process.platform === "win32" ? "rg.exe" : "rg";
35
+ const platformPackage = `@vscode/ripgrep-${process.platform}-${arch}`;
36
+ try {
37
+ const wrapperPath = requireFromHere.resolve("@vscode/ripgrep");
38
+ return createRequire(wrapperPath).resolve(`${platformPackage}/bin/${binaryName}`);
39
+ }
40
+ catch {
41
+ throw new Error(`Agent observation textRegex is unavailable: ${platformPackage} is not installed for this platform.`);
42
+ }
43
+ }
44
+ function ripgrepErrorReason(stderr) {
45
+ if (stderr.includes("pattern contains \"\\0\""))
46
+ return "matching NUL bytes is not supported";
47
+ const reasons = [...stderr.matchAll(/^error:\s*(.+)$/gm)];
48
+ return reasons.at(-1)?.[1]?.trim().replace(/\.$/, "");
49
+ }
50
+ function runRipgrepTextRegex(prepared, args, options = {}) {
51
+ return spawnSync(prepared.rgPath, [
52
+ "--no-config",
53
+ "--color=never",
54
+ "--null-data",
55
+ ...args,
56
+ ], {
57
+ ...options,
58
+ encoding: "utf8",
59
+ maxBuffer: PIBO_AGENT_TEXT_REGEX_MAX_OUTPUT_BYTES,
60
+ });
61
+ }
62
+ function throwRipgrepExecutionError(action, error) {
63
+ const errorCode = error && "code" in error ? error.code : undefined;
64
+ if (errorCode === "ENOENT" || errorCode === "EACCES") {
65
+ throw new Error("Agent observation textRegex is unavailable: bundled rg could not be executed.");
66
+ }
67
+ const detail = errorCode === "ENOBUFS"
68
+ ? "bundled rg output exceeded the bounded observation batch limit"
69
+ : "bundled rg could not run";
70
+ throw new Error(`Agent observation textRegex ${action} failed: ${detail}.`);
71
+ }
72
+ export function preparePiboAgentObservationTextRegex(pattern) {
73
+ if (pattern === undefined)
74
+ return undefined;
75
+ assertSupportedNulPattern(pattern);
76
+ const prepared = { pattern, rgPath: resolvePiboAgentObservationRipgrepPath() };
77
+ const result = runRipgrepTextRegex(prepared, ["--quiet", "--", pattern, "-"], { input: "" });
78
+ if (result.error)
79
+ throwRipgrepExecutionError("validation", result.error);
80
+ if (result.status === 0 || result.status === 1)
81
+ return prepared;
82
+ const reason = ripgrepErrorReason(result.stderr);
83
+ if (reason)
84
+ throw new Error(`Agent observation textRegex is invalid: ${reason}.`);
85
+ throwRipgrepExecutionError("validation");
86
+ }
87
+ export function matchPiboAgentObservationTextRegex(prepared, texts) {
88
+ for (const text of texts) {
89
+ if (text.includes("\0")) {
90
+ throw new Error("Agent observation textRegex cannot match observation text containing NUL bytes.");
91
+ }
92
+ }
93
+ if (texts.length === 0)
94
+ return [];
95
+ // One private file per observation preserves record boundaries. --files-with-matches
96
+ // emits each fixed filename at most once, so output cannot grow with submatch count.
97
+ const directory = mkdtempSync(join(tmpdir(), "pibo-agent-observe-regex-"));
98
+ const filenames = texts.map((_, index) => index.toString().padStart(6, "0"));
99
+ try {
100
+ for (let index = 0; index < texts.length; index += 1) {
101
+ writeFileSync(join(directory, filenames[index]), `${texts[index]}\0`, {
102
+ encoding: "utf8",
103
+ flag: "wx",
104
+ mode: 0o600,
105
+ });
106
+ }
107
+ const result = runRipgrepTextRegex(prepared, ["--files-with-matches", "--null", "--", prepared.pattern, ...filenames], { cwd: directory });
108
+ if (result.error)
109
+ throwRipgrepExecutionError("matching", result.error);
110
+ if (result.status === 1)
111
+ return texts.map(() => false);
112
+ if (result.status !== 0) {
113
+ const reason = ripgrepErrorReason(result.stderr);
114
+ if (reason)
115
+ throw new Error(`Agent observation textRegex is invalid: ${reason}.`);
116
+ throwRipgrepExecutionError("matching");
117
+ }
118
+ const matched = new Set(result.stdout.split("\0").filter(Boolean));
119
+ return filenames.map((filename) => matched.has(filename));
120
+ }
121
+ finally {
122
+ rmSync(directory, { recursive: true, force: true });
123
+ }
124
+ }
@@ -187,11 +187,11 @@ export function createAgentToolDefinitions(subagents, controller) {
187
187
  name: "pibo_agents_observe",
188
188
  title: "Pibo Agents Observe",
189
189
  description: [
190
- "Read completed delegated-agent messages with bounded cursor, identity, event, time, text, order, and limit filters.",
190
+ "Read completed delegated-agent messages with bounded cursor, identity, event, time, substring, regex, order, and limit filters.",
191
191
  "Default: the newest 20 completed assistant messages, with streaming deltas and tools hidden.",
192
192
  "Set includeTools=true to add compact tool calls and terminal results, or pass toolCallIds to retrieve only those exact tool observations. Set toolDetail=full only for bounded diagnostic inspection.",
193
193
  ].join("\n"),
194
- promptSnippet: "Observe child-agent progress through completed assistant messages. Defaults: newest 20 messages, no streaming deltas, no duplicate tool progress events, no tools. Set includeTools=true for compact tool call/result summaries, pass up to 50 exact toolCallIds to retrieve only those tool observations, use toolDetail=full for bounded raw tool text, or eventTypes/kinds for explicit progress diagnostics. Compact tool entries expose their toolCallId for follow-up queries. Pass afterSequence from the previous result for cursor polling; cursor pages consume the oldest unseen matches even when order is desc.",
194
+ promptSnippet: "Observe child-agent progress through completed assistant messages. Defaults: newest 20 messages, no streaming deltas, no duplicate tool progress events, no tools. Use textContains for case-insensitive substring matching or textRegex for case-sensitive rg/Rust-regex matching; when both are present, both must match. Set includeTools=true for compact tool call/result summaries, pass up to 50 exact toolCallIds to retrieve only those tool observations, use toolDetail=full for bounded raw tool text, or eventTypes/kinds for explicit progress diagnostics. Compact tool entries expose their toolCallId for follow-up queries. Pass afterSequence from the previous result for cursor polling; cursor pages consume the oldest unseen matches even when order is desc.",
195
195
  executionMode: "parallel",
196
196
  annotations: { readOnly: true },
197
197
  inputSchema: Type.Object({
@@ -206,6 +206,7 @@ export function createAgentToolDefinitions(subagents, controller) {
206
206
  since: Type.Optional(Type.String({ description: "Inclusive ISO-8601 lower timestamp bound" })),
207
207
  until: Type.Optional(Type.String({ description: "Inclusive ISO-8601 upper timestamp bound" })),
208
208
  textContains: Type.Optional(Type.String({ description: "Case-insensitive substring match against normalized observation text" })),
209
+ textRegex: Type.Optional(Type.String({ description: "Case-sensitive rg/Rust-regex match against normalized observation text. Use inline flags such as (?i) to change case behavior. Combines with textContains using AND semantics. NUL text and literal or escaped NUL patterns are rejected; regex use requires the optional rg platform binary." })),
209
210
  afterSequence: Type.Optional(Type.Integer({ description: "Exclusive live observation cursor. Cursor pages consume the oldest unseen matches; desc reverses only the returned page.", minimum: 0 })),
210
211
  order: Type.Optional(piboStringEnum(["asc", "desc"], { default: "desc", description: "Newest first by default when no cursor is supplied" })),
211
212
  limit: Type.Optional(Type.Integer({ description: "Maximum completed messages or activity records to return. Use 50 explicitly when needed.", minimum: 1, maximum: 200, default: 20 })),
@@ -0,0 +1,64 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadToolDefinition, } from "@earendil-works/pi-coding-agent";
3
+ export const HASHLINE_TOOL_NAME = "hashline";
4
+ export const HASHLINE_REPLACED_BUILTIN_TOOLS = ["read"];
5
+ const HASH_ALPHABET = "ZPMQVRWSNKTXJBYH";
6
+ const CONTINUATION_NOTICE_RE = /\n\n(\[(?:Showing lines \d+-\d+ of \d+(?: \([^)]+ limit\))?\. Use offset=\d+ to continue\.|\d+ more lines in file\. Use offset=\d+ to continue\.)\])$/;
7
+ export function hashLineContent(line) {
8
+ const normalized = line.endsWith("\r") ? line.slice(0, -1) : line;
9
+ const byte = createHash("sha256").update(normalized, "utf8").digest()[0];
10
+ return `${HASH_ALPHABET[(byte >>> 4) & 0x0f]}${HASH_ALPHABET[byte & 0x0f]}`;
11
+ }
12
+ export function formatHashlineReadText(text, offset = 1, details) {
13
+ if (details?.truncation?.firstLineExceedsLimit || text.startsWith("Read image file ["))
14
+ return text;
15
+ if (text.length === 0)
16
+ return "File is empty.";
17
+ const continuation = text.match(CONTINUATION_NOTICE_RE);
18
+ const body = continuation ? text.slice(0, -continuation[0].length) : text;
19
+ const notice = continuation?.[1];
20
+ const lines = body.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
21
+ const firstLine = Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 1;
22
+ const width = String(firstLine + lines.length - 1).length;
23
+ const formatted = lines.map((line, index) => {
24
+ const lineNumber = String(firstLine + index).padStart(width, " ");
25
+ return `${lineNumber}#${hashLineContent(line)}:${line}`;
26
+ }).join("\n");
27
+ return notice ? `${formatted}\n\n${notice}` : formatted;
28
+ }
29
+ export function createHashlineToolDefinition(cwd) {
30
+ const read = createReadToolDefinition(cwd);
31
+ return {
32
+ ...read,
33
+ name: HASHLINE_TOOL_NAME,
34
+ label: HASHLINE_TOOL_NAME,
35
+ description: "Read workspace files with short content hashes on every text line. Text output uses LINE#HASH:CONTENT anchors; images remain attachments. Use offset and limit for large files.",
36
+ promptSnippet: "Read file contents with LINE#HASH anchors",
37
+ promptGuidelines: [
38
+ "Use hashline instead of read, cat, or sed when examining files.",
39
+ "Treat the LINE#HASH prefix as metadata; the text after the first colon is the file content.",
40
+ ],
41
+ async execute(toolCallId, input, signal, onUpdate, context) {
42
+ const result = await read.execute(toolCallId, input, signal, onUpdate, context);
43
+ if (result.content.some((item) => item.type === "image"))
44
+ return result;
45
+ return {
46
+ ...result,
47
+ content: result.content.map((item) => item.type === "text"
48
+ ? { ...item, text: formatHashlineReadText(item.text, input.offset, result.details) }
49
+ : item),
50
+ };
51
+ },
52
+ };
53
+ }
54
+ export function createHashlineToolProfile() {
55
+ return {
56
+ name: HASHLINE_TOOL_NAME,
57
+ description: "Pi-only read replacement that prefixes every text line with a short content hash.",
58
+ yieldable: false,
59
+ replacesBuiltinTools: HASHLINE_REPLACED_BUILTIN_TOOLS,
60
+ createDefinition(context) {
61
+ return createHashlineToolDefinition(context.cwd ?? process.cwd());
62
+ },
63
+ };
64
+ }