@matthewfl/pi-contemplator 0.0.10 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/package.json +6 -6
- package/src/agents/contemplator/agent.ts +325 -91
- package/src/agents/contemplator/prompts.ts +6 -6
- package/src/agents/observer/agent.ts +96 -39
- package/src/agents/observer/prompts.ts +19 -10
- package/src/agents/reviewer/agent.ts +24 -4
- package/src/agents/reviewer/prompts.ts +1 -1
- package/src/agents/reviewer/tools.ts +24 -9
- package/src/agents/stream-errors.ts +1 -1
- package/src/agents/summarizer/agent.ts +597 -0
- package/src/agents/summarizer/prompts.ts +46 -0
- package/src/agents/summarizer/sampling.ts +80 -0
- package/src/commands/contemplator-view.ts +22 -1
- package/src/commands/settings.ts +95 -70
- package/src/commands/status.ts +60 -36
- package/src/commands/summarizer-view.ts +58 -0
- package/src/commands/view.ts +22 -10
- package/src/config.ts +30 -37
- package/src/hooks/compaction-hook.ts +32 -17
- package/src/hooks/compaction-resume.ts +4 -4
- package/src/hooks/compaction-trigger.ts +33 -11
- package/src/hooks/consolidation-trigger.ts +245 -215
- package/src/memory-citations.ts +37 -0
- package/src/required-tool-choice.ts +28 -0
- package/src/runtime.ts +115 -32
- package/src/session-ledger/fold.ts +82 -53
- package/src/session-ledger/index.ts +1 -0
- package/src/session-ledger/pools.ts +77 -0
- package/src/session-ledger/progress.ts +8 -19
- package/src/session-ledger/projection.ts +45 -177
- package/src/session-ledger/recall.ts +129 -127
- package/src/session-ledger/render-summary.ts +20 -19
- package/src/session-ledger/search.ts +99 -115
- package/src/session-ledger/types.ts +103 -77
- package/src/tools/compact-context.ts +1 -1
- package/src/tools/recall-observation.ts +99 -459
- package/src/tools/search-memories.ts +31 -72
- package/src/agents/dropper/agent.ts +0 -291
- package/src/agents/dropper/coverage.ts +0 -128
- package/src/agents/dropper/pool.ts +0 -67
- package/src/agents/dropper/prompts.ts +0 -48
- package/src/agents/reflector/agent.ts +0 -213
- package/src/agents/reflector/prompts.ts +0 -81
|
@@ -2,16 +2,13 @@ import { Type } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import type { Static } from "typebox";
|
|
4
4
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
searchMemories,
|
|
7
|
-
type MemorySearchResult,
|
|
8
|
-
} from "../session-ledger/search.js";
|
|
9
|
-
import type { Entry } from "../session-ledger/index.js";
|
|
10
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
+
import { searchMemories, type MemorySearchResult, type SearchMemoriesOptions } from "../session-ledger/search.js";
|
|
7
|
+
import type { Entry } from "../session-ledger/index.js";
|
|
11
8
|
|
|
12
9
|
export const SEARCH_MEMORIES_TOOL_NAME = "search_memories";
|
|
13
10
|
export const SEARCH_MEMORIES_DESCRIPTION =
|
|
14
|
-
"Search recorded
|
|
11
|
+
"Search recorded observations, cited summaries, and advisory review results by topic or keywords. Use recall with a result id to inspect exact evidence or walk summary citations.";
|
|
15
12
|
|
|
16
13
|
export type SearchMemoriesArgs = Static<typeof SEARCH_MEMORIES_PARAMETERS>;
|
|
17
14
|
|
|
@@ -19,7 +16,7 @@ export type SearchDetails = {
|
|
|
19
16
|
query: string;
|
|
20
17
|
limit: number;
|
|
21
18
|
observationsSearched: number;
|
|
22
|
-
|
|
19
|
+
summariesSearched: number;
|
|
23
20
|
reviewsSearched: number;
|
|
24
21
|
results: MemorySearchResult[];
|
|
25
22
|
};
|
|
@@ -29,101 +26,63 @@ function formatResult(result: MemorySearchResult): string {
|
|
|
29
26
|
const label = result.outcome === "proposal"
|
|
30
27
|
? `${result.scope} proposal${result.title ? ` — ${result.title}` : ""}`
|
|
31
28
|
: `${result.scope} review concluded with no proposal`;
|
|
32
|
-
|
|
29
|
+
const forward = result.citedBySummaryIds?.length ? `\n cited by summaries: [${result.citedBySummaryIds.join(", ")}]` : "";
|
|
30
|
+
return `- [${result.id}] ${label}: ${result.content}${forward}`;
|
|
33
31
|
}
|
|
34
|
-
const
|
|
32
|
+
const visibility = result.visibility === "summarized" ? " [summarized away]" : " [visible]";
|
|
35
33
|
const relevance = result.relevance ? ` [${result.relevance}]` : "";
|
|
34
|
+
const retention = result.retention ? ` [${result.retention}]` : "";
|
|
36
35
|
const timestamp = result.timestamp ? ` ${result.timestamp}` : "";
|
|
37
|
-
|
|
36
|
+
const graph = [
|
|
37
|
+
result.consumedBySummaryId ? `consumed by [${result.consumedBySummaryId}]` : undefined,
|
|
38
|
+
result.citedBySummaryIds?.length ? `cited by [${result.citedBySummaryIds.join(", ")}]` : undefined,
|
|
39
|
+
].filter(Boolean).join("; ");
|
|
40
|
+
return `- ${result.kind} [${result.id}]${visibility}${timestamp}${relevance}${retention}: ${result.content}${graph ? `\n ${graph}` : ""}`;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export const SEARCH_MEMORIES_PARAMETERS = Type.Object({
|
|
41
|
-
query: Type.String({
|
|
42
|
-
|
|
43
|
-
}),
|
|
44
|
-
limit: Type.Optional(
|
|
45
|
-
Type.Integer({
|
|
46
|
-
minimum: 1,
|
|
47
|
-
maximum: 20,
|
|
48
|
-
description: "Maximum results to return (default 8).",
|
|
49
|
-
}),
|
|
50
|
-
),
|
|
44
|
+
query: Type.String({ description: "Topic, phrase, or distinctive keywords to search for." }),
|
|
45
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Maximum results to return (default 8)." })),
|
|
51
46
|
});
|
|
52
47
|
|
|
53
|
-
export function executeSearchMemories(branchEntries: Entry[], params: SearchMemoriesArgs): { content: [{ type: "text"; text: string }]; details: SearchDetails } {
|
|
48
|
+
export function executeSearchMemories(branchEntries: Entry[], params: SearchMemoriesArgs, options: SearchMemoriesOptions = {}): { content: [{ type: "text"; text: string }]; details: SearchDetails } {
|
|
54
49
|
const query = params.query.trim();
|
|
55
50
|
const limit = params.limit ?? 8;
|
|
56
51
|
if (!query) {
|
|
57
|
-
const details: SearchDetails = {
|
|
58
|
-
query,
|
|
59
|
-
limit,
|
|
60
|
-
observationsSearched: 0,
|
|
61
|
-
reflectionsSearched: 0,
|
|
62
|
-
reviewsSearched: 0,
|
|
63
|
-
results: [],
|
|
64
|
-
};
|
|
52
|
+
const details: SearchDetails = { query, limit, observationsSearched: 0, summariesSearched: 0, reviewsSearched: 0, results: [] };
|
|
65
53
|
return { content: [{ type: "text", text: "Search query must not be empty." }], details };
|
|
66
54
|
}
|
|
67
|
-
|
|
68
|
-
const search = searchMemories(branchEntries, query, limit);
|
|
55
|
+
const search = searchMemories(branchEntries, query, limit, options);
|
|
69
56
|
const details: SearchDetails = { ...search, limit };
|
|
57
|
+
const counts = `${search.observationsSearched} observations, ${search.summariesSearched} summaries, and ${search.reviewsSearched} review results`;
|
|
70
58
|
const text = search.results.length
|
|
71
|
-
? [
|
|
72
|
-
|
|
73
|
-
...search.results.map(formatResult),
|
|
74
|
-
"Use recall(<id>) for exact source context.",
|
|
75
|
-
].join("\n")
|
|
76
|
-
: `No memories matched ${JSON.stringify(query)} (searched ${search.observationsSearched} observations, ${search.reflectionsSearched} reflections, and ${search.reviewsSearched} review results). Try alternate or more distinctive keywords.`;
|
|
59
|
+
? [`Found ${search.results.length} matching memories (searched ${counts}):`, ...search.results.map(formatResult), "Use recall(<id>) for exact content and immediate graph links."].join("\n")
|
|
60
|
+
: `No memories matched ${JSON.stringify(query)} (searched ${counts}). Try alternate or more distinctive keywords.`;
|
|
77
61
|
return { content: [{ type: "text", text }], details };
|
|
78
62
|
}
|
|
79
63
|
|
|
80
|
-
export function createSearchMemoriesAgentTool(getBranch: () => Entry[]): AgentTool<typeof SEARCH_MEMORIES_PARAMETERS> {
|
|
81
|
-
return {
|
|
82
|
-
name: SEARCH_MEMORIES_TOOL_NAME,
|
|
83
|
-
label: "Search memories",
|
|
84
|
-
description: SEARCH_MEMORIES_DESCRIPTION,
|
|
85
|
-
parameters: SEARCH_MEMORIES_PARAMETERS,
|
|
86
|
-
execute: async (_toolCallId, params) => executeSearchMemories(getBranch(), params),
|
|
87
|
-
};
|
|
64
|
+
export function createSearchMemoriesAgentTool(getBranch: () => Entry[], options: SearchMemoriesOptions = {}): AgentTool<typeof SEARCH_MEMORIES_PARAMETERS> {
|
|
65
|
+
return { name: SEARCH_MEMORIES_TOOL_NAME, label: "Search memories", description: SEARCH_MEMORIES_DESCRIPTION, parameters: SEARCH_MEMORIES_PARAMETERS, execute: async (_id, params) => executeSearchMemories(getBranch(), params, options) };
|
|
88
66
|
}
|
|
89
67
|
|
|
90
68
|
export const searchMemoriesTool = defineTool({
|
|
91
69
|
name: SEARCH_MEMORIES_TOOL_NAME,
|
|
92
70
|
label: "Search observational memories",
|
|
93
71
|
description: SEARCH_MEMORIES_DESCRIPTION,
|
|
94
|
-
promptSnippet:
|
|
95
|
-
"Use search_memories(query) to find relevant older observations or reflections, then use recall(id) when exact source context matters.",
|
|
72
|
+
promptSnippet: "Use search_memories(query) to find older observations, summaries, or reviews, then use recall(id) when exact context matters.",
|
|
96
73
|
promptGuidelines: [
|
|
97
|
-
"Use search_memories when
|
|
98
|
-
"Search with
|
|
99
|
-
"
|
|
100
|
-
"Do not assume the absence of results means the fact never occurred; search with alternate wording or narrower keywords.",
|
|
74
|
+
"Use search_memories when current context may be missing earlier decisions, constraints, preferences, outcomes, or rationale.",
|
|
75
|
+
"Search with distinctive keywords; visible and summarized-away memories are both searched.",
|
|
76
|
+
"Use recall with an exact 12-character id to inspect a node and walk its citation links.",
|
|
101
77
|
],
|
|
102
78
|
parameters: SEARCH_MEMORIES_PARAMETERS,
|
|
103
|
-
renderCall(args) {
|
|
104
|
-
return new Text(`search_memories ${JSON.stringify(args.query)}`, 0, 0);
|
|
105
|
-
},
|
|
79
|
+
renderCall(args) { return new Text(`search_memories ${JSON.stringify(args.query)}`, 0, 0); },
|
|
106
80
|
renderResult(result) {
|
|
107
81
|
const details = result.details as SearchDetails | undefined;
|
|
108
|
-
const text = result.content
|
|
109
|
-
|
|
110
|
-
(part): part is { type: "text"; text: string } =>
|
|
111
|
-
part.type === "text" && typeof part.text === "string",
|
|
112
|
-
)
|
|
113
|
-
.map((part) => part.text)
|
|
114
|
-
.join("\n");
|
|
115
|
-
return new Text(
|
|
116
|
-
text ||
|
|
117
|
-
(details
|
|
118
|
-
? `${details.results.length} memory results`
|
|
119
|
-
: "search_memories"),
|
|
120
|
-
0,
|
|
121
|
-
0,
|
|
122
|
-
);
|
|
123
|
-
},
|
|
124
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
125
|
-
return executeSearchMemories(ctx.sessionManager.getBranch() as Entry[], params);
|
|
82
|
+
const text = result.content.filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
|
|
83
|
+
return new Text(text || (details ? `${details.results.length} memory results` : "search_memories"), 0, 0);
|
|
126
84
|
},
|
|
85
|
+
async execute(_id, params, _signal, _onUpdate, ctx) { return executeSearchMemories(ctx.sessionManager.getBranch() as Entry[], params); },
|
|
127
86
|
});
|
|
128
87
|
|
|
129
88
|
export function registerSearchMemoriesTool(pi: ExtensionAPI): void {
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
4
|
-
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
5
|
-
import type { Static } from "typebox";
|
|
6
|
-
import { debugLog } from "../../debug-log.js";
|
|
7
|
-
import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "../../model-budget.js";
|
|
8
|
-
import { logAgentStreamError } from "../stream-errors.js";
|
|
9
|
-
import { reflectionToSummaryLine, type Observation, type Reflection } from "../../session-ledger/index.js";
|
|
10
|
-
import { DROPPER_SYSTEM } from "./prompts.js";
|
|
11
|
-
import {
|
|
12
|
-
REFLECTION_COVERAGE_DROP_RANK,
|
|
13
|
-
coverageTierForObservation,
|
|
14
|
-
reflectionCoverageMap,
|
|
15
|
-
summarizeCoverageByRelevance,
|
|
16
|
-
summarizeCoverageByRelevanceForIds,
|
|
17
|
-
observationToDropperLine,
|
|
18
|
-
} from "./coverage.js";
|
|
19
|
-
import { observationPoolMetrics } from "./pool.js";
|
|
20
|
-
import type { LlmUsageInput } from "../../runtime.js";
|
|
21
|
-
export {
|
|
22
|
-
maxDropCountForPool,
|
|
23
|
-
observationPoolFullness,
|
|
24
|
-
observationPoolMetrics,
|
|
25
|
-
} from "./pool.js";
|
|
26
|
-
export type { ObservationPoolMetrics } from "./pool.js";
|
|
27
|
-
export {
|
|
28
|
-
REFLECTION_COVERAGE_TIERS,
|
|
29
|
-
coverageTierForObservation,
|
|
30
|
-
emptyCoverageSummaryByRelevance,
|
|
31
|
-
observationToDropperLine,
|
|
32
|
-
reflectionCoverageMap,
|
|
33
|
-
reflectionCoverageTierForCount,
|
|
34
|
-
reflectionSupportCounts,
|
|
35
|
-
summarizeCoverageByRelevance,
|
|
36
|
-
summarizeCoverageByRelevanceForIds,
|
|
37
|
-
summarizeCoverageTransitionsByRelevance,
|
|
38
|
-
} from "./coverage.js";
|
|
39
|
-
export type { CoverageSummaryByRelevance, CoverageTransitionSummaryByRelevance, ReflectionCoverageTier } from "./coverage.js";
|
|
40
|
-
|
|
41
|
-
interface RunDropperArgs {
|
|
42
|
-
model: Model<any>;
|
|
43
|
-
apiKey: string;
|
|
44
|
-
headers?: Record<string, string>;
|
|
45
|
-
reflections: Reflection[];
|
|
46
|
-
observations: Observation[];
|
|
47
|
-
targetTokens: number;
|
|
48
|
-
signal?: AbortSignal;
|
|
49
|
-
agentLoop?: typeof agentLoop;
|
|
50
|
-
maxTurns?: number;
|
|
51
|
-
thinkingLevel?: ModelThinkingLevel;
|
|
52
|
-
recordUsage?: (usage: LlmUsageInput) => void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const RELEVANCE_DROP_RANK: Record<Observation["relevance"], number> = {
|
|
56
|
-
low: 0,
|
|
57
|
-
medium: 1,
|
|
58
|
-
high: 2,
|
|
59
|
-
critical: 3,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const DropObservationsSchema = Type.Object({
|
|
63
|
-
ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
64
|
-
reason: Type.Optional(Type.String()),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
type DropObservationsArgs = Static<typeof DropObservationsSchema>;
|
|
68
|
-
|
|
69
|
-
function joinOrEmpty(items: string[]): string {
|
|
70
|
-
return items.length ? items.join("\n") : "(none yet)";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function relevanceCounts(observations: readonly Observation[]): Record<Observation["relevance"], number> {
|
|
74
|
-
return observations.reduce<Record<Observation["relevance"], number>>((counts, observation) => {
|
|
75
|
-
counts[observation.relevance]++;
|
|
76
|
-
return counts;
|
|
77
|
-
}, { low: 0, medium: 0, high: 0, critical: 0 });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function normalizeDropObservationIds(
|
|
81
|
-
ids: readonly string[] | undefined,
|
|
82
|
-
observations: readonly Observation[],
|
|
83
|
-
): string[] | undefined {
|
|
84
|
-
if (!ids || ids.length === 0) return undefined;
|
|
85
|
-
const allowed = new Map(observations.map((observation) => [observation.id, observation]));
|
|
86
|
-
const result: string[] = [];
|
|
87
|
-
const seen = new Set<string>();
|
|
88
|
-
for (const id of ids) {
|
|
89
|
-
const observation = allowed.get(id);
|
|
90
|
-
if (!observation) continue;
|
|
91
|
-
if (seen.has(id)) continue;
|
|
92
|
-
seen.add(id);
|
|
93
|
-
result.push(id);
|
|
94
|
-
}
|
|
95
|
-
return result.length > 0 ? result : undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function timestampRank(timestamp: string): number {
|
|
99
|
-
const parsed = Date.parse(timestamp);
|
|
100
|
-
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function selectDropCandidates(
|
|
104
|
-
ids: readonly string[],
|
|
105
|
-
observations: readonly Observation[],
|
|
106
|
-
maxDrops: number,
|
|
107
|
-
reflections: readonly Reflection[] = [],
|
|
108
|
-
): string[] {
|
|
109
|
-
if (maxDrops <= 0 || ids.length === 0) return [];
|
|
110
|
-
|
|
111
|
-
const byId = new Map(observations.map((observation) => [observation.id, observation]));
|
|
112
|
-
const coverageById = reflectionCoverageMap(observations, reflections);
|
|
113
|
-
const firstProposalIndex = new Map<string, number>();
|
|
114
|
-
for (let i = 0; i < ids.length; i++) {
|
|
115
|
-
const id = ids[i];
|
|
116
|
-
if (!firstProposalIndex.has(id)) firstProposalIndex.set(id, i);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return Array.from(firstProposalIndex.entries())
|
|
120
|
-
.map(([id, index]) => ({ id, index, observation: byId.get(id) }))
|
|
121
|
-
.filter((candidate): candidate is { id: string; index: number; observation: Observation } =>
|
|
122
|
-
candidate.observation !== undefined
|
|
123
|
-
)
|
|
124
|
-
.sort((a, b) => {
|
|
125
|
-
const coverageDelta = REFLECTION_COVERAGE_DROP_RANK[coverageTierForObservation(a.observation, coverageById)]
|
|
126
|
-
- REFLECTION_COVERAGE_DROP_RANK[coverageTierForObservation(b.observation, coverageById)];
|
|
127
|
-
const relevanceDelta = RELEVANCE_DROP_RANK[a.observation.relevance] - RELEVANCE_DROP_RANK[b.observation.relevance];
|
|
128
|
-
const ageDelta = timestampRank(a.observation.timestamp) - timestampRank(b.observation.timestamp);
|
|
129
|
-
return coverageDelta || relevanceDelta || ageDelta || a.index - b.index;
|
|
130
|
-
})
|
|
131
|
-
.slice(0, maxDrops)
|
|
132
|
-
.map((candidate) => candidate.id);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export async function runDropper(args: RunDropperArgs): Promise<string[] | undefined> {
|
|
136
|
-
const { model, apiKey, headers, reflections, observations, targetTokens, signal } = args;
|
|
137
|
-
if (observations.length === 0) return undefined;
|
|
138
|
-
|
|
139
|
-
const metrics = observationPoolMetrics(observations, targetTokens);
|
|
140
|
-
const { observationTokens, fullness, tokensOverTarget, maxDropsAllowed } = metrics;
|
|
141
|
-
const coverageById = reflectionCoverageMap(observations, reflections);
|
|
142
|
-
const coverageSummaryByRelevance = summarizeCoverageByRelevance(observations, coverageById);
|
|
143
|
-
debugLog("dropper.agent_start", {
|
|
144
|
-
activeObservationCount: observations.length,
|
|
145
|
-
reflectionCount: reflections.length,
|
|
146
|
-
observationTokens,
|
|
147
|
-
targetTokens,
|
|
148
|
-
tokensOverTarget,
|
|
149
|
-
fullness,
|
|
150
|
-
maxDropsAllowed,
|
|
151
|
-
relevanceCounts: relevanceCounts(observations),
|
|
152
|
-
coverageSummaryByRelevance,
|
|
153
|
-
});
|
|
154
|
-
if (maxDropsAllowed <= 0) {
|
|
155
|
-
debugLog("dropper.result", {
|
|
156
|
-
reason: "not_over_target",
|
|
157
|
-
toolCallCount: 0,
|
|
158
|
-
rawRequestedIdsCount: 0,
|
|
159
|
-
acceptedCandidateCount: 0,
|
|
160
|
-
selectedDropsCount: 0,
|
|
161
|
-
selectedDropTokens: 0,
|
|
162
|
-
selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds([], observations, coverageById),
|
|
163
|
-
maxDropsAllowed,
|
|
164
|
-
});
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const proposedDropIds: string[] = [];
|
|
169
|
-
const proposed = new Set<string>();
|
|
170
|
-
const allowed = new Map(observations.map((observation) => [observation.id, observation]));
|
|
171
|
-
let toolCallCount = 0;
|
|
172
|
-
let rawRequestedIdsCount = 0;
|
|
173
|
-
let missingIdsCount = 0;
|
|
174
|
-
let criticalCandidateIdsCount = 0;
|
|
175
|
-
let duplicateInRequestCount = 0;
|
|
176
|
-
let duplicateInRunCount = 0;
|
|
177
|
-
|
|
178
|
-
const dropObservations: AgentTool<typeof DropObservationsSchema> = {
|
|
179
|
-
name: "drop_observations",
|
|
180
|
-
label: "Drop observations",
|
|
181
|
-
description: "Propose active observation ids that are safe to remove from compacted memory.",
|
|
182
|
-
parameters: DropObservationsSchema,
|
|
183
|
-
execute: async (_id, params: DropObservationsArgs) => {
|
|
184
|
-
toolCallCount++;
|
|
185
|
-
rawRequestedIdsCount += params.ids.length;
|
|
186
|
-
const seenInRequest = new Set<string>();
|
|
187
|
-
let added = 0;
|
|
188
|
-
let requestMissingIds = 0;
|
|
189
|
-
let requestCriticalCandidateIds = 0;
|
|
190
|
-
let requestDuplicateIds = 0;
|
|
191
|
-
let requestDuplicateInRunIds = 0;
|
|
192
|
-
for (const id of params.ids) {
|
|
193
|
-
const observation = allowed.get(id);
|
|
194
|
-
if (!observation) {
|
|
195
|
-
missingIdsCount++;
|
|
196
|
-
requestMissingIds++;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (seenInRequest.has(id)) {
|
|
200
|
-
duplicateInRequestCount++;
|
|
201
|
-
requestDuplicateIds++;
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
seenInRequest.add(id);
|
|
205
|
-
if (proposed.has(id)) {
|
|
206
|
-
duplicateInRunCount++;
|
|
207
|
-
requestDuplicateInRunIds++;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
proposed.add(id);
|
|
211
|
-
proposedDropIds.push(id);
|
|
212
|
-
if (observation.relevance === "critical") {
|
|
213
|
-
criticalCandidateIdsCount++;
|
|
214
|
-
requestCriticalCandidateIds++;
|
|
215
|
-
}
|
|
216
|
-
added++;
|
|
217
|
-
}
|
|
218
|
-
debugLog("dropper.tool_call", {
|
|
219
|
-
toolCallCount,
|
|
220
|
-
rawRequestedIdsCount: params.ids.length,
|
|
221
|
-
acceptedIdsCount: added,
|
|
222
|
-
missingIdsCount: requestMissingIds,
|
|
223
|
-
criticalCandidateIdsCount: requestCriticalCandidateIds,
|
|
224
|
-
duplicateInRequestCount: requestDuplicateIds,
|
|
225
|
-
duplicateInRunCount: requestDuplicateInRunIds,
|
|
226
|
-
totalCandidates: proposedDropIds.length,
|
|
227
|
-
maxDropsAllowed,
|
|
228
|
-
});
|
|
229
|
-
return {
|
|
230
|
-
content: [{ type: "text", text: `Queued ${added} drop candidate${added === 1 ? "" : "s"}. Candidates this run: ${proposedDropIds.length}. Maximum drops allowed: ${maxDropsAllowed}.` }],
|
|
231
|
-
details: { added, totalCandidates: proposedDropIds.length, maxDropsAllowed },
|
|
232
|
-
};
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const fullnessPercent = Math.round(fullness * 100);
|
|
237
|
-
const userText = `CURRENT REFLECTIONS:\n${joinOrEmpty(reflections.map(reflectionToSummaryLine))}\n\nCURRENT OBSERVATIONS:\n${joinOrEmpty(observations.map((observation) => observationToDropperLine(observation, coverageTierForObservation(observation, coverageById))))}\n\nActive observation pool: ~${observationTokens.toLocaleString()} tokens; target: ~${targetTokens.toLocaleString()} tokens; fullness against target: ~${fullnessPercent.toLocaleString()}%; over target by ~${tokensOverTarget.toLocaleString()} tokens.\nMaximum drops allowed this run: ${maxDropsAllowed.toLocaleString()} observation${maxDropsAllowed === 1 ? "" : "s"}. This maximum is sized to move the active pool toward the target if every proposed drop is clearly safe.\nThis maximum is a hard upper bound, not a target. Drop fewer or none if fewer observations are clearly safe.`;
|
|
238
|
-
const prompts: Message[] = [{ role: "user", content: [{ type: "text", text: userText }], timestamp: Date.now() }];
|
|
239
|
-
const context: AgentContext = { systemPrompt: DROPPER_SYSTEM, messages: [], tools: [dropObservations as AgentTool<any>] };
|
|
240
|
-
const reasoning = (model as { reasoning?: unknown }).reasoning;
|
|
241
|
-
const thinkingLevel = args.thinkingLevel ?? "low";
|
|
242
|
-
const effectiveMaxTurns = args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
|
|
243
|
-
let turnCount = 0;
|
|
244
|
-
const config: AgentLoopConfig = {
|
|
245
|
-
model,
|
|
246
|
-
apiKey,
|
|
247
|
-
headers,
|
|
248
|
-
maxTokens: boundedMaxTokens(model, AGENT_LOOP_MAX_TOKENS),
|
|
249
|
-
convertToLlm: (msgs) => msgs as Message[],
|
|
250
|
-
toolExecution: "sequential",
|
|
251
|
-
...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
|
|
252
|
-
...(effectiveMaxTurns !== undefined ? { shouldStopAfterTurn: () => ++turnCount >= effectiveMaxTurns } : {}),
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const loop = args.agentLoop ?? agentLoop;
|
|
256
|
-
const stream = loop(prompts, context, config, signal, streamSimple);
|
|
257
|
-
for await (const event of stream) {
|
|
258
|
-
// Tool execution collects candidate ids.
|
|
259
|
-
logAgentStreamError("dropper", event);
|
|
260
|
-
}
|
|
261
|
-
const result = await stream.result();
|
|
262
|
-
if (args.recordUsage) {
|
|
263
|
-
for (const message of result) {
|
|
264
|
-
if (message.role === "assistant" && message.usage) args.recordUsage(message.usage);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const droppedIds = selectDropCandidates(proposedDropIds, observations, maxDropsAllowed, reflections);
|
|
268
|
-
const reason = droppedIds.length > 0
|
|
269
|
-
? "selected_nonempty"
|
|
270
|
-
: toolCallCount === 0
|
|
271
|
-
? "no_tool_call"
|
|
272
|
-
: proposedDropIds.length === 0
|
|
273
|
-
? "all_filtered"
|
|
274
|
-
: "selected_empty";
|
|
275
|
-
const selectedDropTokens = droppedIds.reduce((sum, id) => sum + (allowed.get(id)?.tokenCount ?? 0), 0);
|
|
276
|
-
debugLog("dropper.result", {
|
|
277
|
-
reason,
|
|
278
|
-
toolCallCount,
|
|
279
|
-
rawRequestedIdsCount,
|
|
280
|
-
missingIdsCount,
|
|
281
|
-
criticalCandidateIdsCount,
|
|
282
|
-
duplicateInRequestCount,
|
|
283
|
-
duplicateInRunCount,
|
|
284
|
-
acceptedCandidateCount: proposedDropIds.length,
|
|
285
|
-
selectedDropsCount: droppedIds.length,
|
|
286
|
-
selectedDropTokens,
|
|
287
|
-
selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds(droppedIds, observations, coverageById),
|
|
288
|
-
maxDropsAllowed,
|
|
289
|
-
});
|
|
290
|
-
return droppedIds.length > 0 ? droppedIds : undefined;
|
|
291
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { Observation, Reflection } from "../../session-ledger/index.js";
|
|
2
|
-
|
|
3
|
-
export const REFLECTION_COVERAGE_TIERS = ["none", "partial", "strong"] as const;
|
|
4
|
-
export type ReflectionCoverageTier = typeof REFLECTION_COVERAGE_TIERS[number];
|
|
5
|
-
|
|
6
|
-
type Relevance = Observation["relevance"];
|
|
7
|
-
|
|
8
|
-
type CoverageBucket = Record<ReflectionCoverageTier, { count: number; tokens: number }>;
|
|
9
|
-
export type CoverageSummaryByRelevance = Record<Relevance, CoverageBucket>;
|
|
10
|
-
export type CoverageTransitionSummaryByRelevance = Record<Relevance, Record<string, { count: number; tokens: number }>>;
|
|
11
|
-
|
|
12
|
-
export const REFLECTION_COVERAGE_DROP_RANK: Record<ReflectionCoverageTier, number> = {
|
|
13
|
-
strong: 0,
|
|
14
|
-
partial: 1,
|
|
15
|
-
none: 2,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function reflectionSupportCounts(reflections: readonly Reflection[]): Map<string, number> {
|
|
19
|
-
const counts = new Map<string, number>();
|
|
20
|
-
for (const reflection of reflections) {
|
|
21
|
-
const uniqueIds = new Set(reflection.supportingObservationIds);
|
|
22
|
-
for (const id of uniqueIds) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
23
|
-
}
|
|
24
|
-
return counts;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function reflectionCoverageTierForCount(count: number): ReflectionCoverageTier {
|
|
28
|
-
if (count <= 0) return "none";
|
|
29
|
-
if (count === 1) return "partial";
|
|
30
|
-
return "strong";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function reflectionCoverageMap(
|
|
34
|
-
observations: readonly Observation[],
|
|
35
|
-
reflections: readonly Reflection[],
|
|
36
|
-
): Map<string, ReflectionCoverageTier> {
|
|
37
|
-
const counts = reflectionSupportCounts(reflections);
|
|
38
|
-
return new Map(observations.map((observation) => [
|
|
39
|
-
observation.id,
|
|
40
|
-
reflectionCoverageTierForCount(counts.get(observation.id) ?? 0),
|
|
41
|
-
]));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function emptyCoverageBucket(): CoverageBucket {
|
|
45
|
-
return {
|
|
46
|
-
none: { count: 0, tokens: 0 },
|
|
47
|
-
partial: { count: 0, tokens: 0 },
|
|
48
|
-
strong: { count: 0, tokens: 0 },
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function emptyCoverageSummaryByRelevance(): CoverageSummaryByRelevance {
|
|
53
|
-
return {
|
|
54
|
-
low: emptyCoverageBucket(),
|
|
55
|
-
medium: emptyCoverageBucket(),
|
|
56
|
-
high: emptyCoverageBucket(),
|
|
57
|
-
critical: emptyCoverageBucket(),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function summarizeCoverageByRelevance(
|
|
62
|
-
observations: readonly Observation[],
|
|
63
|
-
coverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
64
|
-
): CoverageSummaryByRelevance {
|
|
65
|
-
const summary = emptyCoverageSummaryByRelevance();
|
|
66
|
-
for (const observation of observations) {
|
|
67
|
-
const tier = coverageById.get(observation.id) ?? "none";
|
|
68
|
-
const bucket = summary[observation.relevance][tier];
|
|
69
|
-
bucket.count++;
|
|
70
|
-
bucket.tokens += observation.tokenCount;
|
|
71
|
-
}
|
|
72
|
-
return summary;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function summarizeCoverageByRelevanceForIds(
|
|
76
|
-
ids: readonly string[],
|
|
77
|
-
observations: readonly Observation[],
|
|
78
|
-
coverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
79
|
-
): CoverageSummaryByRelevance {
|
|
80
|
-
const byId = new Map(observations.map((observation) => [observation.id, observation]));
|
|
81
|
-
const selected = ids.flatMap((id) => {
|
|
82
|
-
const observation = byId.get(id);
|
|
83
|
-
return observation ? [observation] : [];
|
|
84
|
-
});
|
|
85
|
-
return summarizeCoverageByRelevance(selected, coverageById);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function emptyCoverageTransitionSummaryByRelevance(): CoverageTransitionSummaryByRelevance {
|
|
89
|
-
return {
|
|
90
|
-
low: {},
|
|
91
|
-
medium: {},
|
|
92
|
-
high: {},
|
|
93
|
-
critical: {},
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function summarizeCoverageTransitionsByRelevance(
|
|
98
|
-
observations: readonly Observation[],
|
|
99
|
-
beforeCoverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
100
|
-
afterCoverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
101
|
-
): CoverageTransitionSummaryByRelevance {
|
|
102
|
-
const summary = emptyCoverageTransitionSummaryByRelevance();
|
|
103
|
-
for (const observation of observations) {
|
|
104
|
-
const before = beforeCoverageById.get(observation.id) ?? "none";
|
|
105
|
-
const after = afterCoverageById.get(observation.id) ?? "none";
|
|
106
|
-
if (before === after) continue;
|
|
107
|
-
const key = `${before}->${after}`;
|
|
108
|
-
const bucket = summary[observation.relevance][key] ?? { count: 0, tokens: 0 };
|
|
109
|
-
bucket.count++;
|
|
110
|
-
bucket.tokens += observation.tokenCount;
|
|
111
|
-
summary[observation.relevance][key] = bucket;
|
|
112
|
-
}
|
|
113
|
-
return summary;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function observationToDropperLine(
|
|
117
|
-
observation: Observation,
|
|
118
|
-
coverage: ReflectionCoverageTier,
|
|
119
|
-
): string {
|
|
120
|
-
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] [coverage: ${coverage}] ${observation.content}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function coverageTierForObservation(
|
|
124
|
-
observation: Observation,
|
|
125
|
-
coverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
126
|
-
): ReflectionCoverageTier {
|
|
127
|
-
return coverageById.get(observation.id) ?? "none";
|
|
128
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { Observation } from "../../session-ledger/index.js";
|
|
2
|
-
|
|
3
|
-
export type ObservationPoolMetrics = {
|
|
4
|
-
observationTokens: number;
|
|
5
|
-
targetTokens: number;
|
|
6
|
-
tokensOverTarget: number;
|
|
7
|
-
fullness: number;
|
|
8
|
-
activeObservationCount: number;
|
|
9
|
-
droppableCount: number;
|
|
10
|
-
maxDropsAllowed: number;
|
|
11
|
-
overTarget: boolean;
|
|
12
|
-
ready: boolean;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function observationTokenSum(observations: readonly { tokenCount: number }[]): number {
|
|
16
|
-
return observations.reduce((sum, observation) => sum + observation.tokenCount, 0);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function observationPoolFullness(observationTokens: number, targetTokens: number): number {
|
|
20
|
-
if (!Number.isFinite(observationTokens) || observationTokens <= 0) return 0;
|
|
21
|
-
if (!Number.isFinite(targetTokens) || targetTokens <= 0) return 0;
|
|
22
|
-
return observationTokens / targetTokens;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function droppableObservationCount(observations: readonly Observation[]): number {
|
|
26
|
-
return observations.length;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function maxDropCountForPool(observations: readonly Observation[], observationTokens: number, targetTokens: number): number {
|
|
30
|
-
const activeObservationCount = observations.length;
|
|
31
|
-
if (activeObservationCount === 0) return 0;
|
|
32
|
-
if (!Number.isFinite(observationTokens) || observationTokens <= 0) return 0;
|
|
33
|
-
if (!Number.isFinite(targetTokens) || targetTokens < 0) return 0;
|
|
34
|
-
|
|
35
|
-
const tokensOverTarget = observationTokens - targetTokens;
|
|
36
|
-
if (tokensOverTarget <= 0) return 0;
|
|
37
|
-
|
|
38
|
-
const averageObservationTokens = observationTokens / activeObservationCount;
|
|
39
|
-
if (!Number.isFinite(averageObservationTokens) || averageObservationTokens <= 0) return 0;
|
|
40
|
-
|
|
41
|
-
const estimatedDrops = Math.ceil(tokensOverTarget / averageObservationTokens);
|
|
42
|
-
return Math.min(activeObservationCount, Math.max(1, estimatedDrops));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function observationPoolMetrics(
|
|
46
|
-
observations: readonly Observation[],
|
|
47
|
-
targetTokens: number,
|
|
48
|
-
): ObservationPoolMetrics {
|
|
49
|
-
const observationTokens = observationTokenSum(observations);
|
|
50
|
-
const fullness = observationPoolFullness(observationTokens, targetTokens);
|
|
51
|
-
const activeObservationCount = observations.length;
|
|
52
|
-
const droppableCount = droppableObservationCount(observations);
|
|
53
|
-
const tokensOverTarget = Math.max(0, observationTokens - targetTokens);
|
|
54
|
-
const maxDropsAllowed = maxDropCountForPool(observations, observationTokens, targetTokens);
|
|
55
|
-
const overTarget = Number.isFinite(targetTokens) && targetTokens >= 0 && observationTokens > targetTokens;
|
|
56
|
-
return {
|
|
57
|
-
observationTokens,
|
|
58
|
-
targetTokens,
|
|
59
|
-
tokensOverTarget,
|
|
60
|
-
fullness,
|
|
61
|
-
activeObservationCount,
|
|
62
|
-
droppableCount,
|
|
63
|
-
maxDropsAllowed,
|
|
64
|
-
overTarget,
|
|
65
|
-
ready: overTarget && maxDropsAllowed > 0,
|
|
66
|
-
};
|
|
67
|
-
}
|