import { randomUUID } from "node:crypto";
import { PilotDeckToolRuntimeError } from "../../tool/protocol/errors.js";
import type { PilotDeckToolDefinition } from "../../tool/protocol/types.js";
import { parsePlanMarkdown, type PlanContractOptions } from "../contracts/PlanContract.js";
import { AlwaysOnError } from "../protocol/errors.js";
import type { DiscoveryPlanRecord } from "../protocol/types.js";
import type { AlwaysOnRunContextRegistry, DiscoveryRunContext } from "../runtime/AlwaysOnRunContextRegistry.js";
export type AlwaysOnDiscoveryPlanInput = {
title: string;
summary: string;
rationale: string;
dedupeKey: string;
content: string;
};
export type AlwaysOnDiscoveryPlanOutput = {
ok: true;
planId: string;
planFilePath: string;
dedupeKey: string;
};
export type CreateAlwaysOnDiscoveryPlanToolOptions = {
runContexts: AlwaysOnRunContextRegistry;
contract?: PlanContractOptions;
now?: () => Date;
uuid?: () => string;
};
export const ALWAYS_ON_PLAN_TOOL_NAME = "always_on_discovery_plan";
export function createAlwaysOnDiscoveryPlanTool(
options: CreateAlwaysOnDiscoveryPlanToolOptions,
): PilotDeckToolDefinition<AlwaysOnDiscoveryPlanInput, AlwaysOnDiscoveryPlanOutput> {
const now = options.now ?? (() => new Date());
const uuid = options.uuid ?? randomUUID;
return {
name: ALWAYS_ON_PLAN_TOOL_NAME,
aliases: ["AlwaysOnDiscoveryPlan"],
description:
"Save the single discovery plan for this Always-On fire. Returns plan_quota_exhausted if called more than once per fire. Plan content must follow the PilotDeck Always-On plan markdown contract.",
kind: "session",
inputSchema: {
type: "object",
required: ["title", "summary", "rationale", "dedupeKey", "content"],
additionalProperties: false,
properties: {
title: { type: "string" },
summary: { type: "string" },
rationale: { type: "string" },
dedupeKey: { type: "string" },
content: { type: "string", description: "Full plan markdown body." },
},
},
isReadOnly: () => false,
isConcurrencySafe: () => false,
execute: async (input, context) => {
const ctx = options.runContexts.getDiscovery(context.sessionId);
if (!ctx) {
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
`${ALWAYS_ON_PLAN_TOOL_NAME} called outside of an Always-On discovery turn.`,
);
}
ctx.planCallCount += 1;
if (ctx.plan) {
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
"plan_quota_exhausted: Always-On discovery permits at most one plan per fire.",
);
}
let parsed;
try {
parsed = parsePlanMarkdown(input.content, options.contract);
} catch (error) {
if (error instanceof AlwaysOnError) {
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
`plan_invalid: ${error.message}`,
);
}
throw error;
}
const planId = parsed.metadata.id || `plan_${uuid()}`;
const filePath = await ctx.planStore.writePlanMarkdown(planId, parsed.rawContent);
const record: DiscoveryPlanRecord = {
id: planId,
title: input.title.trim() || parsed.title,
createdAt: now().toISOString(),
status: "ready",
summary: input.summary.trim(),
rationale: input.rationale.trim(),
dedupeKey: input.dedupeKey.trim() || parsed.metadata.dedupeKey,
sourceRunId: parsed.metadata.sourceRunId || ctx.runId,
planFilePath: filePath,
};
const stored = await ctx.planStore.upsert(record);
ctx.plan = { record: stored, markdown: parsed.rawContent };
const data: AlwaysOnDiscoveryPlanOutput = {
ok: true,
planId: stored.id,
planFilePath: stored.planFilePath,
dedupeKey: stored.dedupeKey,
};
return {
content: [
{ type: "text", text: `Plan saved as ${stored.id} (${stored.dedupeKey}).` },
{ type: "json", value: data },
],
data,
metadata: { runId: ctx.runId },
};
},
};
}