@matthewfl/pi-contemplator 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/package.json +8 -6
- package/src/agents/contemplator/agent.ts +325 -91
- package/src/agents/contemplator/prompts.ts +6 -6
- package/src/agents/observer/agent.ts +14 -6
- package/src/agents/observer/prompts.ts +16 -7
- 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 +73 -69
- 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 +25 -32
- package/src/hooks/compaction-hook.ts +36 -19
- package/src/hooks/compaction-resume.ts +4 -4
- package/src/hooks/compaction-trigger.ts +96 -56
- package/src/hooks/consolidation-trigger.ts +213 -196
- package/src/memory-citations.ts +37 -0
- package/src/required-tool-choice.ts +28 -0
- package/src/runtime.ts +116 -33
- 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 +7 -18
- 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 +102 -75
- 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
|
@@ -6,7 +6,7 @@ export function buildContemplatorSystemPrompt(
|
|
|
6
6
|
|
|
7
7
|
Neither you nor the primary agent should be assumed to know the correct solution. You are jointly exploring a problem space from different perspectives. The primary agent interacts with the actual environment and carries out the work. You maintain a longer-term view of the reasoning, evidence, assumptions, alternatives, unresolved questions${reviewerEnabled ? ", and recurring structural patterns" : ""} that emerge over time.
|
|
8
8
|
|
|
9
|
-
You receive incremental observations and
|
|
9
|
+
You receive incremental observations and cited summaries produced by other agent loops. Some memories summarize user messages. Pay extra attention to memories about the user’s intent, priorities, constraints, corrections, and desired outcome.
|
|
10
10
|
|
|
11
11
|
You see only the memory ledger, not the primary agent’s live activity. Your understanding may be incomplete or slightly stale. Do not infer inactivity, failure, or lack of progress from missing recent results. A result may simply not have reached memory yet.
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ Do not treat the absence of a recent test result, tool result, or implementation
|
|
|
42
42
|
|
|
43
43
|
Do not encourage speculation merely because several possibilities can be imagined. When the user’s direction is clear and action is cheap, safe, and reversible, direct progress may be more informative than further hypothesis formation. Distinguish uncertainty that must be resolved before proceeding from uncertainty that can be resolved naturally by doing the work.
|
|
44
44
|
|
|
45
|
-
Your other high-value responsibility is reality-checking. Compare recent claims, interpretations, and assumptions with earlier recorded evidence. When a recent claim conflicts with an earlier observation,
|
|
45
|
+
Your other high-value responsibility is reality-checking. Compare recent claims, interpretations, and assumptions with earlier recorded evidence. When a recent claim conflicts with an earlier observation, summary, user-intent memory, or previously supported conclusion, make the contradiction visible and cite the memories on both sides.
|
|
46
46
|
|
|
47
47
|
Also look for opportunities to help the primary agent:
|
|
48
48
|
|
|
@@ -72,7 +72,7 @@ A useful probe may ask:
|
|
|
72
72
|
|
|
73
73
|
Questions should be grounded in the actual memories rather than generic problem-solving advice.
|
|
74
74
|
|
|
75
|
-
You have access to search_memories for finding older ${reviewerEnabled ? `observations,
|
|
75
|
+
You have access to search_memories for finding older ${reviewerEnabled ? `observations, summaries, and durable review results` : `observations and summaries`} on the current branch. Use it when the updates provided do not contain enough context, searching with distinctive terms rather than broad questions. Results include memory identifiers that you can cite in a probe${reviewerEnabled ? ` or review request` : ``}.
|
|
76
76
|
|
|
77
77
|
You also have recall for recovering exact source context behind a specific memory identifier. Use it when a result is important but compressed.
|
|
78
78
|
|
|
@@ -185,8 +185,8 @@ A good review request should:
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
${reviewerEnabled ?
|
|
188
|
-
`Your interventions are asynchronous.
|
|
189
|
-
`Your probes are delivered asynchronously.
|
|
188
|
+
`Your interventions are asynchronous. You must finish every update by calling one final-action tool: send_probe, request_review, or no_intervention. This requirement is bookkeeping, not pressure to intervene. no_intervention takes no arguments and is the preferred default whenever no specific, grounded, materially useful intervention is clearly warranted or usefulness is uncertain. Never send a probe merely to satisfy the tool requirement. A valid final-action call ends your turn immediately, so do not plan to add narration afterward. If an intervention tool reports an invalid memory citation, correct it and call the appropriate tool again; the later call replaces the earlier one.` :
|
|
189
|
+
`Your probes are delivered asynchronously. You must finish every update by calling one final-action tool: send_probe or no_intervention. This requirement is bookkeeping, not pressure to intervene. no_intervention takes no arguments and is the preferred default whenever no specific, grounded, materially useful probe is clearly warranted or usefulness is uncertain. Never send a probe merely to satisfy the tool requirement. A valid final-action call ends your turn immediately, so do not plan to add narration afterward. If send_probe reports an invalid memory citation, correct it and call send_probe again; the later call replaces the earlier one.`}
|
|
190
190
|
|
|
191
191
|
Prioritize:
|
|
192
192
|
|
|
@@ -203,7 +203,7 @@ ${
|
|
|
203
203
|
: ``
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
If no specific, grounded, materially useful intervention exists,
|
|
206
|
+
If no specific, grounded, materially useful intervention clearly exists, or if its usefulness is uncertain, call the argument-free no_intervention tool.`
|
|
207
207
|
.replace(/\n{3,}/g, "\n\n")
|
|
208
208
|
.trim();
|
|
209
209
|
}
|
|
@@ -8,7 +8,7 @@ import { logAgentStreamError } from "../stream-errors.js";
|
|
|
8
8
|
import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "../../model-budget.js";
|
|
9
9
|
import { OBSERVER_SYSTEM } from "./prompts.js";
|
|
10
10
|
import { nowTimestamp, truncateRecordContent } from "../../serialize.js";
|
|
11
|
-
import type { Observation, Relevance } from "../../session-ledger/index.js";
|
|
11
|
+
import type { Observation, Relevance, Retention } from "../../session-ledger/index.js";
|
|
12
12
|
import { estimateStringTokens } from "../../tokens.js";
|
|
13
13
|
import type { LlmUsageInput } from "../../runtime.js";
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ interface RunObserverArgs {
|
|
|
16
16
|
model: Model<any>;
|
|
17
17
|
apiKey: string;
|
|
18
18
|
headers?: Record<string, string>;
|
|
19
|
-
|
|
19
|
+
priorSummaries?: string[];
|
|
20
20
|
priorObservations: string[];
|
|
21
21
|
chunk: string;
|
|
22
22
|
allowedSourceEntryIds: string[];
|
|
@@ -34,6 +34,12 @@ const RelevanceSchema = Type.Union([
|
|
|
34
34
|
Type.Literal("critical"),
|
|
35
35
|
]);
|
|
36
36
|
|
|
37
|
+
const RetentionSchema = Type.Union([
|
|
38
|
+
Type.Literal("ephemeral"),
|
|
39
|
+
Type.Literal("contextual"),
|
|
40
|
+
Type.Literal("durable"),
|
|
41
|
+
]);
|
|
42
|
+
|
|
37
43
|
export const OBSERVATION_TIMESTAMP_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$";
|
|
38
44
|
|
|
39
45
|
const RecordObservationsSchema = Type.Object({
|
|
@@ -48,6 +54,7 @@ const RecordObservationsSchema = Type.Object({
|
|
|
48
54
|
description: "Single-line plain prose. No markdown, no tags, no embedded timestamp.",
|
|
49
55
|
}),
|
|
50
56
|
relevance: RelevanceSchema,
|
|
57
|
+
retention: Type.Optional(RetentionSchema),
|
|
51
58
|
sourceEntryIds: Type.Array(
|
|
52
59
|
Type.String({ minLength: 1 }),
|
|
53
60
|
{
|
|
@@ -86,7 +93,7 @@ export function normalizeSourceEntryIds(
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
export async function runObserver(args: RunObserverArgs): Promise<Observation[] | undefined> {
|
|
89
|
-
const { model, apiKey, headers,
|
|
96
|
+
const { model, apiKey, headers, priorSummaries = [], priorObservations, chunk, allowedSourceEntryIds, signal } = args;
|
|
90
97
|
const conversation = chunk.trim();
|
|
91
98
|
if (!conversation) return undefined;
|
|
92
99
|
|
|
@@ -121,6 +128,7 @@ export async function runObserver(args: RunObserverArgs): Promise<Observation[]
|
|
|
121
128
|
content,
|
|
122
129
|
timestamp: obs.timestamp,
|
|
123
130
|
relevance: obs.relevance as Relevance,
|
|
131
|
+
retention: (obs.retention ?? "contextual") as Retention,
|
|
124
132
|
sourceEntryIds,
|
|
125
133
|
tokenCount: estimateStringTokens(content),
|
|
126
134
|
});
|
|
@@ -142,13 +150,13 @@ export async function runObserver(args: RunObserverArgs): Promise<Observation[]
|
|
|
142
150
|
const now = nowTimestamp();
|
|
143
151
|
const userText = `Current local time: ${now}
|
|
144
152
|
|
|
145
|
-
CURRENT
|
|
146
|
-
${joinOrEmpty(
|
|
153
|
+
CURRENT SUMMARIES:
|
|
154
|
+
${joinOrEmpty(priorSummaries)}
|
|
147
155
|
|
|
148
156
|
CURRENT OBSERVATIONS:
|
|
149
157
|
${joinOrEmpty(priorObservations)}
|
|
150
158
|
|
|
151
|
-
Compress the following new conversation chunk into observations by calling record_observations one or more times. Do not restate facts already present in current
|
|
159
|
+
Compress the following new conversation chunk into observations by calling record_observations one or more times. Do not restate facts already present in current summaries or current observations. Prefer inline conversation timestamps when assigning times; fall back to the current local time above only if no message timestamp applies. Stop calling the tool and reply with a short plain-text confirmation once the chunk is fully covered.
|
|
152
160
|
|
|
153
161
|
NEW CONVERSATION CHUNK:
|
|
154
162
|
${conversation}`;
|
|
@@ -2,26 +2,27 @@ export const OBSERVER_SYSTEM = `You are the observation agent for a coding assis
|
|
|
2
2
|
|
|
3
3
|
These records are the ONLY information the assistant will have about past interactions once the raw conversation is compacted out of context. Anything you do not capture here will be forgotten. Anything you distort here will be remembered wrong. Take this seriously.
|
|
4
4
|
|
|
5
|
-
Your job is to compress a chunk of recent conversation into timestamped
|
|
5
|
+
Your job is to compress a chunk of recent conversation into timestamped observations with separate relevance and retention judgments by calling the record_observations tool. The observations you emit — together with cited summaries compressed from them — are the assistant's ONLY memory of this session after the raw conversation falls out of context.
|
|
6
6
|
|
|
7
7
|
You receive:
|
|
8
|
-
- Current
|
|
8
|
+
- Current summaries (compressed memories already recorded).
|
|
9
9
|
- Current observations (already-recorded observations, each shown as "[id] YYYY-MM-DD HH:MM [relevance] content").
|
|
10
10
|
- A new chunk of conversation with source entry labels and inline message timestamps. Each source block starts with "[Source entry id: <id>]" followed by content formatted as "[User @ YYYY-MM-DD HH:MM]:", "[Assistant @ ...]:", "[Tool result for <name> @ ...]:", custom messages, or branch summaries.
|
|
11
11
|
- A current local time fallback for observations that have no obvious message timestamp.
|
|
12
12
|
|
|
13
13
|
How you work:
|
|
14
|
-
1. Read
|
|
14
|
+
1. Read summaries and current observations so you know what is already captured.
|
|
15
15
|
2. Read the conversation chunk and identify what new information it contains.
|
|
16
16
|
3. Call record_observations with a batch covering part (or all) of the chunk.
|
|
17
17
|
4. Read the progress receipt. If content remains uncovered, call again. You may call the tool many times.
|
|
18
18
|
5. When the chunk is fully covered, STOP calling the tool and reply with a brief plain-text confirmation (one short sentence). That ends the run.
|
|
19
19
|
|
|
20
20
|
What to emit:
|
|
21
|
-
- Produce NEW observations for the new chunk only. Do not restate facts already present in
|
|
21
|
+
- Produce NEW observations for the new chunk only. Do not restate facts already present in summaries or current observations unless something has materially changed.
|
|
22
22
|
- Use the timestamp from the relevant conversation message. Fall back to current local time ONLY when no message timestamp applies.
|
|
23
23
|
- For every observation, include sourceEntryIds: the smallest exact set of "[Source entry id: ...]" ids that directly support the observation.
|
|
24
24
|
- Never invent source entry ids. Use only ids printed in the chunk. If an observation spans multiple turns or tool results, include every supporting source entry id.
|
|
25
|
+
- For every observation, choose retention independently from relevance. Recording the observation correctly comes first; never skip useful evidence because retention is uncertain.
|
|
25
26
|
- Observations with missing, empty, or invalid sourceEntryIds will be rejected and not recorded, so do not call record_observations until you can cite valid source ids.
|
|
26
27
|
- Group repeated similar tool calls into a single observation rather than one per call.
|
|
27
28
|
- Skip routine, low-information events. It is fine to emit zero observations if the chunk carries no new information — in that case, simply do not call the tool and end with a plain-text confirmation.
|
|
@@ -57,7 +58,7 @@ Use precise action verbs. Replace vague verbs with ones that clarify the nature
|
|
|
57
58
|
Frame state changes as supersession so the old state is explicit.
|
|
58
59
|
BAD: User prefers React Query now.
|
|
59
60
|
GOOD: User will use React Query (switching from SWR).
|
|
60
|
-
Why this matters: without supersession framing,
|
|
61
|
+
Why this matters: without supersession framing, a later summary may preserve both the old and the new as equally valid preferences.
|
|
61
62
|
|
|
62
63
|
Mark concrete completions explicitly.
|
|
63
64
|
Use "completed:", "resolved:", "confirmed working", or similar phrasing so future runs know not to redo the work.
|
|
@@ -73,7 +74,7 @@ If a single message contains multiple independent facts, intents, or events, emi
|
|
|
73
74
|
GOOD: User started a new job. + User will move to a new apartment next week.
|
|
74
75
|
BAD: Assistant recommended Lucia, NextAuth, and Clerk for auth, and user chose Lucia.
|
|
75
76
|
GOOD: Assistant recommended auth libraries: Lucia (session-based, minimal), NextAuth (OAuth-heavy, Next-native), Clerk (hosted, paid). + User chose Lucia.
|
|
76
|
-
Why this matters: a future query like "which auth library did the user pick?" can match a single-fact observation cleanly; a compound observation hides the decision inside a recommendation list.
|
|
77
|
+
Why this matters: a future query like "which auth library did the user pick?" can match a single-fact observation cleanly; a compound observation hides the decision inside a recommendation list and makes safe summarization harder.
|
|
77
78
|
|
|
78
79
|
Group repeated similar tool calls into a single observation rather than one per call.
|
|
79
80
|
BAD: Agent viewed src/auth.ts. Agent viewed src/users.ts. Agent viewed src/routes.ts.
|
|
@@ -104,10 +105,18 @@ Relevance levels (pick one per observation; this field drives future dropping):
|
|
|
104
105
|
- critical: user assertions about identity, role, or persistent preferences; explicit corrections ("no, don't do X"); concrete completions that future runs MUST NOT redo. These are highest-resistance, load-bearing observations and require the strongest evidence before leaving active memory. Why this matters: if a "critical" item is lost, the assistant may redo finished work, contradict a correction, or misrepresent who the user is.
|
|
105
106
|
- high: non-trivial technical decisions, architectural direction, unresolved blockers, key constraints. Worth keeping across many compactions.
|
|
106
107
|
- medium: task-level context that helps within the current work but isn't durable. The default when you are unsure between medium and high.
|
|
107
|
-
- low: routine tool-call acks, repetitive status updates, content trivially re-derivable from recent messages. The
|
|
108
|
+
- low: routine tool-call acks, repetitive status updates, content trivially re-derivable from recent messages. The summarizer considers relevance together with retention, age, and later evidence.
|
|
108
109
|
|
|
109
110
|
Do NOT default to "critical" or "high". Most observations are medium or low. Reserve "critical" for things that would cause real damage if forgotten.
|
|
110
111
|
|
|
112
|
+
Retention horizons (pick one independently from relevance):
|
|
113
|
+
|
|
114
|
+
- ephemeral: likely useful for only the next few steps; intermediate attempts, routine outputs, temporary readings, and local status.
|
|
115
|
+
- contextual: useful while related work continues or may soon resume; exact errors, hypotheses, partial state, subsystem details, and unresolved local choices. Use this when uncertain.
|
|
116
|
+
- durable: likely useful across context changes; persistent preferences, constraints, corrections, decisions, rationale, reusable findings, and significant outcomes.
|
|
117
|
+
|
|
118
|
+
A critical exact blocker can be contextual; a medium stable preference can be durable. Retention is a hint for summarizer sampling and judgment, never an instruction for code to remove anything automatically. Notice factual transitions—replacement, abandonment, resolution, moving away from a subject, resuming it, or invalidating an old result—but record the transition as ordinary evidence rather than commands to the summarizer.
|
|
119
|
+
|
|
111
120
|
BAD: relevance=critical for "Agent ran tests and they passed."
|
|
112
121
|
GOOD: relevance=low for "Agent ran tests and they passed." (routine; captured by a completion observation if it matters)
|
|
113
122
|
|
|
@@ -4,6 +4,7 @@ import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
|
4
4
|
import { hashId } from "../../ids.js";
|
|
5
5
|
import { boundedMaxTokens, REVIEWER_TOTAL_TOKEN_LIMIT } from "../../model-budget.js";
|
|
6
6
|
import type { LlmUsageInput } from "../../runtime.js";
|
|
7
|
+
import { recallMemorySources } from "../../session-ledger/recall.js";
|
|
7
8
|
import type { Entry, ReviewResult, StructuralReviewRequest } from "../../session-ledger/types.js";
|
|
8
9
|
import { createRecallAgentTool } from "../../tools/recall-observation.js";
|
|
9
10
|
import { createSearchMemoriesAgentTool } from "../../tools/search-memories.js";
|
|
@@ -94,18 +95,30 @@ function budgetExhaustedResult(request: StructuralReviewRequest): ReviewResult {
|
|
|
94
95
|
|
|
95
96
|
export async function runStructuralReview(args: RunStructuralReviewArgs): Promise<ReviewResult | undefined> {
|
|
96
97
|
let terminal: ReviewTerminalResult | undefined;
|
|
97
|
-
|
|
98
|
+
let warnedTerminal: ReviewTerminalResult | undefined;
|
|
99
|
+
const acceptTerminal = (candidate: ReviewTerminalResult, missingReferenceIds: string[]) => {
|
|
98
100
|
if (terminal) throw new Error("A structural reviewer may make only one terminal tool call.");
|
|
101
|
+
const overwritten = warnedTerminal !== undefined;
|
|
102
|
+
if (missingReferenceIds.length > 0) {
|
|
103
|
+
warnedTerminal = candidate;
|
|
104
|
+
return { terminal: false, overwritten };
|
|
105
|
+
}
|
|
99
106
|
terminal = candidate;
|
|
107
|
+
warnedTerminal = undefined;
|
|
108
|
+
return { terminal: true, overwritten };
|
|
109
|
+
};
|
|
110
|
+
const referenceExists = (id: string): boolean => {
|
|
111
|
+
const branch = args.getBranch();
|
|
112
|
+
return branch.some((entry) => entry.id === id) || recallMemorySources(branch, id).status === "found";
|
|
100
113
|
};
|
|
101
114
|
const searchMemories = createSearchMemoriesAgentTool(args.getBranch);
|
|
102
115
|
const recall = createRecallAgentTool(args.getBranch);
|
|
103
116
|
const searchChatHistory = createSearchChatHistoryAgentTool(args.getBranch);
|
|
104
117
|
const readChatHistory = createReadChatHistoryAgentTool(args.getBranch);
|
|
105
118
|
const scopeTool = args.request.scope === "workflow"
|
|
106
|
-
? createWorkflowProposalTool(acceptTerminal)
|
|
107
|
-
: createSoftwareProposalTool(acceptTerminal);
|
|
108
|
-
const noProposal = createNoProposalTool(args.request.scope, acceptTerminal);
|
|
119
|
+
? createWorkflowProposalTool(acceptTerminal, referenceExists)
|
|
120
|
+
: createSoftwareProposalTool(acceptTerminal, referenceExists);
|
|
121
|
+
const noProposal = createNoProposalTool(args.request.scope, acceptTerminal, referenceExists);
|
|
109
122
|
const tools = [
|
|
110
123
|
searchMemories as AgentTool<any>,
|
|
111
124
|
recall as AgentTool<any>,
|
|
@@ -179,6 +192,13 @@ export async function runStructuralReview(args: RunStructuralReviewArgs): Promis
|
|
|
179
192
|
// assistant usage that the real stream wrapper did not already account for.
|
|
180
193
|
const reportedOutputTokens = assistantOutputTokens(assistants);
|
|
181
194
|
totalOutputTokens += Math.max(0, reportedOutputTokens - streamedOutputTokens);
|
|
195
|
+
// A warned terminal call deliberately leaves shouldStopAfterTurn false so
|
|
196
|
+
// the reviewer can search/recall and replace it. If the reviewer instead
|
|
197
|
+
// ends its turn, honor the staged outcome exactly as the warning promised.
|
|
198
|
+
if (!terminal && warnedTerminal) {
|
|
199
|
+
terminal = warnedTerminal;
|
|
200
|
+
warnedTerminal = undefined;
|
|
201
|
+
}
|
|
182
202
|
return totalOutputTokens - iterationStartTokens;
|
|
183
203
|
};
|
|
184
204
|
|
|
@@ -28,7 +28,7 @@ First recall every memory cited in the request. Then search for:
|
|
|
28
28
|
- existing proposals, tools, abstractions, scripts, representations, or workflows that may already address it;
|
|
29
29
|
- evidence showing whether the pattern is isolated, temporary, or recurring.
|
|
30
30
|
|
|
31
|
-
Observations and
|
|
31
|
+
Observations and summaries are compressed. When they do not preserve enough detail to evaluate the suspected pattern, use search_chat_history to search the primary agent's recorded chat with a regular expression. The search is deterministic, scans newest entries first, and returns each match separately. It does not interpret concepts for you. Form concrete patterns from the review concern and use regular expression wild cards around a term when the returned match should include nearby text. Use read_chat_history with a returned entry id when you need the complete entry or preceding and following messages.
|
|
32
32
|
|
|
33
33
|
Do not search chat history indiscriminately or treat a missing regex match as proof that an event did not occur. Search for specific evidence that can support, weaken, or contextualize the concern.
|
|
34
34
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { Static, TSchema } from "typebox";
|
|
4
|
+
import { memoryReferenceIds } from "../../memory-citations.js";
|
|
4
5
|
import type { ReviewNoProposal, SoftwareReviewProposal, WorkflowReviewProposal } from "../../session-ledger/types.js";
|
|
5
6
|
|
|
6
7
|
const prose = (description: string) => Type.String({ minLength: 1, description });
|
|
@@ -39,19 +40,26 @@ export type WorkflowProposalArgs = Static<typeof WorkflowProposalSchema>;
|
|
|
39
40
|
export type SoftwareProposalArgs = Static<typeof SoftwareProposalSchema>;
|
|
40
41
|
export type NoProposalArgs = Static<typeof NoProposalSchema>;
|
|
41
42
|
export type ReviewTerminalResult = Omit<WorkflowReviewProposal, "id" | "version" | "reviewRequestId" | "createdAt" | "requestedBy"> | Omit<SoftwareReviewProposal, "id" | "version" | "reviewRequestId" | "createdAt" | "requestedBy"> | Omit<ReviewNoProposal, "id" | "version" | "reviewRequestId" | "createdAt" | "requestedBy">;
|
|
43
|
+
export type TerminalWriteResult = { terminal: boolean; overwritten: boolean };
|
|
44
|
+
export type ReviewTerminalWriter = (result: ReviewTerminalResult, missingReferenceIds: string[]) => TerminalWriteResult;
|
|
42
45
|
|
|
43
46
|
function trimOptional(value: string | undefined): string | undefined {
|
|
44
47
|
const normalized = value?.trim();
|
|
45
48
|
return normalized || undefined;
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
function citedIds(params: Record<string, unknown>): string[] {
|
|
52
|
+
return memoryReferenceIds(Object.values(params).filter((value): value is string => typeof value === "string").join("\n"));
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
function terminalTool<T extends TSchema>(
|
|
49
56
|
name: string,
|
|
50
57
|
label: string,
|
|
51
58
|
description: string,
|
|
52
59
|
parameters: T,
|
|
53
60
|
build: (params: Static<T>) => ReviewTerminalResult,
|
|
54
|
-
onTerminal:
|
|
61
|
+
onTerminal: ReviewTerminalWriter,
|
|
62
|
+
referenceExists: (id: string) => boolean,
|
|
55
63
|
): AgentTool<T> {
|
|
56
64
|
return {
|
|
57
65
|
name,
|
|
@@ -59,26 +67,33 @@ function terminalTool<T extends TSchema>(
|
|
|
59
67
|
description,
|
|
60
68
|
parameters,
|
|
61
69
|
execute: async (_id, params) => {
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
const missingReferenceIds = citedIds(params as Record<string, unknown>).filter((id) => !referenceExists(id));
|
|
71
|
+
const write = onTerminal(build(params), missingReferenceIds);
|
|
72
|
+
if (missingReferenceIds.length === 0) {
|
|
73
|
+
return { content: [{ type: "text", text: "Terminal review outcome recorded." }], details: { terminal: true, overwritten: write.overwritten, missingReferenceIds } };
|
|
74
|
+
}
|
|
75
|
+
const warnings = missingReferenceIds.map((id) => `WARNING: memory or primary-chat entry ${id} not found; use search_memories and recall, or search_chat_history and read_chat_history, to find the correct reference.`);
|
|
76
|
+
if (write.overwritten) warnings.push("WARNING: overwriting the prior review outcome; only the latest outcome will be delivered.");
|
|
77
|
+
warnings.push(`You can call ${name} again to replace this review, or end your turn and it will be delivered as-is.`);
|
|
78
|
+
return { content: [{ type: "text", text: warnings.join("\n") }], details: { terminal: false, replaceable: true, overwritten: write.overwritten, missingReferenceIds } };
|
|
64
79
|
},
|
|
65
80
|
};
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
export function createWorkflowProposalTool(onTerminal: (
|
|
83
|
+
export function createWorkflowProposalTool(onTerminal: ReviewTerminalWriter, referenceExists: (id: string) => boolean = () => true): AgentTool<typeof WorkflowProposalSchema> {
|
|
69
84
|
return terminalTool("submit_workflow_proposal", "Submit workflow proposal", "Record the one durable workflow proposal for this review.", WorkflowProposalSchema, (params: WorkflowProposalArgs) => ({
|
|
70
85
|
outcome: "proposal", proposalKind: "workflow", scope: "workflow", title: params.title.trim(), summary: params.summary.trim(), evidence: params.evidence.trim(), inefficiency: params.inefficiency.trim(), conceptualDesign: params.conceptual_design.trim(), inputs: trimOptional(params.inputs), outputs: trimOptional(params.outputs), integration: trimOptional(params.integration), expectedEffect: params.expected_effect.trim(), uncertainties: params.uncertainties.trim(),
|
|
71
|
-
}), onTerminal);
|
|
86
|
+
}), onTerminal, referenceExists);
|
|
72
87
|
}
|
|
73
88
|
|
|
74
|
-
export function createSoftwareProposalTool(onTerminal: (
|
|
89
|
+
export function createSoftwareProposalTool(onTerminal: ReviewTerminalWriter, referenceExists: (id: string) => boolean = () => true): AgentTool<typeof SoftwareProposalSchema> {
|
|
75
90
|
return terminalTool("submit_software_proposal", "Submit software proposal", "Record the one durable software design proposal for this review.", SoftwareProposalSchema, (params: SoftwareProposalArgs) => ({
|
|
76
91
|
outcome: "proposal", proposalKind: "software", scope: "software", title: params.title.trim(), summary: params.summary.trim(), evidence: params.evidence.trim(), structuralIssue: params.structural_issue.trim(), conceptualDesign: params.conceptual_design.trim(), preservedBehavior: params.preserved_behavior.trim(), expectedEffect: params.expected_effect.trim(), uncertainties: params.uncertainties.trim(),
|
|
77
|
-
}), onTerminal);
|
|
92
|
+
}), onTerminal, referenceExists);
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
export function createNoProposalTool(scope: "workflow" | "software", onTerminal: (
|
|
95
|
+
export function createNoProposalTool(scope: "workflow" | "software", onTerminal: ReviewTerminalWriter, referenceExists: (id: string) => boolean = () => true): AgentTool<typeof NoProposalSchema> {
|
|
81
96
|
return terminalTool("review_concluded_no_proposal", "Conclude no proposal", "Record that this review found no durable proposal justified.", NoProposalSchema, (params: NoProposalArgs) => ({
|
|
82
97
|
outcome: "no_proposal", scope, reason: params.reason.trim(), evidenceReviewed: params.evidence_reviewed.trim(), reconsiderIf: trimOptional(params.reconsider_if),
|
|
83
|
-
}), onTerminal);
|
|
98
|
+
}), onTerminal, referenceExists);
|
|
84
99
|
}
|
|
@@ -10,7 +10,7 @@ import { debugLog } from "../debug-log.js";
|
|
|
10
10
|
* "the model chose not to call the tool", which hides the real cause
|
|
11
11
|
* (rate limits, oversized prompts, auth failures, ...) from the debug log.
|
|
12
12
|
*/
|
|
13
|
-
export function logAgentStreamError(stage: "observer" | "
|
|
13
|
+
export function logAgentStreamError(stage: "observer" | "summarizer" | "contemplator" | "reviewer", event: AgentEvent): void {
|
|
14
14
|
if (event.type !== "message_end") return;
|
|
15
15
|
const message = event.message;
|
|
16
16
|
if (message.role !== "assistant") return;
|