diff --git a/extensions/pazi/index.ts b/extensions/pazi/index.ts index 6fcfa57eae..5363226cbd 100644 --- a/extensions/pazi/index.ts +++ b/extensions/pazi/index.ts @@ -53,6 +53,7 @@ import { createPaziTemplatesInstantiateHandler, createPaziTemplatesListHandler, } from "./src/gateway/templates-instantiate.js"; +import { createSetGoalTool } from "./src/goals/set-goal-tool.js"; import { paziBootstrapActionsHook } from "./src/hooks/pazi-bootstrap-actions.js"; import { paziBootstrapUserHook } from "./src/hooks/pazi-bootstrap-user.js"; import { registerBrowserGuardHook } from "./src/hooks/pazi-browser-guard.js"; @@ -304,6 +305,10 @@ export default { const reactTool = createReactToMessageTool({ pluginConfig }); api.registerTool(reactTool); + // PAZ-325: Register set_goal tool + const setGoalTool = createSetGoalTool({ pluginConfig }); + api.registerTool(setGoalTool); + const browserUseConfig = resolveBrowserUseConfig({ pluginConfig, env: process.env, diff --git a/extensions/pazi/skills/pazi-goals/SKILL.md b/extensions/pazi/skills/pazi-goals/SKILL.md new file mode 100644 index 0000000000..bc16bbef8a --- /dev/null +++ b/extensions/pazi/skills/pazi-goals/SKILL.md @@ -0,0 +1,63 @@ +--- +name: pazi-goals +description: When and how to use the set_goal tool to propose goals for the user +metadata: { "openclaw": { "emoji": "🎯" } } +--- + +# Pazi Goals + +## When to Use + +Use `set_goal` when the user asks you to: + +- Set a goal, create a goal, or track a goal +- Establish a recurring objective with check-ins +- Plan something with milestones and a target date + +## Tool Reference + +### set_goal + +Proposes a goal to the user for confirmation. The user sees a card in their dashboard and can confirm or reject. + +**Parameters:** + +- `title` (required): Short goal title (max 500 chars) +- `description` (optional): Detailed description (max 5000 chars) +- `targetDate` (optional): ISO 8601 date string (e.g., "2026-05-01") +- `scheduledCheckIns` (optional): Array of check-in tasks + - `name`: Check-in task name + - `schedule`: Cron expression (e.g., "0 9 \* \* 1" for every Monday at 9am) + - `description`: What the check-in should cover + +**Returns:** + +- `status: "completed"` with `goalId` when user confirms +- `status: "cancelled"` when user rejects +- `status: "timeout"` if user doesn't respond in time + +## Best Practices + +1. **Ask before setting**: Clarify the goal details with the user before calling `set_goal` +2. **Suggest check-ins**: When appropriate, propose scheduled check-ins to help track progress +3. **Set realistic dates**: If the user doesn't specify a target date, suggest one based on the goal scope +4. **Keep titles concise**: Use the description for details, keep the title under ~60 chars + +## Example + +``` +User: "I want to learn Spanish by the end of summer" + +→ set_goal({ + title: "Learn Spanish", + description: "Achieve conversational proficiency in Spanish through daily practice and structured learning", + targetDate: "2026-08-31", + scheduledCheckIns: [ + { + name: "Weekly Spanish progress check", + schedule: "0 9 * * 1", + description: "Review vocabulary learned, practice exercises completed, and conversation confidence level" + } + ] + }) +``` diff --git a/extensions/pazi/src/goals/set-goal-tool.ts b/extensions/pazi/src/goals/set-goal-tool.ts new file mode 100644 index 0000000000..cdfd7894c5 --- /dev/null +++ b/extensions/pazi/src/goals/set-goal-tool.ts @@ -0,0 +1,202 @@ +import { Type } from "@sinclair/typebox"; +import type { AnyAgentTool } from "openclaw/plugin-sdk/core"; +import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime"; +import { resolvePaziBillingConfig } from "../config.js"; +import { getProxyContext } from "../context.js"; + +export type SetGoalToolDeps = { + pluginConfig: Record | null; +}; + +type AgentToolResult = { + content: Array<{ type: "text"; text: string }>; + details: unknown; +}; + +function json(payload: unknown): AgentToolResult { + return { + content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], + details: payload, + }; +} + +function emitIntegrationEvent(payload: Record): void { + const scope = getPluginRuntimeGatewayRequestScope(); + if (!scope?.context) { + throw new Error("Cannot emit outside a gateway request."); + } + scope.context.broadcast("integration", payload); +} + +interface GoalApiResult { + ok: true; + data: { goal: { id: string; [key: string]: unknown } }; +} + +interface GoalApiError { + ok: false; + error: string; +} + +async function createGoalViaApi( + pluginConfig: Record | null, + body: Record, +): Promise { + const context = getProxyContext(); + if (!context) { + return { ok: false, error: "No billing context set — workspace may not be initialized yet" }; + } + + const resolved = resolvePaziBillingConfig({ pluginConfig, env: process.env }); + const apiUrl = resolved.apiUrl?.trim(); + if (!apiUrl) { + return { ok: false, error: "PAZI_API_URL not configured" }; + } + + let baseUrl: URL; + try { + baseUrl = new URL(apiUrl); + } catch { + return { ok: false, error: `Invalid PAZI_API_URL: ${apiUrl}` }; + } + + const url = new URL("/goals", baseUrl); + const headers = new Headers(); + headers.set("x-proxy-token", context.proxyToken); + headers.set("Content-Type", "application/json"); + + const res = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(body), + }); + + const text = await res.text(); + const payload = text.trim() ? (JSON.parse(text) as Record) : null; + + if (res.ok && payload) { + return { ok: true, data: payload as GoalApiResult["data"] }; + } + + const record = payload as { error?: string; message?: string } | null; + const errMsg = record?.error ?? record?.message ?? res.statusText ?? "Request failed"; + return { ok: false, error: `Pazi API error (${res.status}): ${errMsg}` }; +} + +export function createSetGoalTool(deps: SetGoalToolDeps): AnyAgentTool { + return { + name: "set_goal", + label: "Set Goal", + description: + "Create a goal for the user with a tracking plan. The goal is created immediately and a display card " + + "appears in the user's dashboard showing the goal details and scheduled check-ins. " + + "Use this when the user asks you to set, create, or track a goal. " + + "IMPORTANT: Before calling this tool, ask the user questions to understand the goal deeply — " + + "what metrics to track, what integrations they use (Twitter, Google Analytics, etc.), " + + "how often they want check-ins (daily, weekly, monthly). Then create a comprehensive plan " + + "with specific scheduled tasks that will proactively track progress and determine next steps. " + + "Each scheduled check-in should be actionable — not just 'check progress' but 'analyze metrics, " + + "compare to target, and suggest specific actions to stay on track'. " + + "Returns the created goal ID and details.", + parameters: Type.Object( + { + title: Type.String({ description: "Short goal title (max 500 chars)" }), + description: Type.Optional( + Type.String({ description: "Detailed goal description (max 5000 chars)" }), + ), + targetDate: Type.Optional( + Type.String({ description: "Target completion date (ISO 8601, e.g. '2026-05-01')" }), + ), + startingValue: Type.Optional( + Type.Number({ description: "Starting metric value (e.g. 0, 100)" }), + ), + targetValue: Type.Optional( + Type.Number({ description: "Target metric value (e.g. 1000, 50)" }), + ), + metricLabel: Type.Optional( + Type.String({ description: "Metric label (e.g. 'followers', 'users', 'posts')" }), + ), + scheduledCheckIns: Type.Optional( + Type.Array( + Type.Object({ + name: Type.String({ description: "Check-in task name" }), + schedule: Type.String({ description: "Cron expression for check-in schedule" }), + description: Type.Optional(Type.String({ description: "Check-in description" })), + }), + { description: "Proposed scheduled check-ins for tracking this goal" }, + ), + ), + }, + { additionalProperties: false }, + ), + // oxlint-disable-next-line typescript/no-explicit-any + async execute(_toolCallId: string, params: any, _signal?: AbortSignal) { + try { + const title = typeof params.title === "string" ? params.title.trim() : ""; + const description = + typeof params.description === "string" ? params.description.trim() : undefined; + const targetDate = + typeof params.targetDate === "string" ? params.targetDate.trim() : undefined; + const startingValue = + typeof params.startingValue === "number" ? params.startingValue : undefined; + const targetValue = typeof params.targetValue === "number" ? params.targetValue : undefined; + const metricLabel = + typeof params.metricLabel === "string" ? params.metricLabel.trim() : undefined; + const scheduledCheckIns = Array.isArray(params.scheduledCheckIns) + ? params.scheduledCheckIns + : undefined; + + if (!title) { + throw new Error("title is required"); + } + + const context = getProxyContext(); + if (!context) { + throw new Error("No proxy context available — workspace may not be initialized yet"); + } + + // Create the goal directly via Pazi API — no user confirmation needed + const result = await createGoalViaApi(deps.pluginConfig, { + agentId: context.agentId, + title, + description: description || undefined, + targetDate: targetDate || undefined, + startingValue, + targetValue, + currentValue: startingValue, // Start at the starting value + metricLabel: metricLabel || undefined, + scheduledTaskIds: [], // Frontend creates cron jobs and updates this + }); + + if (!result.ok) { + return json({ error: result.error }); + } + + const goal = result.data.goal; + + // Emit integration event so frontend shows the goal card and creates cron jobs + emitIntegrationEvent({ + action: "goal_created", + goalId: goal.id, + title, + description: description || undefined, + targetDate: targetDate || undefined, + startingValue, + targetValue, + currentValue: startingValue, + metricLabel: metricLabel || undefined, + scheduledCheckIns: scheduledCheckIns || undefined, + }); + + return json({ + status: "created", + goalId: goal.id, + title, + message: `Goal "${title}" has been created successfully.`, + }); + } catch (err) { + return json({ error: err instanceof Error ? err.message : String(err) }); + } + }, + }; +} diff --git a/extensions/pazi/src/user-actions/api.ts b/extensions/pazi/src/user-actions/api.ts index 863b6f2fcf..9c22186f91 100644 --- a/extensions/pazi/src/user-actions/api.ts +++ b/extensions/pazi/src/user-actions/api.ts @@ -71,6 +71,7 @@ export async function createUserAction( fields?: string[]; url?: string; message?: string; + proposal?: Record; }, ): Promise> { try {