@bytesbrains/pi-tool-awareness-gate 1.0.3

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/AGENTS.md ADDED
@@ -0,0 +1,36 @@
1
+ # Tool Awareness Gate — Agent Instructions
2
+
3
+ This gate operates transparently. No special agent instructions needed — it injects limitation signals directly into tool result context.
4
+
5
+ ## How Agents Should Use Awareness Signals
6
+
7
+ When you see a tool result prefixed with a limitation reminder:
8
+
9
+ ⚠️ [bash] conf:0% partial truncated
10
+ • Non-zero exit code: 1 • Output was truncated
11
+ → Check stderr output for error details before retrying
12
+
13
+ **Do:**
14
+ - Read the status icon (✅ success, ⚠️ partial, ❌ failure, ⏱️ timeout, 🔒 unauthorized)
15
+ - Check `conf:N%` — confidence below 50% means the result may be unreliable
16
+ - Read warnings — they explain what went wrong
17
+ - Follow suggestions — they provide corrective actions
18
+ - For `partial` results: consider retrying or narrowing scope
19
+ - For `failure` results: do not treat the output as valid
20
+
21
+ **Don't:**
22
+ - Ignore the reminder and treat the result as authoritative
23
+ - Proceed with downstream actions based on partial/failed results without verification
24
+ - Ignore truncation warnings — you may be missing critical data
25
+
26
+ ## Rich Payload Access
27
+
28
+ The full envelope is available in `details._awareness` for programmatic inspection. Agents that need to make meta-decisions (retry? escalate? ask user?) should inspect:
29
+
30
+ ```typescript
31
+ details._awareness.status // "success" | "partial" | "failure" | "timeout" | "unauthorized"
32
+ details._awareness.quality // { confidence, completeness, freshness, accuracy }
33
+ details._awareness.limitations // { truncated, stale, limited_scope, requires_human_review, ... }
34
+ details._awareness.warnings // string[]
35
+ details._awareness.suggestions // string[] (optional)
36
+ ```
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # Tool Awareness Gate for Pi
2
+
3
+ > Intercepts every tool result, infers quality signals, injects limitation reminders into agent context, and logs structured envelopes for evaluation. **Zero code changes required in other gates.**
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:pi-tool-awareness-gate
9
+ ```
10
+
11
+ Or add to `.pi/settings.json`:
12
+
13
+ ```json
14
+ {
15
+ "packages": ["npm:pi-tool-awareness-gate"]
16
+ }
17
+ ```
18
+
19
+ ## What It Does
20
+
21
+ Every tool your agent calls — bash, read, write, edit, browser, CI, doctor — returns a result. But how reliable is that result? The awareness gate intercepts every `tool_result` event and adds:
22
+
23
+ | Signal | What It Tells the Agent |
24
+ |---|---|
25
+ | **Status** | `success`, `partial`, `failure`, `timeout`, `unauthorized` |
26
+ | **Confidence** | 0-1 score — how reliable is this result? |
27
+ | **Completeness** | `full`, `partial`, `minimal` — did we get everything? |
28
+ | **Truncation** | Was output cut off? |
29
+ | **Freshness** | How stale is the data? |
30
+ | **Warnings** | Human + agent readable caveats |
31
+ | **Suggestions** | Actionable next steps ("retry with narrower scope") |
32
+
33
+ ## How It Works
34
+
35
+ ```
36
+ Tool executes → tool_result event fires
37
+
38
+
39
+ ┌─────────────────────────────┐
40
+ │ 1. Infer quality signals │ ← heuristic analysis of raw output
41
+ │ 2. Detect limitation flags │ ← truncated? stale? scoped?
42
+ │ 3. Generate warnings │ ← "Output was truncated"
43
+ │ 4. Format short reminder │ ← 1-2 line summary
44
+ │ 5. Inject into agent context│ ← prepended to result text
45
+ │ 6. Log rich payload │ ← .awareness/envelopes.log
46
+ └─────────────────────────────┘
47
+ ```
48
+
49
+ ### Short Reminder Example
50
+
51
+ When a tool result has issues, the agent sees:
52
+
53
+ ```
54
+ ⚠️ [bash] conf:0% partial truncated
55
+ • Non-zero exit code: 1 • Output was truncated
56
+ → Check stderr output for error details before retrying
57
+
58
+ $ ls /nonexistent
59
+ ls: /nonexistent: No such file or directory
60
+ ```
61
+
62
+ ### Rich Payload Example
63
+
64
+ Logged to `.awareness/envelopes.log`:
65
+
66
+ ```json
67
+ {
68
+ "timestamp": "2026-05-15T10:30:00.000Z",
69
+ "tool": "bash",
70
+ "invocation_id": "bash-42",
71
+ "status": "partial",
72
+ "latency_ms": 45,
73
+ "quality": {
74
+ "confidence": 0.8,
75
+ "completeness": "full",
76
+ "freshness": 0.5,
77
+ "accuracy": 0.8
78
+ },
79
+ "limitations": { "truncated": false },
80
+ "warnings": [],
81
+ "suggestions": []
82
+ }
83
+ ```
84
+
85
+ ## Tool-Specific Inference
86
+
87
+ The gate has tool-specific heuristics for richer signals:
88
+
89
+ | Tool | Extra Signals Detected |
90
+ |---|---|
91
+ | `bash` | Exit code, stderr presence, timeout |
92
+ | `read` | Truncation at limit, offset/limit awareness |
93
+ | `write`/`edit` | Bytes written vs expected |
94
+ | `browser_*` | HTTP errors, OCR failures, timeouts |
95
+ | `ci_*` | API errors, Gitea connectivity issues |
96
+ | `doctor_*` | Informational — no warnings |
97
+
98
+ ## Configuration
99
+
100
+ Create `.awarenessrc.yml` in your project root (optional — sensible defaults):
101
+
102
+ ```yaml
103
+ # Enable/disable the awareness layer
104
+ enabled: true
105
+
106
+ # Inject short reminders into agent context
107
+ injectReminders: true
108
+
109
+ # Log rich payloads for evaluation
110
+ logEnvelopes: true
111
+
112
+ # Where to write envelope logs
113
+ envelopeLogPath: .awareness/envelopes.log
114
+
115
+ # Max warnings to include per short reminder (before truncation)
116
+ maxWarningsInReminder: 3
117
+
118
+ # Quality thresholds
119
+ thresholds.lowConfidence: 0.5
120
+ thresholds.staleFreshness: 0.3
121
+ thresholds.maxWarningsBeforeCritical: 5
122
+
123
+ # Tools to skip awareness tracking (comma-separated)
124
+ excludedTools:
125
+ ```
126
+
127
+ ## Architecture
128
+
129
+ ```
130
+ tool-awareness-gate/
131
+ ├── package.json
132
+ ├── src/
133
+ │ ├── index.ts ← Hooks pi.on("tool_result"), main orchestration
134
+ │ ├── types.ts ← ToolResultEnvelope<T>, QualitySignals, LimitationFlags
135
+ │ ├── infer.ts ← Heuristic inference from raw tool outputs
136
+ │ ├── format.ts ← Short reminder + rich payload formatters
137
+ │ ├── config.ts ← .awarenessrc.yml loader
138
+ │ └── helpers.ts ← Invocation IDs, text extraction, log appending
139
+ └── README.md
140
+ ```
141
+
142
+ ## Integration with Other Gates
143
+
144
+ **No code changes needed.** The awareness gate works at the framework level, intercepting events from all gates automatically.
145
+
146
+ When a gate wants to provide domain-specific quality signals (richer than what inference can detect), it imports the envelope types:
147
+
148
+ ```typescript
149
+ import type { ToolResultEnvelope } from "pi-tool-awareness-gate/src/types";
150
+
151
+ // Gate enriches its own result with domain-specific quality signals
152
+ return {
153
+ content: [...],
154
+ details: {
155
+ ...details,
156
+ _awareness: {
157
+ quality: { freshness: 0.95 }, // CI just fetched live data
158
+ limitations: { truncated: false },
159
+ },
160
+ },
161
+ };
162
+ ```
163
+
164
+ The awareness gate will **merge** gate-provided signals with its own inference, preferring the gate's signals when available.
165
+
166
+ ## Benefits
167
+
168
+ - **More trustworthy agents** — agents know when to doubt results
169
+ - **Better debugging** — structured logs show tool quality over time
170
+ - **Reduced overconfidence** — agents see explicit confidence scores
171
+ - **Progressive adoption** — works automatically, gates enrich when ready
172
+ - **Zero breaking changes** — no modifications to existing tool code
173
+
174
+ ## License
175
+
176
+ MIT © [nandal](https://github.com/nandal)
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@bytesbrains/pi-tool-awareness-gate",
3
+ "version": "1.0.3",
4
+ "description": "Tool limitation awareness layer for AI agents — intercepts tool results, infers quality signals, injects limitation reminders, and logs structured envelopes for evaluation.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "tool-awareness",
9
+ "quality-signals",
10
+ "limitation-detection",
11
+ "agent-protocol",
12
+ "observability"
13
+ ],
14
+ "author": "nandal <nandal@users.noreply.github.com>",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/nandal/pi-ext",
18
+ "directory": "tool-awareness-gate"
19
+ },
20
+ "homepage": "https://github.com/nandal/pi-ext/tree/main/tool-awareness-gate",
21
+ "bugs": {
22
+ "url": "https://github.com/nandal/pi-ext/issues"
23
+ },
24
+ "license": "MIT",
25
+ "main": "./src/index.ts",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "files": [
30
+ "src/",
31
+ "README.md",
32
+ "AGENTS.md",
33
+ "LICENSE"
34
+ ],
35
+ "peerDependencies": {
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "typebox": "*"
38
+ },
39
+ "pi": {
40
+ "extensions": [
41
+ "./src/index.ts"
42
+ ]
43
+ },
44
+ "scripts": {
45
+ "test": "vitest run",
46
+ "test:watch": "vitest"
47
+ },
48
+ "devDependencies": {
49
+ "vitest": "^2.0.0"
50
+ }
51
+ }
package/src/config.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * pi-tool-awareness-gate — Config
3
+ *
4
+ * Loads .awarenessrc.yml from project root.
5
+ * Falls back to sensible defaults if not present.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import type { AwarenessConfig } from "./types";
10
+ import { DEFAULT_AWARENESS_CONFIG } from "./types";
11
+
12
+ export function loadConfig(cwd: string): AwarenessConfig {
13
+ const configPath = path.join(cwd, ".awarenessrc.yml");
14
+ const config: AwarenessConfig = { ...DEFAULT_AWARENESS_CONFIG };
15
+
16
+ if (!fs.existsSync(configPath)) return config;
17
+
18
+ try {
19
+ const content = fs.readFileSync(configPath, "utf-8");
20
+ const result: Record<string, string> = {};
21
+
22
+ for (const line of content.split("\n")) {
23
+ const trimmed = line.trim();
24
+ if (!trimmed || trimmed.startsWith("#")) continue;
25
+ const m = trimmed.match(/^\s*([\w][\w.]*):\s*(.+)$/);
26
+ if (m) {
27
+ let v = m[2].trim();
28
+ // Strip surrounding quotes
29
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
30
+ v = v.slice(1, -1);
31
+ }
32
+ result[m[1]] = v;
33
+ }
34
+ }
35
+
36
+ // Top-level booleans
37
+ if ("enabled" in result) config.enabled = result["enabled"] !== "false";
38
+ if ("injectReminders" in result) config.injectReminders = result["injectReminders"] !== "false";
39
+ if ("logEnvelopes" in result) config.logEnvelopes = result["logEnvelopes"] !== "false";
40
+
41
+ // Strings
42
+ if (result["envelopeLogPath"]) config.envelopeLogPath = result["envelopeLogPath"];
43
+ if (result["excludedTools"]) {
44
+ config.excludedTools = result["excludedTools"].split(",").map(s => s.trim()).filter(Boolean);
45
+ }
46
+
47
+ // Numbers
48
+ if (result["maxWarningsInReminder"]) {
49
+ const n = parseInt(result["maxWarningsInReminder"], 10);
50
+ if (!isNaN(n)) config.maxWarningsInReminder = n;
51
+ }
52
+ if (result["thresholds.lowConfidence"]) {
53
+ const n = parseFloat(result["thresholds.lowConfidence"]);
54
+ if (!isNaN(n)) config.thresholds.lowConfidence = n;
55
+ }
56
+ if (result["thresholds.staleFreshness"]) {
57
+ const n = parseFloat(result["thresholds.staleFreshness"]);
58
+ if (!isNaN(n)) config.thresholds.staleFreshness = n;
59
+ }
60
+ if (result["thresholds.maxWarningsBeforeCritical"]) {
61
+ const n = parseInt(result["thresholds.maxWarningsBeforeCritical"], 10);
62
+ if (!isNaN(n)) config.thresholds.maxWarningsBeforeCritical = n;
63
+ }
64
+
65
+ return config;
66
+ } catch {
67
+ return config;
68
+ }
69
+ }
package/src/format.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * pi-tool-awareness-gate — Formatters
3
+ *
4
+ * Formats ToolResultEnvelope into:
5
+ * 1. Short reminders — 1-2 line summaries injected into agent context
6
+ * 2. Rich payloads — structured JSON for logging and evaluation
7
+ */
8
+ import type {
9
+ ToolResultEnvelope,
10
+ ShortReminder,
11
+ AwarenessConfig,
12
+ } from "./types";
13
+
14
+ // ── Icons ─────────────────────────────────────────────────────────────────────
15
+
16
+ const STATUS_ICONS: Record<string, string> = {
17
+ success: "✅",
18
+ partial: "⚠️",
19
+ failure: "❌",
20
+ timeout: "⏱️",
21
+ unauthorized: "🔒",
22
+ };
23
+
24
+ const WARNING_PREFIX: Record<string, string> = {
25
+ info: "ℹ️",
26
+ warn: "⚠️",
27
+ critical: "🚨",
28
+ };
29
+
30
+ // ── Short Reminder ────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Format a short reminder (1-2 lines) for injection into agent context.
34
+ */
35
+ export function formatShortReminder(
36
+ envelope: ToolResultEnvelope,
37
+ config: AwarenessConfig,
38
+ ): ShortReminder {
39
+ const parts: string[] = [];
40
+ const icon = STATUS_ICONS[envelope.status] || "❓";
41
+ const truncatedWarnings = envelope.warnings.slice(0, config.maxWarningsInReminder);
42
+
43
+ // Build a compact one-liner starting with tool name
44
+ let summary = `[${envelope.meta.tool_name}] ${icon}`;
45
+
46
+ // Add key quality signals as compact badges
47
+ if (envelope.quality.confidence !== undefined) {
48
+ const pct = Math.round(envelope.quality.confidence * 100);
49
+ summary += ` conf:${pct}%`;
50
+ }
51
+ if (envelope.quality.completeness && envelope.quality.completeness !== "full") {
52
+ summary += ` ${envelope.quality.completeness}`;
53
+ }
54
+ if (envelope.limitations.truncated) {
55
+ summary += " truncated";
56
+ }
57
+ if (envelope.limitations.stale) {
58
+ summary += " stale";
59
+ }
60
+
61
+ parts.push(summary);
62
+
63
+ // Add warnings as a second line if any
64
+ if (truncatedWarnings.length > 0) {
65
+ const warnText = truncatedWarnings
66
+ .map(w => `• ${w}`)
67
+ .join(" | ");
68
+ parts.push(` ${warnText}`);
69
+ }
70
+
71
+ if (envelope.warnings.length > config.maxWarningsInReminder) {
72
+ const remaining = envelope.warnings.length - config.maxWarningsInReminder;
73
+ parts.push(` ... +${remaining} more warnings`);
74
+ }
75
+
76
+ // Add top suggestion if available
77
+ if (envelope.suggestions && envelope.suggestions.length > 0) {
78
+ parts.push(` → ${envelope.suggestions[0]}`);
79
+ }
80
+
81
+ // Determine severity level
82
+ let level: ShortReminder["level"] = "info";
83
+ if (envelope.status === "failure" || envelope.status === "timeout") {
84
+ level = "critical";
85
+ } else if (
86
+ envelope.warnings.length >= config.thresholds.maxWarningsBeforeCritical
87
+ ) {
88
+ level = "critical";
89
+ } else if (
90
+ envelope.warnings.length > 0 ||
91
+ envelope.status === "partial"
92
+ ) {
93
+ level = "warn";
94
+ }
95
+
96
+ return {
97
+ tool: envelope.meta.tool_name,
98
+ summary: `${WARNING_PREFIX[level]} ${parts.join("\n")}`,
99
+ level,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Inject a short reminder into a tool result's content.
105
+ * Prepends the reminder to the first text block.
106
+ */
107
+ export function injectReminder(
108
+ result: unknown,
109
+ reminder: ShortReminder,
110
+ ): unknown {
111
+ if (!result || typeof result !== "object") return result;
112
+
113
+ const r = result as Record<string, unknown>;
114
+
115
+ // Only inject if result has a content array (standard pi tool result)
116
+ if (!Array.isArray(r.content) || r.content.length === 0) return result;
117
+
118
+ const blocks = r.content as Array<Record<string, unknown>>;
119
+ const firstTextIdx = blocks.findIndex(
120
+ b => b.type === "text" && typeof b.text === "string",
121
+ );
122
+
123
+ if (firstTextIdx === -1) return result;
124
+
125
+ const original = blocks[firstTextIdx].text as string;
126
+ blocks[firstTextIdx] = {
127
+ type: "text",
128
+ text: `${reminder.summary}\n\n${original}`,
129
+ };
130
+
131
+ return { ...r, content: blocks };
132
+ }
133
+
134
+ // ── Rich Payload ──────────────────────────────────────────────────────────────
135
+
136
+ /**
137
+ * Format the full ToolResultEnvelope as a loggable JSON object.
138
+ */
139
+ export function formatEnvelopeLog(
140
+ envelope: ToolResultEnvelope,
141
+ ): Record<string, unknown> {
142
+ return {
143
+ timestamp: envelope.meta.timestamp,
144
+ tool: envelope.meta.tool_name,
145
+ invocation_id: envelope.meta.invocation_id,
146
+ status: envelope.status,
147
+ latency_ms: envelope.meta.latency_ms,
148
+ quality: envelope.quality,
149
+ limitations: envelope.limitations,
150
+ warnings: envelope.warnings,
151
+ suggestions: envelope.suggestions || [],
152
+ };
153
+ }
154
+
155
+ // ── Display Formatters ────────────────────────────────────────────────────────
156
+
157
+ /**
158
+ * Format envelope as a human-readable text block (for supervisor_status-like tools).
159
+ */
160
+ export function formatEnvelopeDisplay(envelope: ToolResultEnvelope): string {
161
+ const icon = STATUS_ICONS[envelope.status] || "❓";
162
+ const lines = [
163
+ `${icon} Tool: ${envelope.meta.tool_name}`,
164
+ ` Status: ${envelope.status} | Latency: ${envelope.meta.latency_ms}ms | Retries: ${envelope.meta.retries}`,
165
+ ` Invocation: ${envelope.meta.invocation_id}`,
166
+ ];
167
+
168
+ if (Object.keys(envelope.quality).length > 0) {
169
+ const q = envelope.quality;
170
+ const qParts: string[] = [];
171
+ if (q.confidence !== undefined) qParts.push(`confidence: ${(q.confidence * 100).toFixed(0)}%`);
172
+ if (q.completeness) qParts.push(`completeness: ${q.completeness}`);
173
+ if (q.freshness !== undefined) qParts.push(`freshness: ${(q.freshness * 100).toFixed(0)}%`);
174
+ lines.push(` Quality: ${qParts.join(" | ")}`);
175
+ }
176
+
177
+ if (Object.keys(envelope.limitations).length > 0) {
178
+ const activeFlags = Object.entries(envelope.limitations)
179
+ .filter(([, v]) => v === true)
180
+ .map(([k]) => k);
181
+ if (activeFlags.length > 0) {
182
+ lines.push(` Limitations: ${activeFlags.join(", ")}`);
183
+ }
184
+ }
185
+
186
+ if (envelope.warnings.length > 0) {
187
+ lines.push(` Warnings (${envelope.warnings.length}):`);
188
+ for (const w of envelope.warnings) {
189
+ lines.push(` • ${w}`);
190
+ }
191
+ }
192
+
193
+ if (envelope.suggestions && envelope.suggestions.length > 0) {
194
+ lines.push(` Suggestions:`);
195
+ for (const s of envelope.suggestions) {
196
+ lines.push(` → ${s}`);
197
+ }
198
+ }
199
+
200
+ return lines.join("\n");
201
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * pi-tool-awareness-gate — Helpers
3
+ *
4
+ * Utility functions for the awareness gate.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import type { AwarenessConfig } from "./types";
9
+
10
+ /**
11
+ * Generate a unique invocation ID for each tool call.
12
+ */
13
+ export function generateInvocationId(): string {
14
+ const timestamp = Date.now().toString(36);
15
+ const random = Math.random().toString(36).substring(2, 8);
16
+ return `${timestamp}-${random}`;
17
+ }
18
+
19
+ /**
20
+ * Get the envelope log path (relative to cwd).
21
+ */
22
+ export function getEnvelopeLogPath(cwd: string, config: AwarenessConfig): string {
23
+ return path.join(cwd, config.envelopeLogPath);
24
+ }
25
+
26
+ /**
27
+ * Append a structured envelope to the log.
28
+ */
29
+ export function appendEnvelopeLog(
30
+ cwd: string,
31
+ config: AwarenessConfig,
32
+ entry: Record<string, unknown>,
33
+ ): void {
34
+ if (!config.logEnvelopes) return;
35
+ const logPath = getEnvelopeLogPath(cwd, config);
36
+ const dir = path.dirname(logPath);
37
+ if (!fs.existsSync(dir)) {
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+ fs.appendFileSync(logPath, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
41
+ }
42
+
43
+ /**
44
+ * Extract text content from a tool result, regardless of shape.
45
+ */
46
+ export function extractTextContent(result: unknown): string {
47
+ if (!result || typeof result !== "object") return "";
48
+
49
+ const r = result as Record<string, unknown>;
50
+
51
+ // Standard pi tool result: { content: [{ type: "text", text: "..." }] }
52
+ if (Array.isArray(r.content)) {
53
+ const texts: string[] = [];
54
+ for (const block of r.content as Array<Record<string, unknown>>) {
55
+ if (block.type === "text" && typeof block.text === "string") {
56
+ texts.push(block.text);
57
+ }
58
+ }
59
+ return texts.join("\n");
60
+ }
61
+
62
+ // Raw string result
63
+ if (typeof r.text === "string") return r.text;
64
+ if (typeof r.message === "string") return r.message;
65
+
66
+ return "";
67
+ }
68
+
69
+ /**
70
+ * Check if text content appears truncated.
71
+ * Uses heuristics: sudden cutoffs, missing closing markers, etc.
72
+ */
73
+ export function isTextTruncated(text: string): boolean {
74
+ if (!text || text.length < 100) return false;
75
+
76
+ // Truncation indicators in typical tool output
77
+ const truncationMarkers = [
78
+ /\.\.\.\s*(truncated|omitted|remaining|(?:more|d+) (?:lines|bytes))/i,
79
+ /\[truncated\]/i,
80
+ /<omitted\s*\/?>/i,
81
+ /\.\.\.\s*$/m, // trailing ellipsis on last line
82
+ /\btruncated\b.*\b(?:output|result|response)\b/i,
83
+ // pi read tool truncation
84
+ /more lines in file/i,
85
+ /Use offset=\d+ to continue/i,
86
+ ];
87
+
88
+ for (const marker of truncationMarkers) {
89
+ if (marker.test(text)) return true;
90
+ }
91
+
92
+ // Heuristic: text ends mid-sentence with no punctuation
93
+ const lastChar = text.trim().slice(-1);
94
+ const lastLine = text.trim().split("\n").pop() || "";
95
+ if (
96
+ lastLine.length > 20 &&
97
+ !/[.!?;:)\]}"'>]$/.test(lastChar) &&
98
+ !/^[\s#*\-–—]/.test(lastLine)
99
+ ) {
100
+ // Could be truncated — moderate confidence
101
+ // Only flag if the last line looks like a natural sentence fragment
102
+ if (/[a-z]$/.test(lastLine) && lastLine.split(" ").length > 3) {
103
+ return true;
104
+ }
105
+ }
106
+
107
+ return false;
108
+ }
109
+
110
+ /**
111
+ * Determine status from raw tool result.
112
+ */
113
+ export function inferStatus(result: unknown, error?: unknown): string {
114
+ if (error) return "failure";
115
+
116
+ if (result && typeof result === "object") {
117
+ const r = result as Record<string, unknown>;
118
+ if (r.isError === true) return "failure";
119
+ if (r.status && typeof r.status === "string") return r.status;
120
+ if (r.timeout === true) return "timeout";
121
+ }
122
+
123
+ return "success";
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * pi-tool-awareness-gate — Main Extension
3
+ *
4
+ * Intercepts every tool_result event, infers quality signals and limitation
5
+ * flags, injects short reminders into agent context, and logs structured
6
+ * envelopes for evaluation.
7
+ *
8
+ * Works automatically with ALL existing gates — no code changes required.
9
+ *
10
+ * Events hooked:
11
+ * - tool_result → infer quality, inject reminders, log envelopes
12
+ * - tool_error → flag as failure
13
+ * - session_start → initialize config + session state
14
+ * - session_shutdown → flush logs
15
+ */
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+ import { loadConfig } from "./config";
18
+ import { inferQuality, inferLimitations, inferWarnings, inferSuggestions } from "./infer";
19
+ import {
20
+ formatShortReminder,
21
+ formatEnvelopeLog,
22
+ injectReminder,
23
+ } from "./format";
24
+ import {
25
+ generateInvocationId,
26
+ appendEnvelopeLog,
27
+ } from "./helpers";
28
+ import type {
29
+ ToolResultEnvelope,
30
+ RawToolResult,
31
+ AwarenessConfig,
32
+ } from "./types";
33
+
34
+ // ── Module-level state ───────────────────────────────────────────────────────
35
+
36
+ let config: AwarenessConfig | null = null;
37
+ /** Track invocation start times to calculate latency */
38
+ const invocationStarts = new Map<string, number>();
39
+ let invocationCounter = 0;
40
+
41
+ // ── Extension ────────────────────────────────────────────────────────────────
42
+
43
+ export default function (pi: ExtensionAPI) {
44
+ // ── Session lifecycle ────────────────────────────────────────────────────
45
+
46
+ pi.on("session_start", (_event, ctx) => {
47
+ config = loadConfig(ctx.cwd);
48
+ invocationCounter = 0;
49
+ invocationStarts.clear();
50
+ });
51
+
52
+ pi.on("session_shutdown", () => {
53
+ invocationStarts.clear();
54
+ config = null;
55
+ });
56
+
57
+ // ── Track tool execution start for latency calculation ───────────────────
58
+
59
+ pi.on("tool_execution_start", (event) => {
60
+ if (!config?.enabled) return;
61
+ if (config.excludedTools.includes(event.toolName)) return;
62
+
63
+ invocationCounter++;
64
+ const invId = `${event.toolName}-${invocationCounter}`;
65
+ invocationStarts.set(event.toolCallId, Date.now());
66
+ });
67
+
68
+ // ── Main interception: tool_result ───────────────────────────────────────
69
+
70
+ pi.on("tool_result", async (event, ctx) => {
71
+ if (!config?.enabled) return;
72
+ if (config.excludedTools.includes(event.toolName)) return;
73
+
74
+ const startTime = invocationStarts.get(event.toolCallId);
75
+ const latencyMs = startTime ? Date.now() - startTime : 0;
76
+ invocationStarts.delete(event.toolCallId);
77
+
78
+ // Build raw input for inference
79
+ const raw: RawToolResult = {
80
+ toolName: event.toolName,
81
+ result: {
82
+ content: event.content,
83
+ details: event.details,
84
+ isError: event.isError,
85
+ },
86
+ error: event.isError ? "tool error" : undefined,
87
+ isError: event.isError,
88
+ content: event.content,
89
+ details: event.details,
90
+ };
91
+
92
+ // Infer quality signals
93
+ const quality = inferQuality(raw, config);
94
+ const limitations = inferLimitations(raw);
95
+ const warnings = inferWarnings(raw, quality, limitations, config);
96
+ const suggestions = inferSuggestions(raw, warnings, quality);
97
+
98
+ // Determine status
99
+ let status: ToolResultEnvelope["status"] = "success";
100
+ if (event.isError) {
101
+ status = "failure";
102
+ } else if (limitations.truncated || limitations.limited_scope ||
103
+ limitations.requires_human_review) {
104
+ status = "partial";
105
+ } else if (warnings.length > 0) {
106
+ status = "partial";
107
+ }
108
+
109
+ // Build envelope
110
+ const envelope: ToolResultEnvelope = {
111
+ status,
112
+ result: { content: event.content, details: event.details },
113
+ meta: {
114
+ tool_name: event.toolName,
115
+ invocation_id: `${event.toolName}-${invocationCounter}`,
116
+ latency_ms: latencyMs,
117
+ retries: 0,
118
+ timestamp: new Date().toISOString(),
119
+ },
120
+ quality,
121
+ limitations,
122
+ warnings,
123
+ suggestions: suggestions.length > 0 ? suggestions : undefined,
124
+ };
125
+
126
+ // Log rich payload
127
+ appendEnvelopeLog(ctx.cwd, config, formatEnvelopeLog(envelope));
128
+
129
+ // Inject short reminder into result content
130
+ if (config.injectReminders && (warnings.length > 0 || status !== "success")) {
131
+ const reminder = formatShortReminder(envelope, config);
132
+ const enriched = injectReminder(
133
+ { content: event.content, details: event.details, isError: event.isError },
134
+ reminder,
135
+ ) as { content?: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean };
136
+
137
+ return {
138
+ content: enriched?.content ?? event.content,
139
+ details: { ...event.details, _awareness: envelope },
140
+ isError: event.isError,
141
+ };
142
+ }
143
+
144
+ // Even without reminder injection, attach envelope to details
145
+ return {
146
+ details: { ...event.details, _awareness: envelope },
147
+ };
148
+ });
149
+
150
+ // ── Error tracking ───────────────────────────────────────────────────────
151
+
152
+ pi.on("tool_error", (event, ctx) => {
153
+ if (!config?.enabled) return;
154
+ if (config.excludedTools.includes(event.toolName)) return;
155
+
156
+ const startTime = invocationStarts.get(event.toolCallId);
157
+ const latencyMs = startTime ? Date.now() - startTime : 0;
158
+
159
+ const envelope: ToolResultEnvelope = {
160
+ status: "failure",
161
+ result: { error: String(event.error) },
162
+ meta: {
163
+ tool_name: event.toolName,
164
+ invocation_id: `${event.toolName}-${++invocationCounter}`,
165
+ latency_ms: latencyMs,
166
+ retries: 0,
167
+ timestamp: new Date().toISOString(),
168
+ },
169
+ quality: {
170
+ confidence: 0,
171
+ completeness: "minimal",
172
+ freshness: 0,
173
+ accuracy: 0,
174
+ },
175
+ limitations: {
176
+ requires_human_review: true,
177
+ },
178
+ warnings: [`Tool execution failed: ${String(event.error).substring(0, 200)}`],
179
+ suggestions: ["Check tool parameters and retry", "Consider fallback approach"],
180
+ };
181
+
182
+ appendEnvelopeLog(ctx.cwd, config, formatEnvelopeLog(envelope));
183
+ });
184
+ }
package/src/infer.ts ADDED
@@ -0,0 +1,361 @@
1
+ /**
2
+ * pi-tool-awareness-gate — Inference Engine
3
+ *
4
+ * Inspects raw tool results from the `tool_result` event and
5
+ * infers quality signals, limitation flags, and warnings.
6
+ *
7
+ * Inference is heuristic-based — domain-specific signals require
8
+ * gates to enrich their results using the ToolResultEnvelope types.
9
+ */
10
+ import type {
11
+ RawToolResult,
12
+ QualitySignals,
13
+ LimitationFlags,
14
+ AwarenessConfig,
15
+ } from "./types";
16
+ import { extractTextContent, isTextTruncated, inferStatus } from "./helpers";
17
+
18
+ /**
19
+ * Detect common bash error patterns in command output.
20
+ * pi's standard bash tool doesn't expose exitCode in details,
21
+ * so we detect errors from content text heuristics.
22
+ */
23
+ function looksLikeBashError(content: string): boolean {
24
+ if (!content || content.length < 3) return false;
25
+
26
+ const errorPatterns = [
27
+ /No such file or directory/i,
28
+ /command not found/i,
29
+ /Permission denied/i,
30
+ /cannot access/i,
31
+ /not a directory/i,
32
+ /Is a directory/i,
33
+ /Operation not permitted/i,
34
+ /No space left on device/i,
35
+ /Connection refused/i,
36
+ /Could not resolve host/i,
37
+ /fatal:/i,
38
+ /error:/i,
39
+ /Error:/,
40
+ /ERROR:/,
41
+ /syntax error/i,
42
+ /unexpected token/i,
43
+ /npm error/i,
44
+ /npm ERR!/,
45
+ /exit code [1-9]/i,
46
+ ];
47
+
48
+ // Check first few lines — errors typically appear early
49
+ const firstLines = content.split("\n").slice(0, 10).join("\n");
50
+ for (const pattern of errorPatterns) {
51
+ if (pattern.test(firstLines)) return true;
52
+ }
53
+
54
+ return false;
55
+ }
56
+
57
+ /**
58
+ * Infer quality signals from a raw tool result.
59
+ */
60
+ export function inferQuality(
61
+ raw: RawToolResult,
62
+ _config: AwarenessConfig,
63
+ ): QualitySignals {
64
+ const signals: QualitySignals = {};
65
+ const content = extractTextContent(raw.result);
66
+ const error = raw.error || (raw.result as any)?.isError;
67
+
68
+ // Confidence: penalized by errors, empty results, bash errors
69
+ if (error) {
70
+ signals.confidence = 0.0;
71
+ } else if (!content || content.length === 0) {
72
+ signals.confidence = 0.3;
73
+ } else if (raw.toolName === "bash" && looksLikeBashError(content)) {
74
+ signals.confidence = 0.3;
75
+ } else {
76
+ signals.confidence = 0.8; // Default for successful tool calls
77
+ }
78
+
79
+ // Completeness
80
+ if (!content || content.length === 0) {
81
+ signals.completeness = "minimal";
82
+ } else if (
83
+ isTextTruncated(content) ||
84
+ (raw.result as any)?.details?.truncated === true
85
+ ) {
86
+ signals.completeness = "partial";
87
+ } else {
88
+ signals.completeness = "full";
89
+ }
90
+
91
+ // Freshness — can't infer without timestamps in result
92
+ signals.freshness = 0.5; // Unknown by default
93
+
94
+ // Accuracy — correlated with confidence unless contradicted
95
+ signals.accuracy = signals.confidence;
96
+
97
+ return signals;
98
+ }
99
+
100
+ /**
101
+ * Infer limitation flags from raw result + tool name.
102
+ */
103
+ export function inferLimitations(raw: RawToolResult): LimitationFlags {
104
+ const flags: LimitationFlags = {};
105
+ const content = extractTextContent(raw.result);
106
+ const result = raw.result as Record<string, unknown> | undefined;
107
+
108
+ // Truncated: check heuristic + explicit flag
109
+ flags.truncated = isTextTruncated(content) || result?.truncated === true;
110
+
111
+ // Limited scope: partial results, filtered output
112
+ if (result?.details) {
113
+ const details = result.details as Record<string, unknown>;
114
+ if (details.truncated === true) flags.truncated = true;
115
+ if (details.totalCount && details.count) {
116
+ flags.limited_scope = (details.count as number) < (details.totalCount as number);
117
+ }
118
+ // Pagination indicators
119
+ if (details.limit && details.total) {
120
+ flags.limited_scope = (details.limit as number) < (details.total as number);
121
+ }
122
+ }
123
+
124
+ // API response indicators
125
+ if (result) {
126
+ // HTTP error status
127
+ if (
128
+ (result.status && typeof result.status === "number" && result.status >= 400) ||
129
+ (result.statusCode && typeof result.statusCode === "number" && result.statusCode >= 400)
130
+ ) {
131
+ flags.requires_human_review = true;
132
+ }
133
+
134
+ // Rate limiting
135
+ if (
136
+ result.retryAfter !== undefined ||
137
+ (typeof result.text === "string" && /rate.?limit/i.test(result.text))
138
+ ) {
139
+ flags.limited_scope = true;
140
+ }
141
+ }
142
+
143
+ // Tool-specific inference
144
+ switch (raw.toolName) {
145
+ case "bash": {
146
+ // pi's standard bash tool doesn't include exitCode in details —
147
+ // detect errors from content text patterns instead
148
+ if (looksLikeBashError(content)) {
149
+ flags.requires_human_review = true;
150
+ }
151
+ break;
152
+ }
153
+ case "read": {
154
+ // Partial reads (details.offset/limit)
155
+ const rDetails = result?.details as Record<string, unknown> | undefined;
156
+ if (rDetails?.offset !== undefined || rDetails?.limit !== undefined) {
157
+ flags.limited_scope = true;
158
+ }
159
+ break;
160
+ }
161
+ case "browser_navigate":
162
+ case "browser_click":
163
+ case "browser_type":
164
+ case "browser_read":
165
+ case "browser_screenshot":
166
+ case "browser_scroll":
167
+ case "browser_evaluate": {
168
+ // Browser navigations may have HTTP errors
169
+ if (
170
+ typeof content === "string" &&
171
+ (content.includes("Error:") || content.includes("ERR_") || content.includes("timeout"))
172
+ ) {
173
+ flags.limited_scope = true;
174
+ }
175
+ break;
176
+ }
177
+ case "write":
178
+ case "edit": {
179
+ // File writes are usually fine but check for partial writes
180
+ const wDetails = result?.details as Record<string, unknown> | undefined;
181
+ if (wDetails?.bytesWritten && wDetails?.bytesExpected) {
182
+ flags.truncated =
183
+ (wDetails.bytesWritten as number) < (wDetails.bytesExpected as number);
184
+ }
185
+ break;
186
+ }
187
+ }
188
+
189
+ return flags;
190
+ }
191
+
192
+ /**
193
+ * Generate warnings from the inferred signals + tool context.
194
+ */
195
+ export function inferWarnings(
196
+ raw: RawToolResult,
197
+ quality: QualitySignals,
198
+ limitations: LimitationFlags,
199
+ config: AwarenessConfig,
200
+ ): string[] {
201
+ const warnings: string[] = [];
202
+
203
+ // Confidence warnings
204
+ if (quality.confidence !== undefined && quality.confidence < config.thresholds.lowConfidence) {
205
+ warnings.push(
206
+ `Low confidence (${(quality.confidence * 100).toFixed(0)}%) — result may be unreliable`,
207
+ );
208
+ }
209
+
210
+ // Freshness warnings
211
+ if (quality.freshness !== undefined && quality.freshness < config.thresholds.staleFreshness) {
212
+ warnings.push("Data may be stale — freshness score is low");
213
+ }
214
+
215
+ // Completeness warnings
216
+ if (quality.completeness === "minimal") {
217
+ warnings.push("Minimal result returned — data may be incomplete");
218
+ } else if (quality.completeness === "partial") {
219
+ warnings.push("Partial result — not all requested data was returned");
220
+ }
221
+
222
+ // Truncation
223
+ if (limitations.truncated) {
224
+ warnings.push("Output was truncated — some content may be missing");
225
+ }
226
+
227
+ // Limited scope
228
+ if (limitations.limited_scope) {
229
+ warnings.push("Result is scoped/limited — may not cover full request");
230
+ }
231
+
232
+ // Human review
233
+ if (limitations.requires_human_review) {
234
+ warnings.push("Human review recommended before acting on this result");
235
+ }
236
+
237
+ // Sensitive content
238
+ if (limitations.sensitive_content) {
239
+ warnings.push("Result may contain sensitive data — handle with care");
240
+ }
241
+
242
+ // Staleness (explicit flag)
243
+ if (limitations.stale) {
244
+ warnings.push("Data may be outdated — verify freshness");
245
+ }
246
+
247
+ // Tool-specific warnings
248
+ const result = raw.result as Record<string, unknown> | undefined;
249
+ const detailsWarn = result?.details as Record<string, unknown> | undefined;
250
+ const content = extractTextContent(raw.result);
251
+
252
+ switch (raw.toolName) {
253
+ case "bash": {
254
+ if (looksLikeBashError(content)) {
255
+ // Extract the first error line for the warning
256
+ const errLine = content.split("\n").find(l =>
257
+ /error|Error|ERROR|cannot|denied|not found|No such/i.test(l)
258
+ );
259
+ if (errLine) {
260
+ warnings.push(`Bash error: ${errLine.trim().substring(0, 100)}`);
261
+ } else {
262
+ warnings.push("Command produced error output");
263
+ }
264
+ }
265
+ if (detailsWarn?.timedOut) {
266
+ warnings.push("Command timed out");
267
+ }
268
+ break;
269
+ }
270
+ case "read": {
271
+ if (detailsWarn?.truncated) {
272
+ warnings.push("File read was truncated at limit");
273
+ }
274
+ break;
275
+ }
276
+ case "browser_navigate":
277
+ case "browser_read": {
278
+ if (content.includes("screenshot/OCR failed") || content.includes("OCR unavailable")) {
279
+ warnings.push("OCR/rendering failure — page content may be incomplete");
280
+ }
281
+ break;
282
+ }
283
+ case "ci_get_logs":
284
+ case "ci_list_runs":
285
+ case "ci_get_run": {
286
+ if (content.includes("❌") || content.includes("Failed")) {
287
+ warnings.push("CI API returned errors — check Gitea connectivity");
288
+ }
289
+ break;
290
+ }
291
+ case "doctor_audit":
292
+ case "doctor_check_file": {
293
+ // Doctor tools are informational — no warnings needed
294
+ break;
295
+ }
296
+ }
297
+
298
+ return warnings;
299
+ }
300
+
301
+ /**
302
+ * Generate actionable suggestions based on warnings and tool type.
303
+ */
304
+ export function inferSuggestions(
305
+ raw: RawToolResult,
306
+ warnings: string[],
307
+ quality: QualitySignals,
308
+ ): string[] {
309
+ const suggestions: string[] = [];
310
+
311
+ if (warnings.length === 0) return suggestions;
312
+
313
+ // Generic suggestions based on limitation types
314
+ if (quality.completeness === "partial" || quality.completeness === "minimal") {
315
+ suggestions.push("Consider retrying with narrower scope or pagination parameters");
316
+ }
317
+
318
+ if (quality.freshness !== undefined && quality.freshness < 0.3) {
319
+ suggestions.push("Verify time-sensitive claims against a real-time source");
320
+ }
321
+
322
+ // Tool-specific suggestions
323
+ switch (raw.toolName) {
324
+ case "bash": {
325
+ const result = raw.result as Record<string, unknown> | undefined;
326
+ const details = result?.details as Record<string, unknown> | undefined;
327
+ const bashContent = extractTextContent(raw.result);
328
+ if (looksLikeBashError(bashContent)) {
329
+ suggestions.push("Check the error output above and adjust the command");
330
+ }
331
+ if (details?.timedOut) {
332
+ suggestions.push("Increase timeout or break command into smaller operations");
333
+ }
334
+ break;
335
+ }
336
+ case "read": {
337
+ if (raw.details?.truncated) {
338
+ suggestions.push("Use offset and limit parameters to read remaining content");
339
+ }
340
+ break;
341
+ }
342
+ case "browser_navigate":
343
+ case "browser_click":
344
+ case "browser_read": {
345
+ if (extractTextContent(raw.result).includes("OCR unavailable")) {
346
+ suggestions.push("Install tesseract.js for OCR support: npm install tesseract.js");
347
+ }
348
+ break;
349
+ }
350
+ case "ci_get_logs":
351
+ case "ci_list_runs": {
352
+ if (extractTextContent(raw.result).includes("Failed")) {
353
+ suggestions.push("Verify Gitea instance is running and reachable");
354
+ suggestions.push("Check .circ.yml configuration for correct Gitea URL");
355
+ }
356
+ break;
357
+ }
358
+ }
359
+
360
+ return suggestions;
361
+ }
package/src/types.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * pi-tool-awareness-gate — Types
3
+ *
4
+ * Universal ToolResult envelope for all tool invocations.
5
+ * Gates can import these types for domain-specific enrichment.
6
+ */
7
+
8
+ // ── Tool Result Envelope ──────────────────────────────────────────────────────
9
+
10
+ export interface ToolResultMeta {
11
+ /** Name of the tool that produced this result */
12
+ tool_name: string;
13
+ /** Unique invocation identifier */
14
+ invocation_id: string;
15
+ /** Wall-clock latency in milliseconds */
16
+ latency_ms: number;
17
+ /** Number of retries before this result */
18
+ retries: number;
19
+ /** ISO 8601 timestamp of invocation start */
20
+ timestamp: string;
21
+ }
22
+
23
+ export interface QualitySignals {
24
+ /** 0-1 confidence in the result correctness */
25
+ confidence?: number;
26
+ /** How complete the result is relative to what was requested */
27
+ completeness?: "full" | "partial" | "minimal" | "unknown";
28
+ /** 0-1 freshness — 1 = real-time, 0 = stale/unknown */
29
+ freshness?: number;
30
+ /** 0-1 estimated accuracy */
31
+ accuracy?: number;
32
+ /** Additional tool-specific quality fields */
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ export interface LimitationFlags {
37
+ /** Result was truncated (output, pagination, context window) */
38
+ truncated?: boolean;
39
+ /** Data may be stale/outdated */
40
+ stale?: boolean;
41
+ /** Result covers only a subset of what was requested */
42
+ limited_scope?: boolean;
43
+ /** Human review recommended before acting on this result */
44
+ requires_human_review?: boolean;
45
+ /** Result may contain sensitive data */
46
+ sensitive_content?: boolean;
47
+ /** Additional tool-specific limitation flags */
48
+ [key: string]: boolean | undefined;
49
+ }
50
+
51
+ export type ToolResultStatus =
52
+ | "success" // Complete, no known issues
53
+ | "partial" // Incomplete or degraded
54
+ | "failure" // Tool execution failed
55
+ | "timeout" // Tool timed out
56
+ | "unauthorized"; // Auth/permission error
57
+
58
+ export interface ToolResultEnvelope<T = unknown> {
59
+ /** Deterministic status for control flow decisions */
60
+ status: ToolResultStatus;
61
+ /** The raw tool output payload */
62
+ result: T;
63
+ /** Execution metadata */
64
+ meta: ToolResultMeta;
65
+ /** Quality signals — how reliable is this result? */
66
+ quality: QualitySignals;
67
+ /** Binary limitation flags — what should the agent be cautious about? */
68
+ limitations: LimitationFlags;
69
+ /** Human + agent readable warnings */
70
+ warnings: string[];
71
+ /** Actionable next steps for the agent */
72
+ suggestions?: string[];
73
+ }
74
+
75
+ // ── Short Reminder ───────────────────────────────────────────────────────────
76
+
77
+ export interface ShortReminder {
78
+ /** Tool name */
79
+ tool: string;
80
+ /** 1-2 sentence summary of limitations */
81
+ summary: string;
82
+ /** Severity level */
83
+ level: "info" | "warn" | "critical";
84
+ }
85
+
86
+ // ── Awareness Config ─────────────────────────────────────────────────────────
87
+
88
+ export interface AwarenessConfig {
89
+ /** Enable/disable the awareness layer */
90
+ enabled: boolean;
91
+ /** Inject short reminders into agent context (default: true) */
92
+ injectReminders: boolean;
93
+ /** Log rich payloads for evaluation (default: true) */
94
+ logEnvelopes: boolean;
95
+ /** Path for envelope log (relative to cwd) */
96
+ envelopeLogPath: string;
97
+ /** Max warnings to include per short reminder */
98
+ maxWarningsInReminder: number;
99
+ /** Severity thresholds for quality signals */
100
+ thresholds: {
101
+ /** Confidence below this triggers a warning */
102
+ lowConfidence: number;
103
+ /** Freshness below this triggers a staleness warning */
104
+ staleFreshness: number;
105
+ /** Max warnings before escalating to critical */
106
+ maxWarningsBeforeCritical: number;
107
+ };
108
+ /** Tools to skip awareness tracking */
109
+ excludedTools: string[];
110
+ }
111
+
112
+ export const DEFAULT_AWARENESS_CONFIG: AwarenessConfig = {
113
+ enabled: true,
114
+ injectReminders: true,
115
+ logEnvelopes: true,
116
+ envelopeLogPath: ".awareness/envelopes.log",
117
+ maxWarningsInReminder: 3,
118
+ thresholds: {
119
+ lowConfidence: 0.5,
120
+ staleFreshness: 0.3,
121
+ maxWarningsBeforeCritical: 5,
122
+ },
123
+ excludedTools: [],
124
+ };
125
+
126
+ // ── Inference Input (what we have to work with from the tool_result event) ──
127
+
128
+ export interface RawToolResult {
129
+ toolName: string;
130
+ result: unknown;
131
+ error?: unknown;
132
+ /** If the tool has isError flag */
133
+ isError?: boolean;
134
+ /** Content array if present */
135
+ content?: Array<{ type: string; text?: string }>;
136
+ /** Details object if present */
137
+ details?: Record<string, unknown>;
138
+ }