@sayknow-cli/agent-core 0.3.13 → 0.3.15

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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tool output pruning utilities for compaction.
3
+ *
4
+ * Candidate selection is staleness-aware: tool results that have been
5
+ * superseded by a later result for the same target (same file read again,
6
+ * same search re-run) or invalidated by a later successful edit/write to a
7
+ * covered file are pruned in preference to merely-old results. Protect-window
8
+ * and minimum-savings hysteresis semantics are unchanged.
9
+ */
10
+ import type { SessionEntry, SessionMessageEntry } from "./entries";
11
+ export interface PruneConfig {
12
+ /** Keep the most recent tool output tokens intact. */
13
+ protectTokens: number;
14
+ /** Only prune if total savings meets this threshold. */
15
+ minimumSavings: number;
16
+ /** Tool names that should never be pruned. */
17
+ protectedTools: string[];
18
+ /**
19
+ * Tools in `protectedTools` whose protection is waived once the result is
20
+ * superseded (a later result for the same target, or a later successful
21
+ * edit/write to the covered file). The most recent result per target is
22
+ * never considered superseded. Optional; defaults to none.
23
+ */
24
+ staleOverridableTools?: string[];
25
+ }
26
+ export declare const DEFAULT_PRUNE_CONFIG: PruneConfig;
27
+ export interface PruneResult {
28
+ prunedCount: number;
29
+ tokensSaved: number;
30
+ /**
31
+ * The mutated message entries. Callers whose entry source returns
32
+ * materialized copies (not live references) must write these back into
33
+ * their canonical store by id.
34
+ */
35
+ prunedEntries: SessionMessageEntry[];
36
+ }
37
+ export interface AssistantArgumentPruneResult {
38
+ argumentPrunedCount: number;
39
+ argumentTokensSaved: number;
40
+ /**
41
+ * The mutated assistant message entries. Callers whose entry source returns
42
+ * materialized copies must write these back into their canonical store by id.
43
+ */
44
+ prunedEntries: SessionMessageEntry[];
45
+ }
46
+ export declare function pruneAssistantToolArguments(entries: SessionEntry[], config?: PruneConfig): AssistantArgumentPruneResult;
47
+ /**
48
+ * Estimate the token savings {@link pruneToolOutputs} would achieve, without
49
+ * mutating any entry. Returns 0 savings when below the configured minimum so the
50
+ * caller sees the same gate the real prune enforces.
51
+ */
52
+ export declare function estimateToolOutputPruneSavings(entries: SessionEntry[], config?: PruneConfig): {
53
+ prunableCount: number;
54
+ tokensSaved: number;
55
+ };
56
+ /**
57
+ * Evidence gate for below-threshold maintenance pruning (Finding 13). Pruning
58
+ * forces a prompt-cache-epoch reset, so it only runs when opted in AND the
59
+ * estimated stale savings clear a high minimum AND exceed the one-time reset
60
+ * cost (so the reclaim pays the reset back). Default-off/blocked until live
61
+ * evidence justifies enabling.
62
+ */
63
+ export declare function shouldRunMaintenancePrune(args: {
64
+ enabled: boolean;
65
+ estimatedSavings: number;
66
+ minSavings: number;
67
+ cacheEpochResetCost: number;
68
+ }): boolean;
69
+ export declare function pruneToolOutputs(entries: SessionEntry[], config?: PruneConfig): PruneResult;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared utilities for compaction and branch summarization.
3
+ */
4
+ import type { Message } from "@sayknow-cli/ai";
5
+ import type { AgentMessage } from "../types";
6
+ export interface FileOperations {
7
+ read: Set<string>;
8
+ written: Set<string>;
9
+ edited: Set<string>;
10
+ }
11
+ export declare function createFileOps(): FileOperations;
12
+ /**
13
+ * Extract file operations from tool calls in an assistant message.
14
+ */
15
+ export declare function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void;
16
+ /**
17
+ * Compute final file lists from file operations.
18
+ * Returns readFiles (files only read, not modified) and modifiedFiles.
19
+ */
20
+ export declare function computeFileLists(fileOps: FileOperations): {
21
+ readFiles: string[];
22
+ modifiedFiles: string[];
23
+ };
24
+ export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string;
25
+ export declare function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[]): string;
26
+ /**
27
+ * Serialize LLM messages to text for summarization.
28
+ * This prevents the model from treating it as a conversation to continue.
29
+ * Call convertToLlm() first to handle custom message types.
30
+ */
31
+ export declare function serializeConversation(messages: Message[]): string;
32
+ export declare const SUMMARIZATION_SYSTEM_PROMPT: string;
@@ -0,0 +1 @@
1
+ export * from "./compaction/index";
@@ -0,0 +1,100 @@
1
+ /**
2
+ * GPT-5 Harmony-header leakage detection and recovery.
3
+ *
4
+ * Background and policy: see `docs/ERRATA-GPT5-HARMONY.md`. This module
5
+ * implements §3 of that document: detection by signal fusion, plus a
6
+ * truncate-and-resume primitive for the `edit` tool when its input is in
7
+ * hashline DSL form. Other tools and surfaces fall through to
8
+ * abort-and-retry handled by the agent loop.
9
+ */
10
+ import type { AssistantMessage, Model } from "@sayknow-cli/ai";
11
+ declare const SIGNAL_ORDER: readonly ["M", "C", "G", "S", "B", "R", "T"];
12
+ export type HarmonySignalClass = "H" | "I" | (typeof SIGNAL_ORDER)[number];
13
+ export type HarmonySurface = "assistant_text" | "assistant_thinking" | "tool_arg";
14
+ export interface HarmonySignal {
15
+ classes: HarmonySignalClass[];
16
+ start: number;
17
+ end: number;
18
+ text: string;
19
+ }
20
+ export interface HarmonyDetection {
21
+ surface: HarmonySurface;
22
+ contentIndex?: number;
23
+ toolName?: string;
24
+ toolCallId?: string;
25
+ signals: HarmonySignal[];
26
+ }
27
+ export interface HarmonyAuditEvent {
28
+ action: "truncate_resume" | "abort_retry" | "escalated";
29
+ surface: HarmonySurface;
30
+ signal: string;
31
+ retryN: number;
32
+ model: string;
33
+ provider: string;
34
+ toolName?: string;
35
+ removedLen: number;
36
+ removedSha8: string;
37
+ removedPreview: string;
38
+ removedBlob?: string;
39
+ }
40
+ export interface HarmonyRecoveredToolCall {
41
+ message: AssistantMessage;
42
+ removed: string;
43
+ }
44
+ /**
45
+ * Whether to run leak detection on responses from this model. We default-on
46
+ * for every OpenAI code provider model rather than enumerating ids, so a future
47
+ * gpt-5.6 (or whatever) doesn't silently bypass the mitigation. Detection
48
+ * itself is cheap; the cost of missing a leak on a new model is not.
49
+ */
50
+ export declare function isHarmonyLeakMitigationTarget(model: Model): boolean;
51
+ export declare function shouldMitigateHarmonyLeak(model: Model, detection: HarmonyDetection): boolean;
52
+ export declare function signalListLabel(signals: readonly HarmonySignal[]): string;
53
+ /**
54
+ * Detect harmony-protocol leakage in `text`. Returns undefined if clean.
55
+ *
56
+ * Trip rule: `H` alone, or `M` paired with at least one co-signal
57
+ * (`C`/`G`/`S`/`B`/`R`/`T`). Bare `M` does not trip — this document, its
58
+ * tests, and bug reports legitimately carry the marker.
59
+ *
60
+ * `parsedEnd`, when supplied, marks the byte at which a structurally valid
61
+ * tool-argument parse ends; markers strictly after it set the `T` co-signal.
62
+ * `contentIndex`/`toolName`/`toolCallId` flow through to the returned
63
+ * detection for downstream auditing.
64
+ */
65
+ export declare function detectHarmonyLeak(text: string, surface: HarmonySurface, options?: {
66
+ parsedEnd?: number;
67
+ contentIndex?: number;
68
+ toolName?: string;
69
+ toolCallId?: string;
70
+ }): HarmonyDetection | undefined;
71
+ /** Scan an assistant message's content blocks; return the first detection. */
72
+ export declare function detectHarmonyLeakInAssistantMessage(message: AssistantMessage): HarmonyDetection | undefined;
73
+ /**
74
+ * Truncate a contaminated tool call at the start of the contaminated line and
75
+ * append the tool's recovery sentinel. Returns a recovered AssistantMessage
76
+ * (containing only the cleaned tool call), a synthetic continuation user
77
+ * message asking the model to re-issue the rest, and the removed substring
78
+ * for auditing. Returns undefined when the tool is not recovery-eligible or
79
+ * the truncation would leave nothing meaningful to dispatch.
80
+ *
81
+ * `providerPayload` is dropped from the recovered message: for OpenAI code backend the
82
+ * encrypted reasoning blob is opaque/signed and we cannot validate that it is
83
+ * uncontaminated. The model re-reasons on the next turn.
84
+ */
85
+ export declare function recoverHarmonyToolCall(message: AssistantMessage, detection: HarmonyDetection): HarmonyRecoveredToolCall | undefined;
86
+ /**
87
+ * Return the contaminated substring from `message` for audit purposes when
88
+ * recovery is not applicable (abort path). Walks from the first detected
89
+ * signal to end-of-content within the relevant block. Returns "" if the
90
+ * detection cannot be resolved against the message.
91
+ */
92
+ export declare function extractHarmonyRemoved(message: AssistantMessage, detection: HarmonyDetection): string;
93
+ export declare function createHarmonyAuditEvent(params: {
94
+ action: HarmonyAuditEvent["action"];
95
+ detection: HarmonyDetection;
96
+ model: Model;
97
+ retryN: number;
98
+ removed: string;
99
+ }): HarmonyAuditEvent;
100
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { ImageContent, TextContent } from "@sayknow-cli/ai";
2
+ export declare const IMAGE_PLACEHOLDER_ATTACHMENT_GUIDANCE = "Image placeholder text was submitted without an image payload. Paste the image with #paste-image, attach it with @path/to/image.png, or save the image and provide the saved file path.";
3
+ export declare function isImagePlaceholderOnlyText(text: string): boolean;
4
+ export declare function assertImagePlaceholdersHavePayload(text: string, content: readonly (TextContent | ImageContent)[] | undefined): void;
@@ -0,0 +1,11 @@
1
+ export * from "./agent";
2
+ export * from "./agent-loop";
3
+ export * from "./append-only-context";
4
+ export * from "./compaction";
5
+ export * from "./harmony-leak";
6
+ export * from "./image-placeholder-guard";
7
+ export * from "./proxy";
8
+ export * from "./run-collector";
9
+ export * from "./telemetry";
10
+ export * from "./thinking";
11
+ export * from "./types";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Proxy stream function for apps that route LLM calls through a server.
3
+ * The server manages auth and proxies requests to LLM providers.
4
+ */
5
+ import { type AssistantMessage, type AssistantMessageEvent, type Context, EventStream, type Model, type SimpleStreamOptions, type StopReason } from "@sayknow-cli/ai";
6
+ declare class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
7
+ constructor();
8
+ }
9
+ /**
10
+ * Proxy event types - server sends these with partial field stripped to reduce bandwidth.
11
+ */
12
+ export type ProxyAssistantMessageEvent = {
13
+ type: "start";
14
+ } | {
15
+ type: "text_start";
16
+ contentIndex: number;
17
+ } | {
18
+ type: "text_delta";
19
+ contentIndex: number;
20
+ delta: string;
21
+ } | {
22
+ type: "text_end";
23
+ contentIndex: number;
24
+ contentSignature?: string;
25
+ } | {
26
+ type: "thinking_start";
27
+ contentIndex: number;
28
+ } | {
29
+ type: "thinking_delta";
30
+ contentIndex: number;
31
+ delta: string;
32
+ } | {
33
+ type: "thinking_end";
34
+ contentIndex: number;
35
+ contentSignature?: string;
36
+ } | {
37
+ type: "toolcall_start";
38
+ contentIndex: number;
39
+ id: string;
40
+ toolName: string;
41
+ } | {
42
+ type: "toolcall_delta";
43
+ contentIndex: number;
44
+ delta: string;
45
+ } | {
46
+ type: "toolcall_end";
47
+ contentIndex: number;
48
+ } | {
49
+ type: "done";
50
+ reason: Extract<StopReason, "stop" | "length" | "toolUse">;
51
+ usage: AssistantMessage["usage"];
52
+ } | {
53
+ type: "error";
54
+ reason: Extract<StopReason, "aborted" | "error">;
55
+ errorMessage?: string;
56
+ usage: AssistantMessage["usage"];
57
+ };
58
+ export interface ProxyStreamOptions extends SimpleStreamOptions {
59
+ /** Auth token for the proxy server */
60
+ authToken: string;
61
+ /** Proxy server URL (e.g., "https://genai.example.com") */
62
+ proxyUrl: string;
63
+ }
64
+ /**
65
+ * Stream function that proxies through a server instead of calling LLM providers directly.
66
+ * The server strips the partial field from delta events to reduce bandwidth.
67
+ * We reconstruct the partial message client-side.
68
+ *
69
+ * Use this as the `streamFn` option when creating an Agent that needs to go through a proxy.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const agent = new Agent({
74
+ * streamFn: (model, context, options) =>
75
+ * streamProxy(model, context, {
76
+ * ...options,
77
+ * authToken: await getAuthToken(),
78
+ * proxyUrl: "https://genai.example.com",
79
+ * }),
80
+ * });
81
+ * ```
82
+ */
83
+ export declare function streamProxy(model: Model, context: Context, options: ProxyStreamOptions): ProxyMessageEventStream;
84
+ export {};
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Per-invocation run aggregator. Buffers per-chat and per-tool records as the
3
+ * loop executes and folds them into a single {@link AgentRunSummary} +
4
+ * {@link AgentRunCoverage} value at the end.
5
+ *
6
+ * One collector lives on each {@link AgentTelemetry} handle, which is
7
+ * constructed once per `agentLoop` invocation in {@link resolveTelemetry}.
8
+ * Collector lookups use the live `Span` as a `WeakMap` key — bounded memory,
9
+ * no cross-invoke leakage.
10
+ *
11
+ * The collector is fed exclusively by helpers in `./telemetry.ts`. Loop
12
+ * authors do not interact with it directly except via the public
13
+ * `recordSkippedTool` helper used for the two skip paths that bypass spans
14
+ * entirely (pre-run interrupt and the tail-sweep for tool calls that never
15
+ * produced a result message).
16
+ */
17
+ import type { Span } from "@opentelemetry/api";
18
+ import type { AssistantMessage, Model, StopReason } from "@sayknow-cli/ai";
19
+ /** Terminal status reported by an `execute_tool` span. */
20
+ export type ToolStatus = "ok" | "error" | "skipped" | "blocked" | "timeout" | "aborted";
21
+ /** Raw record for a single `chat` step, finalized by `finishChatSpan`. */
22
+ export interface ChatRecord {
23
+ readonly stepNumber: number;
24
+ readonly model: string;
25
+ readonly provider: string;
26
+ readonly stopReason: StopReason | undefined;
27
+ readonly latencyMs: number;
28
+ readonly inputTokens: number;
29
+ readonly outputTokens: number;
30
+ readonly cachedInputTokens: number;
31
+ readonly cacheWriteTokens: number;
32
+ readonly reasoningOutputTokens: number;
33
+ readonly totalTokens: number;
34
+ readonly costUsd: number | undefined;
35
+ readonly costUnavailableReason: string | undefined;
36
+ readonly errorType: string | undefined;
37
+ }
38
+ /** Raw record for a single `execute_tool` invocation. */
39
+ export interface ToolRecord {
40
+ readonly toolCallId: string;
41
+ readonly toolName: string;
42
+ readonly status: ToolStatus;
43
+ readonly latencyMs: number;
44
+ readonly errorType: string | undefined;
45
+ }
46
+ /** Per-tool counters surfaced under {@link AgentRunSummary.tools.byName}. */
47
+ export interface ToolCounters {
48
+ readonly total: number;
49
+ readonly ok: number;
50
+ readonly error: number;
51
+ readonly skipped: number;
52
+ readonly blocked: number;
53
+ readonly timeout: number;
54
+ readonly aborted: number;
55
+ readonly totalLatencyMs: number;
56
+ }
57
+ /**
58
+ * Run-level rollup returned in the `agent_end` event and passed to
59
+ * {@link AgentTelemetryConfig.onRunEnd}. Pure aggregation — no references to
60
+ * spans, no callbacks, no live state. Safe to persist / diff / assert.
61
+ */
62
+ export interface AgentRunSummary {
63
+ readonly chats: {
64
+ readonly total: number;
65
+ /** Bucketed by raw {@link StopReason}; absent reasons omitted. */
66
+ readonly byStopReason: Readonly<Record<string, number>>;
67
+ readonly totalLatencyMs: number;
68
+ };
69
+ readonly tools: {
70
+ readonly total: number;
71
+ readonly ok: number;
72
+ readonly error: number;
73
+ readonly skipped: number;
74
+ readonly blocked: number;
75
+ readonly timeout: number;
76
+ readonly aborted: number;
77
+ readonly totalLatencyMs: number;
78
+ /** Per-tool-name counters; keys sorted by name on snapshot. */
79
+ readonly byName: Readonly<Record<string, ToolCounters>>;
80
+ };
81
+ readonly usage: {
82
+ readonly inputTokens: number;
83
+ readonly outputTokens: number;
84
+ readonly cachedInputTokens: number;
85
+ readonly cacheWriteTokens: number;
86
+ readonly reasoningOutputTokens: number;
87
+ readonly totalTokens: number;
88
+ };
89
+ readonly cost: {
90
+ readonly estimatedUsd: number;
91
+ /** Sorted, deduped. */
92
+ readonly unavailableReasons: readonly string[];
93
+ };
94
+ readonly errors: {
95
+ readonly total: number;
96
+ readonly byType: Readonly<Record<string, number>>;
97
+ };
98
+ readonly stepCount: number;
99
+ }
100
+ /**
101
+ * Coverage rollup: registered-vs-invoked across the run. All arrays are
102
+ * sorted ascending and deduped so the value is stable for diffing.
103
+ */
104
+ export interface AgentRunCoverage {
105
+ readonly toolsAvailable: readonly string[];
106
+ readonly toolsInvoked: readonly string[];
107
+ readonly toolsUnused: readonly string[];
108
+ readonly modelsUsed: readonly string[];
109
+ readonly providersUsed: readonly string[];
110
+ }
111
+ export declare class AgentRunCollector {
112
+ #private;
113
+ /** True once `markRunEnded()` has been called for this invocation. */
114
+ get runEnded(): boolean;
115
+ /**
116
+ * Mark this run as logically ended. Callers use this to coordinate the
117
+ * `onRunEnd` hook between the success path (fires inside
118
+ * `buildAgentEndEvent`, before `stream.end()`) and the error path (fires
119
+ * inside `finishInvokeAgentSpan`'s finally). Idempotent — returns `true`
120
+ * the first time, `false` on subsequent calls.
121
+ */
122
+ markRunEnded(): boolean;
123
+ /** Record the tool names exposed on a single chat step. */
124
+ noteAvailableTools(tools: readonly {
125
+ readonly name: string;
126
+ }[] | undefined): void;
127
+ beginChat(span: Span, init: {
128
+ readonly stepNumber: number;
129
+ readonly model: Model;
130
+ readonly provider?: string;
131
+ }): void;
132
+ endChat(span: Span, message: AssistantMessage, fields: {
133
+ readonly costUsd: number | undefined;
134
+ readonly costUnavailableReason: string | undefined;
135
+ }): void;
136
+ /**
137
+ * Stamp the chat span as failed without a finalized AssistantMessage. Used
138
+ * by the `catch` arm of `streamAssistantResponse` so error chats still
139
+ * appear in the run summary.
140
+ */
141
+ failChat(span: Span, fields: {
142
+ readonly errorType: string;
143
+ }): void;
144
+ beginTool(span: Span, init: {
145
+ readonly toolCallId: string;
146
+ readonly toolName: string;
147
+ }): void;
148
+ endTool(span: Span, fields: {
149
+ readonly status: ToolStatus;
150
+ readonly errorType: string | undefined;
151
+ }): void;
152
+ /**
153
+ * Record a tool that never produced a span — pre-run interrupt or tail
154
+ * sweep. The LLM still asked for it, so it counts toward
155
+ * {@link AgentRunCoverage.toolsInvoked}.
156
+ */
157
+ recordOrphanTool(record: {
158
+ readonly toolCallId: string;
159
+ readonly toolName: string;
160
+ readonly status: ToolStatus;
161
+ }): void;
162
+ /** Build the immutable summary value from buffered records. */
163
+ snapshot(opts: {
164
+ readonly stepCount: number;
165
+ }): {
166
+ readonly summary: AgentRunSummary;
167
+ readonly coverage: AgentRunCoverage;
168
+ };
169
+ }
170
+ /**
171
+ * Fold multiple per-run summaries into one. Pure aggregation — useful when a
172
+ * caller (verify pass, benchmark harness) drives the agent loop N times and
173
+ * needs a single rollup across all invocations.
174
+ *
175
+ * Counters sum element-wise. Sets (cost reasons, error types, per-tool
176
+ * counters) merge by key. Numeric totals sum. The output is in the same
177
+ * shape as a single `AgentRunSummary`, so all dashboards and persistence
178
+ * layers handle it uniformly.
179
+ */
180
+ export declare function aggregateAgentRunSummaries(summaries: readonly AgentRunSummary[]): AgentRunSummary;
181
+ /** Union-merge multiple coverage values, preserving the sorted+deduped invariant. */
182
+ export declare function aggregateAgentRunCoverage(coverages: readonly AgentRunCoverage[]): AgentRunCoverage;
183
+ /** Empty `AgentRunSummary` constant. Exported for tests and default-initializers. */
184
+ export declare function emptyAgentRunSummary(): AgentRunSummary;
185
+ /** Empty `AgentRunCoverage` constant. Exported for tests and default-initializers. */
186
+ export declare function emptyAgentRunCoverage(): AgentRunCoverage;
187
+ /**
188
+ * Distinguishable error class thrown when `beforeToolCall` returns
189
+ * `{ block: true }`. Lets the catch arm of `runTool` set the terminal status
190
+ * on the execute_tool span to `"blocked"` instead of conflating with a real
191
+ * tool exception.
192
+ */
193
+ export declare class ToolCallBlockedError extends Error {
194
+ readonly name = "ToolCallBlockedError";
195
+ constructor(reason?: string);
196
+ }