@matthewfl/pi-contemplator 0.0.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/LICENSE +21 -0
- package/README.md +120 -0
- package/package.json +60 -0
- package/src/agents/contemplator/agent.ts +718 -0
- package/src/agents/contemplator/prompts.ts +212 -0
- package/src/agents/dropper/agent.ts +291 -0
- package/src/agents/dropper/coverage.ts +128 -0
- package/src/agents/dropper/pool.ts +67 -0
- package/src/agents/dropper/prompts.ts +48 -0
- package/src/agents/observer/agent.ts +207 -0
- package/src/agents/observer/prompts.ts +119 -0
- package/src/agents/reflector/agent.ts +213 -0
- package/src/agents/reflector/prompts.ts +81 -0
- package/src/agents/reviewer/agent.ts +187 -0
- package/src/agents/reviewer/history-tools.ts +337 -0
- package/src/agents/reviewer/prompts.ts +135 -0
- package/src/agents/reviewer/tools.ts +84 -0
- package/src/agents/stream-errors.ts +22 -0
- package/src/clipboard.ts +63 -0
- package/src/commands/contemplator-view.ts +128 -0
- package/src/commands/reviewer-view.ts +89 -0
- package/src/commands/settings.ts +257 -0
- package/src/commands/status.ts +176 -0
- package/src/commands/view.ts +171 -0
- package/src/config.ts +284 -0
- package/src/debug-log.ts +72 -0
- package/src/hooks/compaction-hook.ts +99 -0
- package/src/hooks/compaction-resume.ts +124 -0
- package/src/hooks/compaction-trigger.ts +122 -0
- package/src/hooks/consolidation-trigger.ts +488 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +32 -0
- package/src/model-budget.ts +16 -0
- package/src/runtime.ts +316 -0
- package/src/serialize.ts +274 -0
- package/src/session-ledger/fold.ts +115 -0
- package/src/session-ledger/index.ts +7 -0
- package/src/session-ledger/progress.ts +156 -0
- package/src/session-ledger/projection.ts +243 -0
- package/src/session-ledger/recall.ts +258 -0
- package/src/session-ledger/render-summary.ts +31 -0
- package/src/session-ledger/search.ts +184 -0
- package/src/session-ledger/types.ts +329 -0
- package/src/tokens.ts +27 -0
- package/src/tools/compact-context.ts +54 -0
- package/src/tools/recall-observation.ts +532 -0
- package/src/tools/search-memories.ts +131 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ReviewScope } from "../../session-ledger/types.js";
|
|
2
|
+
|
|
3
|
+
export const REVIEWER_COMMON_SYSTEM = `You are a short-lived structural reviewer commissioned by a background contemplator supporting a primary coding agent.
|
|
4
|
+
|
|
5
|
+
Your job is to investigate one suspected structural problem and reach one bounded conclusion:
|
|
6
|
+
|
|
7
|
+
- produce one durable conceptual proposal when the evidence supports one; or
|
|
8
|
+
- conclude that no proposal is currently justified.
|
|
9
|
+
|
|
10
|
+
You do not implement anything, communicate directly with the primary agent, ask the primary agent questions, or manage its immediate work.
|
|
11
|
+
|
|
12
|
+
Neither the contemplator nor you should be assumed to have correctly diagnosed the problem. Treat the review request as a question to investigate, not as an established conclusion.
|
|
13
|
+
|
|
14
|
+
You see only the memory ledger, not the live conversation, codebase, commands, tool output, or execution environment. Memories may be incomplete or slightly stale. Do not infer facts that are not recorded, and do not treat missing recent results as evidence of failure, inactivity, or an unresolved execution result.
|
|
15
|
+
|
|
16
|
+
Some memories summarize user messages. Pay extra attention to memories about the user’s intent, priorities, constraints, corrections, and desired outcome. Any proposal must remain grounded in that direction.
|
|
17
|
+
|
|
18
|
+
Every important claim in your conclusion must cite relevant memory identifiers, primary-chat entry identifiers, or both.
|
|
19
|
+
|
|
20
|
+
You have access to search_memories, recall, search_chat_history, and read_chat_history.
|
|
21
|
+
|
|
22
|
+
First recall every memory cited in the request. Then search for:
|
|
23
|
+
|
|
24
|
+
- surrounding memories that clarify sequence or context;
|
|
25
|
+
- evidence that contradicts or weakens the suspected concern;
|
|
26
|
+
- relevant memories of user intent;
|
|
27
|
+
- earlier attempts to address the same issue;
|
|
28
|
+
- existing proposals, tools, abstractions, scripts, representations, or workflows that may already address it;
|
|
29
|
+
- evidence showing whether the pattern is isolated, temporary, or recurring.
|
|
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.
|
|
32
|
+
|
|
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
|
+
|
|
35
|
+
Do not produce a proposal merely because an improvement can be imagined. Determine whether the recorded pattern is substantial enough to justify a durable design artifact.
|
|
36
|
+
|
|
37
|
+
Do not assume that speculation is productive. When the requested direction is clear and direct action is cheap, an agent may learn more by acting than by constructing additional hypotheses. Distinguish uncertainty that must be resolved before proceeding from uncertainty that can be resolved naturally through ordinary work.
|
|
38
|
+
|
|
39
|
+
Stay at the conceptual level. Do not write code, exact APIs, formal schemas, filenames, command-line arguments, or a step-by-step implementation plan. The primary agent must evaluate any proposal against the actual environment and decide whether and how to implement it.`;
|
|
40
|
+
|
|
41
|
+
export const WORKFLOW_REVIEWER_SCOPE = `This is a WORKFLOW review.
|
|
42
|
+
|
|
43
|
+
Examine how the primary agent is carrying out the work. Your concern is not the internal structure of the product code except where it affects the agent’s ability to investigate, verify, or make progress.
|
|
44
|
+
|
|
45
|
+
Look for patterns such as:
|
|
46
|
+
|
|
47
|
+
- repeatedly reconstructing the same information;
|
|
48
|
+
- repeatedly performing similar searches, traces, transformations, comparisons, or manual correlations;
|
|
49
|
+
- reasoning at length about a result that could be observed, executed, queried, or measured directly;
|
|
50
|
+
- using an unnecessarily slow, fragile, expensive, or low-information feedback loop;
|
|
51
|
+
- recreating related one-off utilities instead of preserving and extending an existing capability;
|
|
52
|
+
- repeatedly loading large amounts of context to recover relationships that could be represented compactly;
|
|
53
|
+
- failing to preserve a useful intermediate result for later reuse or review;
|
|
54
|
+
- taking actions that do not produce information relevant to the next decision;
|
|
55
|
+
- working around the absence of a capability instead of addressing that capability gap;
|
|
56
|
+
- producing conclusions that exist only in transient reasoning when an executable or structured artifact would make them reproducible.
|
|
57
|
+
|
|
58
|
+
Apply a strong bias toward externalizing repeated or uncertain cognitive work into computation, direct observation, or a durable representation.
|
|
59
|
+
|
|
60
|
+
A cheap, safe executable check is often preferable to mentally simulating its result. If substantially the same nontrivial operation appears a second time, treat that as strong evidence that it may deserve a reusable capability or preserved workflow. Iterating on an existing tool, script, representation, or process is generally preferable to recreating the operation.
|
|
61
|
+
|
|
62
|
+
Near completion is not a reason to dismiss a workflow improvement. A durable check or representation may expose errors in the original reasoning and make the result easier to reproduce and review.
|
|
63
|
+
|
|
64
|
+
A workflow proposal may describe a reusable tool, executable check, evaluation mechanism, query, index, trace, diagnostic, structured representation, or repeatable working process.
|
|
65
|
+
|
|
66
|
+
The conceptual design should explain:
|
|
67
|
+
|
|
68
|
+
- what recurring work it replaces or improves;
|
|
69
|
+
- how it would conceptually operate;
|
|
70
|
+
- what information or artifacts it would work from and produce, in ordinary planning language;
|
|
71
|
+
- how it could be reused, refined, and extended instead of reinvented;
|
|
72
|
+
- how it would improve evidence quality, reliability, speed, token use, reproducibility, or reviewability.
|
|
73
|
+
|
|
74
|
+
Do not prescribe a particular implementation technology unless the memories make that constraint strategically important. The design should describe the capability needed, and not take over its implementation.
|
|
75
|
+
|
|
76
|
+
Use submit_workflow_proposal only when the evidence supports a durable workflow design.`;
|
|
77
|
+
|
|
78
|
+
export const SOFTWARE_REVIEWER_SCOPE = `This is a SOFTWARE review.
|
|
79
|
+
|
|
80
|
+
Examine the structure of the software being produced. Your concern is not whether the primary agent’s personal workflow could be faster except where the memories reveal a structural problem in the product itself.
|
|
81
|
+
|
|
82
|
+
Look for patterns such as:
|
|
83
|
+
|
|
84
|
+
- repeated special cases reflecting the same underlying concept;
|
|
85
|
+
- several bugs, patches, or workarounds involving the same missing invariant;
|
|
86
|
+
- duplicated behavior, state, or policy that may drift apart;
|
|
87
|
+
- unclear or unstable responsibility boundaries;
|
|
88
|
+
- recurring workarounds caused by an unsuitable model or representation;
|
|
89
|
+
- local changes whose interaction suggests a missing abstraction;
|
|
90
|
+
- complexity caused by representing the problem incorrectly;
|
|
91
|
+
- multiple components independently enforcing what appears to be one shared rule;
|
|
92
|
+
- a design that obscures behavior the user expects to remain stable;
|
|
93
|
+
- repeated fixes that address consequences without representing the underlying condition directly.
|
|
94
|
+
|
|
95
|
+
A software proposal may describe an abstraction, invariant, responsibility boundary, state model, interface concept, decomposition, normalization, or refactoring direction.
|
|
96
|
+
|
|
97
|
+
The conceptual design should explain:
|
|
98
|
+
|
|
99
|
+
- the recurring structural symptom;
|
|
100
|
+
- the underlying concept that may be missing or represented poorly;
|
|
101
|
+
- the proposed responsibilities, relationships, and invariants;
|
|
102
|
+
- what recorded behavior, user intent, and constraints must be preserved;
|
|
103
|
+
- why the design could reduce special cases, duplication, contradictions, or hidden coupling;
|
|
104
|
+
- important tradeoffs and uncertainties.
|
|
105
|
+
|
|
106
|
+
Do not turn a single untidy implementation detail into an architectural proposal. Prefer a proposal when multiple memories reveal a recurring structure or when one especially consequential design flaw is strongly supported.
|
|
107
|
+
|
|
108
|
+
Do not prescribe filenames, libraries, exact APIs, code, or an ordered implementation plan. Describe the shape of the software design and leave implementation decisions to the primary agent.
|
|
109
|
+
|
|
110
|
+
Use submit_software_proposal only when the evidence supports a durable software design.`;
|
|
111
|
+
|
|
112
|
+
export const REVIEWER_TERMINAL_RULES = `Reach exactly one terminal outcome.
|
|
113
|
+
|
|
114
|
+
Use the proposal tool available in this review only when the evidence supports a durable structural design.
|
|
115
|
+
|
|
116
|
+
Use review_concluded_no_proposal when:
|
|
117
|
+
|
|
118
|
+
- the pattern appears isolated, temporary, or already resolved;
|
|
119
|
+
- the evidence is too incomplete or contradictory;
|
|
120
|
+
- the concern is already addressed by an existing capability, abstraction, or proposal;
|
|
121
|
+
- the improvement would be generic advice rather than a concrete conceptual design;
|
|
122
|
+
- the concern depends too heavily on details unavailable in memory;
|
|
123
|
+
- the evidence does not support the review request’s diagnosis;
|
|
124
|
+
- the likely value does not justify preserving a durable proposal.
|
|
125
|
+
|
|
126
|
+
A no-proposal conclusion is a valid result. Explain what was examined and, when useful, what new evidence would justify reconsideration.
|
|
127
|
+
|
|
128
|
+
Produce exactly one terminal tool call. Do not emit ordinary assistant text.`;
|
|
129
|
+
|
|
130
|
+
/** Assemble only the prompt subset and terminal rules for the requested scope. */
|
|
131
|
+
export function buildReviewerSystemPrompt(scope: ReviewScope): string {
|
|
132
|
+
const scopedPrompt = scope === "workflow" ? WORKFLOW_REVIEWER_SCOPE : SOFTWARE_REVIEWER_SCOPE;
|
|
133
|
+
|
|
134
|
+
return [REVIEWER_COMMON_SYSTEM, scopedPrompt, REVIEWER_TERMINAL_RULES].join("\n\n");
|
|
135
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { Static, TSchema } from "typebox";
|
|
4
|
+
import type { ReviewNoProposal, SoftwareReviewProposal, WorkflowReviewProposal } from "../../session-ledger/types.js";
|
|
5
|
+
|
|
6
|
+
const prose = (description: string) => Type.String({ minLength: 1, description });
|
|
7
|
+
|
|
8
|
+
export const WorkflowProposalSchema = Type.Object({
|
|
9
|
+
title: prose("Short natural-language name for the workflow improvement."),
|
|
10
|
+
summary: prose("Compact advisory summary suitable for the primary-agent notice."),
|
|
11
|
+
evidence: prose("Evidence grounded in cited memory or primary-chat entry ids, including relevant contrary evidence."),
|
|
12
|
+
inefficiency: prose("Recurring expensive, unreliable, or difficult-to-review work."),
|
|
13
|
+
conceptual_design: prose("High-level planning prose describing the improved capability or process; no code or implementation steps."),
|
|
14
|
+
inputs: Type.Optional(prose("Ordinary-language inputs, artifacts, context, or questions.")),
|
|
15
|
+
outputs: Type.Optional(prose("Ordinary-language outputs, evidence, artifacts, or representations.")),
|
|
16
|
+
integration: Type.Optional(prose("How the primary agent could conceptually reuse, refine, or extend it.")),
|
|
17
|
+
expected_effect: prose("Expected effect on efficiency, reliability, evidence quality, reproducibility, or reviewability."),
|
|
18
|
+
uncertainties: prose("Unknowns and tradeoffs the primary agent must evaluate."),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const SoftwareProposalSchema = Type.Object({
|
|
22
|
+
title: prose("Short natural-language name for the software design improvement."),
|
|
23
|
+
summary: prose("Compact advisory summary suitable for the primary-agent notice."),
|
|
24
|
+
evidence: prose("Evidence grounded in cited memory or primary-chat entry ids, including relevant contrary evidence."),
|
|
25
|
+
structural_issue: prose("Recurring structural symptom, missing invariant, duplicated concept, or unsuitable boundary."),
|
|
26
|
+
conceptual_design: prose("High-level design of concepts, responsibilities, relationships, and invariants; no code or implementation steps."),
|
|
27
|
+
preserved_behavior: prose("User intent, visible behavior, constraints, and semantics to preserve."),
|
|
28
|
+
expected_effect: prose("Expected reduction in special cases, duplication, contradictions, coupling, or maintenance risk."),
|
|
29
|
+
uncertainties: prose("Unknowns and tradeoffs the primary agent must evaluate."),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const NoProposalSchema = Type.Object({
|
|
33
|
+
reason: prose("Why a durable proposal is not currently justified."),
|
|
34
|
+
evidence_reviewed: prose("Memories and primary-chat entries examined, with ids, and the evidence supporting this conclusion."),
|
|
35
|
+
reconsider_if: Type.Optional(prose("Specific future evidence or recurrence that would justify reconsideration.")),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type WorkflowProposalArgs = Static<typeof WorkflowProposalSchema>;
|
|
39
|
+
export type SoftwareProposalArgs = Static<typeof SoftwareProposalSchema>;
|
|
40
|
+
export type NoProposalArgs = Static<typeof NoProposalSchema>;
|
|
41
|
+
export type ReviewTerminalResult = Omit<WorkflowReviewProposal, "id" | "version" | "reviewRequestId" | "createdAt" | "requestedBy"> | Omit<SoftwareReviewProposal, "id" | "version" | "reviewRequestId" | "createdAt" | "requestedBy"> | Omit<ReviewNoProposal, "id" | "version" | "reviewRequestId" | "createdAt" | "requestedBy">;
|
|
42
|
+
|
|
43
|
+
function trimOptional(value: string | undefined): string | undefined {
|
|
44
|
+
const normalized = value?.trim();
|
|
45
|
+
return normalized || undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function terminalTool<T extends TSchema>(
|
|
49
|
+
name: string,
|
|
50
|
+
label: string,
|
|
51
|
+
description: string,
|
|
52
|
+
parameters: T,
|
|
53
|
+
build: (params: Static<T>) => ReviewTerminalResult,
|
|
54
|
+
onTerminal: (result: ReviewTerminalResult) => void,
|
|
55
|
+
): AgentTool<T> {
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
label,
|
|
59
|
+
description,
|
|
60
|
+
parameters,
|
|
61
|
+
execute: async (_id, params) => {
|
|
62
|
+
onTerminal(build(params));
|
|
63
|
+
return { content: [{ type: "text", text: "Terminal review outcome recorded." }], details: { terminal: true } };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createWorkflowProposalTool(onTerminal: (result: ReviewTerminalResult) => void): AgentTool<typeof WorkflowProposalSchema> {
|
|
69
|
+
return terminalTool("submit_workflow_proposal", "Submit workflow proposal", "Record the one durable workflow proposal for this review.", WorkflowProposalSchema, (params: WorkflowProposalArgs) => ({
|
|
70
|
+
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);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createSoftwareProposalTool(onTerminal: (result: ReviewTerminalResult) => void): AgentTool<typeof SoftwareProposalSchema> {
|
|
75
|
+
return terminalTool("submit_software_proposal", "Submit software proposal", "Record the one durable software design proposal for this review.", SoftwareProposalSchema, (params: SoftwareProposalArgs) => ({
|
|
76
|
+
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);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createNoProposalTool(scope: "workflow" | "software", onTerminal: (result: ReviewTerminalResult) => void): AgentTool<typeof NoProposalSchema> {
|
|
81
|
+
return terminalTool("review_concluded_no_proposal", "Conclude no proposal", "Record that this review found no durable proposal justified.", NoProposalSchema, (params: NoProposalArgs) => ({
|
|
82
|
+
outcome: "no_proposal", scope, reason: params.reason.trim(), evidenceReviewed: params.evidence_reviewed.trim(), reconsiderIf: trimOptional(params.reconsider_if),
|
|
83
|
+
}), onTerminal);
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentEvent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { debugLog } from "../debug-log.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Surface LLM failures from an agent-loop event stream.
|
|
6
|
+
*
|
|
7
|
+
* When the underlying LLM call fails, the loop ends the stream with a final
|
|
8
|
+
* assistant message whose stopReason is "error" (or "aborted") — no exception
|
|
9
|
+
* is thrown. Without this hook the drain loops treat such runs exactly like
|
|
10
|
+
* "the model chose not to call the tool", which hides the real cause
|
|
11
|
+
* (rate limits, oversized prompts, auth failures, ...) from the debug log.
|
|
12
|
+
*/
|
|
13
|
+
export function logAgentStreamError(stage: "observer" | "reflector" | "dropper" | "contemplator" | "reviewer", event: AgentEvent): void {
|
|
14
|
+
if (event.type !== "message_end") return;
|
|
15
|
+
const message = event.message;
|
|
16
|
+
if (message.role !== "assistant") return;
|
|
17
|
+
if (message.stopReason !== "error" && message.stopReason !== "aborted") return;
|
|
18
|
+
debugLog(`${stage}.stream_error`, {
|
|
19
|
+
stopReason: message.stopReason,
|
|
20
|
+
errorMessage: message.errorMessage,
|
|
21
|
+
});
|
|
22
|
+
}
|
package/src/clipboard.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface ClipboardCommand {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ClipboardCommandRunner = (command: ClipboardCommand, text: string) => Promise<boolean>;
|
|
9
|
+
|
|
10
|
+
export function getClipboardCommands(platform: NodeJS.Platform = process.platform): ClipboardCommand[] {
|
|
11
|
+
switch (platform) {
|
|
12
|
+
case "darwin":
|
|
13
|
+
return [{ command: "pbcopy", args: [] }];
|
|
14
|
+
case "win32":
|
|
15
|
+
return [{ command: "clip", args: [] }];
|
|
16
|
+
default:
|
|
17
|
+
return [
|
|
18
|
+
{ command: "wl-copy", args: [] },
|
|
19
|
+
{ command: "xclip", args: ["-selection", "clipboard"] },
|
|
20
|
+
{ command: "xsel", args: ["--clipboard", "--input"] },
|
|
21
|
+
{ command: "termux-clipboard-set", args: [] },
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function copyTextToClipboard(
|
|
27
|
+
text: string,
|
|
28
|
+
runner: ClipboardCommandRunner = runClipboardCommand,
|
|
29
|
+
commands: ClipboardCommand[] = getClipboardCommands(),
|
|
30
|
+
): Promise<boolean> {
|
|
31
|
+
for (const command of commands) {
|
|
32
|
+
if (await runner(command, text)) return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function runClipboardCommand(command: ClipboardCommand, text: string): Promise<boolean> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let settled = false;
|
|
40
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
41
|
+
|
|
42
|
+
const finish = (ok: boolean) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
if (timeout) clearTimeout(timeout);
|
|
46
|
+
resolve(ok);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const child = spawn(command.command, command.args, {
|
|
50
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
timeout = setTimeout(() => {
|
|
54
|
+
child.kill();
|
|
55
|
+
finish(false);
|
|
56
|
+
}, 2_000);
|
|
57
|
+
|
|
58
|
+
child.on("error", () => finish(false));
|
|
59
|
+
child.on("close", (code) => finish(code === 0));
|
|
60
|
+
child.stdin.on("error", () => undefined);
|
|
61
|
+
child.stdin.end(text, "utf8");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Entry } from "../session-ledger/index.js";
|
|
2
|
+
|
|
3
|
+
const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
|
|
4
|
+
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
5
|
+
const REVIEW_REQUEST = "om.review.request";
|
|
6
|
+
const REVIEW_RESULT = "om.review.result";
|
|
7
|
+
const DIM = "\x1b[2m";
|
|
8
|
+
const RESET = "\x1b[0m";
|
|
9
|
+
|
|
10
|
+
type MessagePart = {
|
|
11
|
+
type?: unknown;
|
|
12
|
+
text?: unknown;
|
|
13
|
+
name?: unknown;
|
|
14
|
+
arguments?: unknown;
|
|
15
|
+
content?: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type StoredMessage = {
|
|
19
|
+
role?: unknown;
|
|
20
|
+
content?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function estimateTokens(value: unknown): number {
|
|
24
|
+
return Math.max(1, Math.ceil(JSON.stringify(value).length / 4));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderValue(value: unknown): string {
|
|
28
|
+
if (typeof value === "string") return value;
|
|
29
|
+
if (value === undefined || value === null) return "";
|
|
30
|
+
return JSON.stringify(value, null, 2);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderContent(content: unknown): string {
|
|
34
|
+
if (typeof content === "string") return content;
|
|
35
|
+
if (!Array.isArray(content)) return renderValue(content);
|
|
36
|
+
return content
|
|
37
|
+
.map((part: MessagePart) => {
|
|
38
|
+
if (part.type === "text") return typeof part.text === "string" ? part.text : "";
|
|
39
|
+
if (part.type === "toolCall" || part.type === "tool_use" || part.type === "toolUse") {
|
|
40
|
+
const name = typeof part.name === "string" ? part.name : "unknown tool";
|
|
41
|
+
const args = part.arguments === undefined ? "" : ` ${renderValue(part.arguments)}`;
|
|
42
|
+
return `[tool call: ${name}${args}]`;
|
|
43
|
+
}
|
|
44
|
+
if (part.type === "toolResult" || part.type === "tool_result") {
|
|
45
|
+
return `[tool result]\n${renderValue(part.content)}`;
|
|
46
|
+
}
|
|
47
|
+
return `[${String(part.type ?? "content")}] ${renderValue(part)}`;
|
|
48
|
+
})
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderMessage(message: StoredMessage, compacted: boolean): string {
|
|
54
|
+
const role = typeof message.role === "string" ? message.role : "unknown";
|
|
55
|
+
const tokens = estimateTokens(message);
|
|
56
|
+
const marker = compacted ? " [compacted summary]" : "";
|
|
57
|
+
return `${DIM}── ${role}${marker} · ~${tokens} tokens ──${RESET}\n${renderContent(message.content) || `${DIM}(empty message)${RESET}`}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderContemplator(entries: Entry[]): string {
|
|
61
|
+
const messages: Array<{ message: StoredMessage; compacted: boolean }> = [];
|
|
62
|
+
const suggestions: Array<{ suggestion: string; delivered: boolean }> = [];
|
|
63
|
+
const reviews: Array<{ requestId: string; scope: string; outcome: string; memoryId?: string }> = [];
|
|
64
|
+
const suggestionIndexByProbeId = new Map<string, number>();
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.customType === CONTEMPLATOR_MESSAGE && entry.data && typeof entry.data === "object") {
|
|
67
|
+
const data = entry.data as { message?: unknown; compacted?: unknown };
|
|
68
|
+
if (data.message && typeof data.message === "object") {
|
|
69
|
+
messages.push({ message: data.message as StoredMessage, compacted: data.compacted === true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (entry.customType === REVIEW_REQUEST && entry.data && typeof entry.data === "object") {
|
|
73
|
+
const request = (entry.data as { request?: { id?: unknown; scope?: unknown } }).request;
|
|
74
|
+
if (typeof request?.id === "string" && typeof request.scope === "string") reviews.push({ requestId: request.id, scope: request.scope, outcome: "pending" });
|
|
75
|
+
}
|
|
76
|
+
if (entry.customType === REVIEW_RESULT && entry.data && typeof entry.data === "object") {
|
|
77
|
+
const result = (entry.data as { result?: { reviewRequestId?: unknown; scope?: unknown; outcome?: unknown; id?: unknown } }).result;
|
|
78
|
+
if (typeof result?.reviewRequestId === "string" && typeof result.scope === "string" && typeof result.outcome === "string") {
|
|
79
|
+
const existing = reviews.find((review) => review.requestId === result.reviewRequestId);
|
|
80
|
+
const review = { requestId: result.reviewRequestId, scope: result.scope, outcome: result.outcome, memoryId: typeof result.id === "string" ? result.id : undefined };
|
|
81
|
+
if (existing) Object.assign(existing, review); else reviews.push(review);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (entry.customType === CONTEMPLATOR_SUGGESTION && entry.data && typeof entry.data === "object") {
|
|
85
|
+
const data = entry.data as { suggestion?: unknown; delivered?: unknown; probeId?: unknown };
|
|
86
|
+
if (typeof data.suggestion !== "string") continue;
|
|
87
|
+
const suggestion = { suggestion: data.suggestion, delivered: data.delivered === true };
|
|
88
|
+
if (typeof data.probeId !== "string") {
|
|
89
|
+
suggestions.push(suggestion);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const existingIndex = suggestionIndexByProbeId.get(data.probeId);
|
|
93
|
+
if (existingIndex === undefined) {
|
|
94
|
+
suggestionIndexByProbeId.set(data.probeId, suggestions.length);
|
|
95
|
+
suggestions.push(suggestion);
|
|
96
|
+
} else {
|
|
97
|
+
suggestions[existingIndex] = suggestion;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const totalTokens = messages.reduce((total, item) => total + estimateTokens(item.message), 0);
|
|
103
|
+
const lines = [
|
|
104
|
+
`${DIM}CONTEMPLATOR · ${messages.length} messages · ~${totalTokens} estimated tokens${RESET}`,
|
|
105
|
+
"",
|
|
106
|
+
];
|
|
107
|
+
if (messages.length === 0) {
|
|
108
|
+
lines.push(`${DIM}No contemplator messages recorded on this branch.${RESET}`);
|
|
109
|
+
} else {
|
|
110
|
+
messages.forEach((item, index) => {
|
|
111
|
+
if (index > 0) lines.push("");
|
|
112
|
+
lines.push(renderMessage(item.message, item.compacted));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (suggestions.length > 0) {
|
|
116
|
+
lines.push("", `${DIM}── Probes ──${RESET}`);
|
|
117
|
+
for (const item of suggestions) lines.push(`${item.delivered ? "[delivered]" : "[pending]"} ${item.suggestion}`);
|
|
118
|
+
}
|
|
119
|
+
if (reviews.length > 0) {
|
|
120
|
+
lines.push("", `${DIM}── Structural reviews ──${RESET}`);
|
|
121
|
+
for (const review of reviews) lines.push(`[${review.outcome}] ${review.scope} ${review.requestId}${review.memoryId ? ` → [${review.memoryId}]` : ""}`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function stripAnsi(value: string): string {
|
|
127
|
+
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
|
128
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Entry } from "../session-ledger/index.js";
|
|
2
|
+
|
|
3
|
+
const REVIEWER_MESSAGE = "om.reviewer.message";
|
|
4
|
+
const REVIEWER_NOTICE = "om.reviewer.notice";
|
|
5
|
+
const REVIEW_RESULT = "om.review.result";
|
|
6
|
+
const DIM = "\x1b[2m";
|
|
7
|
+
const RESET = "\x1b[0m";
|
|
8
|
+
|
|
9
|
+
type StoredMessage = { role?: unknown; content?: unknown };
|
|
10
|
+
type ContentPart = { type?: unknown; text?: unknown; name?: unknown; arguments?: unknown; content?: unknown };
|
|
11
|
+
|
|
12
|
+
type ReviewerRun = {
|
|
13
|
+
requestId: string;
|
|
14
|
+
scope: string;
|
|
15
|
+
messages: StoredMessage[];
|
|
16
|
+
notice?: string;
|
|
17
|
+
outcome?: string;
|
|
18
|
+
memoryId?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function renderValue(value: unknown): string {
|
|
22
|
+
if (typeof value === "string") return value;
|
|
23
|
+
if (value === undefined || value === null) return "";
|
|
24
|
+
return JSON.stringify(value, null, 2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderContent(content: unknown): string {
|
|
28
|
+
if (!Array.isArray(content)) return renderValue(content);
|
|
29
|
+
return content.map((part: ContentPart) => {
|
|
30
|
+
if (part.type === "text") return typeof part.text === "string" ? part.text : "";
|
|
31
|
+
if (part.type === "toolCall" || part.type === "tool_use" || part.type === "toolUse") {
|
|
32
|
+
return `[tool call: ${typeof part.name === "string" ? part.name : "unknown tool"}${part.arguments === undefined ? "" : ` ${renderValue(part.arguments)}`}]`;
|
|
33
|
+
}
|
|
34
|
+
if (part.type === "toolResult" || part.type === "tool_result") return `[tool result]\n${renderValue(part.content)}`;
|
|
35
|
+
return `[${String(part.type ?? "content")}] ${renderValue(part)}`;
|
|
36
|
+
}).filter(Boolean).join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runFor(runs: Map<string, ReviewerRun>, requestId: string, scope = "unknown"): ReviewerRun {
|
|
40
|
+
let run = runs.get(requestId);
|
|
41
|
+
if (!run) {
|
|
42
|
+
run = { requestId, scope, messages: [] };
|
|
43
|
+
runs.set(requestId, run);
|
|
44
|
+
}
|
|
45
|
+
return run;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Render persisted reviewer assistant/tool output and the proposal notice, if one was queued. */
|
|
49
|
+
export function renderReviewer(entries: Entry[]): string {
|
|
50
|
+
const runs = new Map<string, ReviewerRun>();
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.data || typeof entry.data !== "object") continue;
|
|
53
|
+
const data = entry.data as { reviewRequestId?: unknown; scope?: unknown; message?: unknown; content?: unknown; result?: unknown; reviewMemoryId?: unknown };
|
|
54
|
+
const result = entry.customType === REVIEW_RESULT && data.result && typeof data.result === "object"
|
|
55
|
+
? data.result as { reviewRequestId?: unknown; scope?: unknown; outcome?: unknown; id?: unknown }
|
|
56
|
+
: undefined;
|
|
57
|
+
let requestId: string | undefined;
|
|
58
|
+
if (typeof data.reviewRequestId === "string") requestId = data.reviewRequestId;
|
|
59
|
+
else if (typeof result?.reviewRequestId === "string") requestId = result.reviewRequestId;
|
|
60
|
+
if (!requestId) continue;
|
|
61
|
+
let scope = "unknown";
|
|
62
|
+
if (typeof data.scope === "string") scope = data.scope;
|
|
63
|
+
else if (typeof result?.scope === "string") scope = result.scope;
|
|
64
|
+
const run = runFor(runs, requestId, scope);
|
|
65
|
+
if (entry.customType === REVIEWER_MESSAGE && data.message && typeof data.message === "object") run.messages.push(data.message as StoredMessage);
|
|
66
|
+
if (entry.customType === REVIEWER_NOTICE && typeof data.content === "string") run.notice = data.content;
|
|
67
|
+
if (result) {
|
|
68
|
+
if (typeof result.outcome === "string") run.outcome = result.outcome;
|
|
69
|
+
if (typeof result.id === "string") run.memoryId = result.id;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lines = [`${DIM}STRUCTURAL REVIEWER · ${runs.size} review run${runs.size === 1 ? "" : "s"}${RESET}`, ""];
|
|
74
|
+
if (runs.size === 0) {
|
|
75
|
+
lines.push(`${DIM}No reviewer output recorded on this branch.${RESET}`);
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
for (const run of runs.values()) {
|
|
79
|
+
lines.push(`${DIM}── ${run.scope} review ${run.requestId}${run.outcome ? ` · ${run.outcome}` : " · pending"}${run.memoryId ? ` · [${run.memoryId}]` : ""} ──${RESET}`);
|
|
80
|
+
if (run.messages.length === 0) lines.push(`${DIM}(no assistant output recorded)${RESET}`);
|
|
81
|
+
for (const message of run.messages) {
|
|
82
|
+
const role = typeof message.role === "string" ? message.role : "assistant";
|
|
83
|
+
lines.push(`${DIM}${role}${RESET}\n${renderContent(message.content) || `${DIM}(empty message)${RESET}`}`);
|
|
84
|
+
}
|
|
85
|
+
if (run.notice) lines.push(`${DIM}Primary-agent notice queued${RESET}\n${run.notice}`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
}
|
|
88
|
+
return lines.join("\n").trimEnd();
|
|
89
|
+
}
|