@kodrunhq/opencode-autopilot 1.3.0 → 1.4.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.
@@ -0,0 +1,100 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { z } from "zod";
3
+ import { createMockError } from "../observability/mock/mock-provider";
4
+ import type { MockFailureMode } from "../observability/mock/types";
5
+ import { FAILURE_MODES } from "../observability/mock/types";
6
+ import { classifyErrorType, isRetryableError } from "../orchestrator/fallback/error-classifier";
7
+
8
+ /**
9
+ * Default retryable status codes matching the standard fallback config.
10
+ */
11
+ const DEFAULT_RETRY_CODES: readonly number[] = Object.freeze([429, 503, 529]);
12
+
13
+ /**
14
+ * Human-readable descriptions for each failure mode.
15
+ */
16
+ const MODE_DESCRIPTIONS: Readonly<Record<MockFailureMode, string>> = Object.freeze({
17
+ rate_limit: "Simulates HTTP 429 rate limit response",
18
+ quota_exceeded: "Simulates HTTP 402 quota/billing error",
19
+ timeout: "Simulates HTTP 504 gateway timeout (classifies as service_unavailable)",
20
+ malformed: "Simulates unparseable/corrupt response (not retryable)",
21
+ service_unavailable: "Simulates HTTP 503 service outage",
22
+ });
23
+
24
+ /**
25
+ * Core function for mock fallback testing tool.
26
+ * Follows the *Core + tool() wrapper pattern per CLAUDE.md.
27
+ *
28
+ * - "list" mode returns all available failure modes with descriptions
29
+ * - Any valid failure mode generates and classifies the mock error
30
+ * - Invalid modes return an error JSON
31
+ *
32
+ * This tool does NOT trigger fallback in a live session. It generates and
33
+ * classifies errors, showing what the fallback system would see.
34
+ */
35
+ export async function mockFallbackCore(mode: string): Promise<string> {
36
+ if (mode === "list") {
37
+ const modeLines = FAILURE_MODES.map((m) => ` ${m}: ${MODE_DESCRIPTIONS[m]}`).join("\n");
38
+
39
+ return JSON.stringify({
40
+ action: "mock_fallback_list",
41
+ modes: [...FAILURE_MODES],
42
+ displayText: `Available failure modes:\n${modeLines}`,
43
+ });
44
+ }
45
+
46
+ // Validate mode
47
+ if (!FAILURE_MODES.includes(mode as MockFailureMode)) {
48
+ return JSON.stringify({
49
+ action: "error",
50
+ message: "Invalid failure mode. Use 'list' to see available modes.",
51
+ });
52
+ }
53
+
54
+ const failureMode = mode as MockFailureMode;
55
+ const error = createMockError(failureMode);
56
+ const classification = classifyErrorType(error);
57
+ const retryable = isRetryableError(error, DEFAULT_RETRY_CODES);
58
+
59
+ // Extract error fields for the response
60
+ const errorObj = error as Record<string, unknown>;
61
+ const errorSummary: Record<string, unknown> = {
62
+ name: errorObj.name,
63
+ message: errorObj.message,
64
+ };
65
+ if (errorObj.status !== undefined) {
66
+ errorSummary.status = errorObj.status;
67
+ }
68
+
69
+ const displayText = [
70
+ `Mock ${failureMode} error generated.`,
71
+ `Classification: ${classification}`,
72
+ `Retryable: ${retryable}`,
73
+ "",
74
+ "To test fallback chain: inject this error into FallbackManager.handleError() in a test,",
75
+ "or use oc_mock_fallback in a session to verify error classification behavior.",
76
+ ].join("\n");
77
+
78
+ return JSON.stringify({
79
+ action: "mock_fallback",
80
+ mode: failureMode,
81
+ error: errorSummary,
82
+ classification,
83
+ retryable,
84
+ displayText,
85
+ });
86
+ }
87
+
88
+ // --- Tool wrapper ---
89
+
90
+ export const ocMockFallback = tool({
91
+ description:
92
+ "Generate mock errors for fallback chain testing. " +
93
+ "Use 'list' to see available failure modes.",
94
+ args: {
95
+ mode: z.string().describe("Failure mode to simulate or 'list' for available modes"),
96
+ },
97
+ async execute({ mode }) {
98
+ return mockFallbackCore(mode);
99
+ },
100
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * oc_pipeline_report tool - Decision trace with phase-by-phase breakdown.
3
+ *
4
+ * Reads a session log and produces a read-only report showing:
5
+ * - Phase-by-phase decision timeline
6
+ * - Per-phase decision count
7
+ * - Decisions with agent and rationale context
8
+ *
9
+ * Follows the *Core + tool() wrapper pattern per CLAUDE.md.
10
+ * Returns JSON with displayText field following oc_doctor pattern.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import { tool } from "@opencode-ai/plugin";
16
+ import { z } from "zod";
17
+ import { readLatestSessionLog, readSessionLog } from "../observability/log-reader";
18
+ import { computeDuration, formatDuration } from "../observability/summary-generator";
19
+ import type { SessionLog } from "../observability/types";
20
+
21
+ /**
22
+ * A decision entry within a phase section of the report.
23
+ */
24
+ interface ReportDecision {
25
+ readonly agent: string;
26
+ readonly decision: string;
27
+ readonly rationale: string;
28
+ }
29
+
30
+ /**
31
+ * A phase section in the pipeline report.
32
+ */
33
+ interface ReportPhase {
34
+ readonly phase: string;
35
+ readonly decisions: readonly ReportDecision[];
36
+ }
37
+
38
+ /**
39
+ * Groups decisions by phase, preserving insertion order.
40
+ */
41
+ function groupDecisionsByPhase(log: SessionLog): readonly ReportPhase[] {
42
+ const phaseOrder: string[] = [];
43
+ const phaseMap = new Map<string, ReportDecision[]>();
44
+
45
+ for (const d of log.decisions) {
46
+ if (!phaseMap.has(d.phase)) {
47
+ phaseOrder.push(d.phase);
48
+ phaseMap.set(d.phase, []);
49
+ }
50
+ const decisions = phaseMap.get(d.phase);
51
+ decisions?.push({
52
+ agent: d.agent,
53
+ decision: d.decision,
54
+ rationale: d.rationale,
55
+ });
56
+ }
57
+
58
+ return phaseOrder.map((phase) => ({
59
+ phase,
60
+ decisions: phaseMap.get(phase) ?? [],
61
+ }));
62
+ }
63
+
64
+ /**
65
+ * Builds the displayText report for the pipeline report.
66
+ */
67
+ function buildDisplayText(
68
+ log: SessionLog,
69
+ phases: readonly ReportPhase[],
70
+ durationMs: number,
71
+ ): string {
72
+ const lines: string[] = [];
73
+
74
+ // Header
75
+ const durationStr = log.endedAt ? formatDuration(durationMs) : "In progress";
76
+ lines.push(`Pipeline Report: ${log.sessionId}`);
77
+ lines.push(`Duration: ${durationStr}`);
78
+ lines.push(`Total Decisions: ${log.decisions.length}`);
79
+ lines.push("");
80
+
81
+ if (phases.length === 0) {
82
+ lines.push("No decisions recorded in this session.");
83
+ return lines.join("\n");
84
+ }
85
+
86
+ // Phase-by-phase breakdown
87
+ for (const phase of phases) {
88
+ lines.push(`--- ${phase.phase} (${phase.decisions.length} decision(s)) ---`);
89
+ for (const d of phase.decisions) {
90
+ lines.push(` [${d.agent}] ${d.decision}`);
91
+ lines.push(` Rationale: ${d.rationale}`);
92
+ }
93
+ lines.push("");
94
+ }
95
+
96
+ return lines.join("\n");
97
+ }
98
+
99
+ /**
100
+ * Core function for the oc_pipeline_report tool.
101
+ *
102
+ * @param sessionID - Optional session ID (uses latest if omitted)
103
+ * @param logsDir - Optional override for logs directory (for testing)
104
+ */
105
+ export async function pipelineReportCore(sessionID?: string, logsDir?: string): Promise<string> {
106
+ const log = sessionID
107
+ ? await readSessionLog(sessionID, logsDir)
108
+ : await readLatestSessionLog(logsDir);
109
+
110
+ if (!log) {
111
+ const target = sessionID ? `Session "${sessionID}" not found.` : "No session logs found.";
112
+ return JSON.stringify({
113
+ action: "error",
114
+ message: target,
115
+ });
116
+ }
117
+
118
+ const durationMs = computeDuration(log);
119
+ const phases = groupDecisionsByPhase(log);
120
+ const totalDecisions = log.decisions.length;
121
+ const displayText = buildDisplayText(log, phases, durationMs);
122
+
123
+ return JSON.stringify({
124
+ action: "pipeline_report",
125
+ sessionId: log.sessionId,
126
+ phases,
127
+ totalDecisions,
128
+ displayText,
129
+ });
130
+ }
131
+
132
+ // --- Tool wrapper ---
133
+
134
+ export const ocPipelineReport = tool({
135
+ description:
136
+ "View pipeline decision trace. Shows phase-by-phase breakdown of all autonomous decisions " +
137
+ "with agent and rationale. Read-only report for post-session analysis.",
138
+ args: {
139
+ sessionID: z
140
+ .string()
141
+ .regex(/^[a-zA-Z0-9_-]{1,256}$/)
142
+ .optional()
143
+ .describe("Session ID to view (uses latest if omitted)"),
144
+ },
145
+ async execute({ sessionID }) {
146
+ return pipelineReportCore(sessionID);
147
+ },
148
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * oc_session_stats tool - Event counts, decisions, errors, and per-phase breakdown.
3
+ *
4
+ * Reads a session log and computes:
5
+ * - Event count totals
6
+ * - Decision count and per-phase grouping
7
+ * - Error summary by type with per-phase attribution
8
+ * - Session duration
9
+ * - Per-phase breakdown (when decisions span multiple phases)
10
+ *
11
+ * Follows the *Core + tool() wrapper pattern per CLAUDE.md.
12
+ * Returns JSON with displayText field following oc_doctor pattern.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ import { tool } from "@opencode-ai/plugin";
18
+ import { z } from "zod";
19
+ import { readLatestSessionLog, readSessionLog } from "../observability/log-reader";
20
+ import { computeDuration, formatDuration } from "../observability/summary-generator";
21
+ import type { SessionLog } from "../observability/types";
22
+
23
+ /**
24
+ * Per-phase breakdown entry.
25
+ */
26
+ interface PhaseBreakdownEntry {
27
+ readonly phase: string;
28
+ readonly decisionCount: number;
29
+ readonly errorCount: number;
30
+ }
31
+
32
+ /**
33
+ * Computes per-phase breakdown from session log decisions and events.
34
+ * Groups decisions by phase and counts errors per phase time window.
35
+ *
36
+ * Error-to-phase mapping: for each error event, find the phase whose
37
+ * decision time window (first to last decision timestamp) contains the
38
+ * error timestamp. Unmatched errors are not attributed to any phase
39
+ * (they still appear in the overall errorSummary).
40
+ */
41
+ function computePhaseBreakdown(log: SessionLog): readonly PhaseBreakdownEntry[] {
42
+ const phaseMap = new Map<string, { decisions: number; errors: number }>();
43
+
44
+ // Collect per-phase time windows from decisions
45
+ const phaseWindows = new Map<string, { start: string; end: string }>();
46
+
47
+ for (const d of log.decisions) {
48
+ const existing = phaseMap.get(d.phase) ?? { decisions: 0, errors: 0 };
49
+ phaseMap.set(d.phase, { ...existing, decisions: existing.decisions + 1 });
50
+
51
+ const ts = d.timestamp ?? "";
52
+ if (!ts) continue; // Skip decisions without timestamps — cannot build time windows
53
+ const window = phaseWindows.get(d.phase);
54
+ if (!window) {
55
+ phaseWindows.set(d.phase, { start: ts, end: ts });
56
+ } else {
57
+ if (ts < window.start) phaseWindows.set(d.phase, { ...window, start: ts });
58
+ if (ts > window.end) phaseWindows.set(d.phase, { ...window, end: ts });
59
+ }
60
+ }
61
+
62
+ // Map errors to phases by timestamp overlap with phase time windows
63
+ for (const e of log.events) {
64
+ if (e.type === "error") {
65
+ for (const [phase, window] of phaseWindows) {
66
+ if (e.timestamp >= window.start && e.timestamp <= window.end) {
67
+ const data = phaseMap.get(phase);
68
+ if (data) {
69
+ phaseMap.set(phase, { ...data, errors: data.errors + 1 });
70
+ }
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ const result: PhaseBreakdownEntry[] = [];
78
+ for (const [phase, data] of phaseMap) {
79
+ result.push({
80
+ phase,
81
+ decisionCount: data.decisions,
82
+ errorCount: data.errors,
83
+ });
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Builds the displayText report for session stats.
91
+ */
92
+ function buildDisplayText(
93
+ log: SessionLog,
94
+ durationMs: number,
95
+ phaseBreakdown: readonly PhaseBreakdownEntry[],
96
+ ): string {
97
+ const lines: string[] = [];
98
+
99
+ // Header
100
+ lines.push(`Session Stats: ${log.sessionId}`);
101
+ lines.push("");
102
+
103
+ // Duration
104
+ const durationStr = log.endedAt ? formatDuration(durationMs) : "In progress";
105
+ lines.push(`Duration: ${durationStr}`);
106
+ lines.push(`Events: ${log.events.length}`);
107
+ lines.push(`Decisions: ${log.decisions.length}`);
108
+ lines.push("");
109
+
110
+ // Error summary
111
+ const errorEntries = Object.entries(log.errorSummary);
112
+ if (errorEntries.length > 0) {
113
+ lines.push("Error Summary:");
114
+ for (const [type, count] of errorEntries) {
115
+ lines.push(` ${type}: ${count}`);
116
+ }
117
+ lines.push("");
118
+ }
119
+
120
+ // Per-phase breakdown (when phases exist)
121
+ if (phaseBreakdown.length > 0) {
122
+ lines.push("Phase Breakdown:");
123
+ lines.push("| Phase | Decisions | Errors |");
124
+ lines.push("|-------|-----------|--------|");
125
+ for (const p of phaseBreakdown) {
126
+ lines.push(`| ${p.phase} | ${p.decisionCount} | ${p.errorCount} |`);
127
+ }
128
+ lines.push("");
129
+ }
130
+
131
+ return lines.join("\n");
132
+ }
133
+
134
+ /**
135
+ * Core function for the oc_session_stats tool.
136
+ *
137
+ * @param sessionID - Optional session ID (uses latest if omitted)
138
+ * @param logsDir - Optional override for logs directory (for testing)
139
+ */
140
+ export async function sessionStatsCore(sessionID?: string, logsDir?: string): Promise<string> {
141
+ const log = sessionID
142
+ ? await readSessionLog(sessionID, logsDir)
143
+ : await readLatestSessionLog(logsDir);
144
+
145
+ if (!log) {
146
+ const target = sessionID ? `Session "${sessionID}" not found.` : "No session logs found.";
147
+ return JSON.stringify({
148
+ action: "error",
149
+ message: target,
150
+ });
151
+ }
152
+
153
+ const durationMs = computeDuration(log);
154
+ const phaseBreakdown = computePhaseBreakdown(log);
155
+ const displayText = buildDisplayText(log, durationMs, phaseBreakdown);
156
+
157
+ return JSON.stringify({
158
+ action: "session_stats",
159
+ sessionId: log.sessionId,
160
+ duration: durationMs,
161
+ eventCount: log.events.length,
162
+ decisionCount: log.decisions.length,
163
+ errorSummary: log.errorSummary,
164
+ phaseBreakdown,
165
+ displayText,
166
+ });
167
+ }
168
+
169
+ // --- Tool wrapper ---
170
+
171
+ export const ocSessionStats = tool({
172
+ description:
173
+ "View session statistics including event counts, decisions, errors, and per-phase breakdown. " +
174
+ "Shows duration, error summary, and phase-by-phase activity.",
175
+ args: {
176
+ sessionID: z
177
+ .string()
178
+ .regex(/^[a-zA-Z0-9_-]{1,256}$/)
179
+ .optional()
180
+ .describe("Session ID to view (uses latest if omitted)"),
181
+ },
182
+ async execute({ sessionID }) {
183
+ return sessionStatsCore(sessionID);
184
+ },
185
+ });