@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.
Files changed (44) hide show
  1. package/README.md +17 -11
  2. package/package.json +8 -6
  3. package/src/agents/contemplator/agent.ts +325 -91
  4. package/src/agents/contemplator/prompts.ts +6 -6
  5. package/src/agents/observer/agent.ts +14 -6
  6. package/src/agents/observer/prompts.ts +16 -7
  7. package/src/agents/reviewer/agent.ts +24 -4
  8. package/src/agents/reviewer/prompts.ts +1 -1
  9. package/src/agents/reviewer/tools.ts +24 -9
  10. package/src/agents/stream-errors.ts +1 -1
  11. package/src/agents/summarizer/agent.ts +597 -0
  12. package/src/agents/summarizer/prompts.ts +46 -0
  13. package/src/agents/summarizer/sampling.ts +80 -0
  14. package/src/commands/contemplator-view.ts +22 -1
  15. package/src/commands/settings.ts +73 -69
  16. package/src/commands/status.ts +60 -36
  17. package/src/commands/summarizer-view.ts +58 -0
  18. package/src/commands/view.ts +22 -10
  19. package/src/config.ts +25 -32
  20. package/src/hooks/compaction-hook.ts +36 -19
  21. package/src/hooks/compaction-resume.ts +4 -4
  22. package/src/hooks/compaction-trigger.ts +96 -56
  23. package/src/hooks/consolidation-trigger.ts +213 -196
  24. package/src/memory-citations.ts +37 -0
  25. package/src/required-tool-choice.ts +28 -0
  26. package/src/runtime.ts +116 -33
  27. package/src/session-ledger/fold.ts +82 -53
  28. package/src/session-ledger/index.ts +1 -0
  29. package/src/session-ledger/pools.ts +77 -0
  30. package/src/session-ledger/progress.ts +7 -18
  31. package/src/session-ledger/projection.ts +45 -177
  32. package/src/session-ledger/recall.ts +129 -127
  33. package/src/session-ledger/render-summary.ts +20 -19
  34. package/src/session-ledger/search.ts +99 -115
  35. package/src/session-ledger/types.ts +102 -75
  36. package/src/tools/compact-context.ts +1 -1
  37. package/src/tools/recall-observation.ts +99 -459
  38. package/src/tools/search-memories.ts +31 -72
  39. package/src/agents/dropper/agent.ts +0 -291
  40. package/src/agents/dropper/coverage.ts +0 -128
  41. package/src/agents/dropper/pool.ts +0 -67
  42. package/src/agents/dropper/prompts.ts +0 -48
  43. package/src/agents/reflector/agent.ts +0 -213
  44. 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 reflections 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.
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, reflection, user-intent memory, or previously supported conclusion, make the contradiction visible and cite the memories on both sides.
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, reflections, and durable review results` : `observations and reflections`} 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` : ``}.
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. Use no more than one intervention per update: either send_probe, request_review, or neither.` :
189
- `Your probes are delivered asynchronously. Send no more than one probe per update.`}
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, do not call an intervention tool.`
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
- priorReflections: string[];
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, priorReflections, priorObservations, chunk, allowedSourceEntryIds, signal } = args;
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 REFLECTIONS:
146
- ${joinOrEmpty(priorReflections)}
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 reflections 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.
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, rated observations by calling the record_observations tool. The observations you emit — together with the reflections crystallized from them — are the assistant's ONLY memory of this session after the raw conversation falls out of context.
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 reflections (long-lived facts already crystallized).
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 reflections and current observations so you know what is already captured.
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 reflections or current observations unless something has materially changed.
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, the reflector may crystallize both the old and the new as equally valid preferences.
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 dropper will drop these first.
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
- const acceptTerminal = (candidate: ReviewTerminalResult): void => {
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 reflections 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.
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: (result: ReviewTerminalResult) => void,
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
- onTerminal(build(params));
63
- return { content: [{ type: "text", text: "Terminal review outcome recorded." }], details: { terminal: true } };
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: (result: ReviewTerminalResult) => void): AgentTool<typeof WorkflowProposalSchema> {
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: (result: ReviewTerminalResult) => void): AgentTool<typeof SoftwareProposalSchema> {
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: (result: ReviewTerminalResult) => void): AgentTool<typeof NoProposalSchema> {
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" | "reflector" | "dropper" | "contemplator" | "reviewer", event: AgentEvent): void {
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;