|
1 | | -import fs from "node:fs/promises"; |
2 | | -import path from "node:path"; |
3 | | -import type { AgentEntry } from "@/lib/agent-log"; |
4 | | - |
5 | | -const LOG_PATH = path.join(process.cwd(), "content", "agent-log.json"); |
6 | | - |
7 | 1 | /** |
8 | | - * Appends an entry to the public agent log. Idempotent on `id` — if an entry |
9 | | - * with the same id is already present, it's replaced rather than duplicated. |
| 2 | + * Append an entry to the public agent log via the spec Context. |
10 | 3 | * |
11 | | - * Designed to run from a Node-side handler (Cloudflare Worker, scheduled job, |
12 | | - * webhook receiver). At deploy time, the static export bakes the log into the |
13 | | - * /agent page; for live updates we'd swap this for an edge KV write. |
| 4 | + * The log file is a JSON array at /_meta/agent-log.json in workspace VFS. |
| 5 | + * Each runtime impl decides what that path means: |
| 6 | + * - Cloudflare runtime → committed to content/agent-log.json on GitHub |
| 7 | + * - Node/local dev → written to ./content/agent-log.json on disk |
| 8 | + * |
| 9 | + * Agent code calls this via `await writeLogEntry(ctx, {...})` and stays |
| 10 | + * runtime-agnostic. |
14 | 11 | */ |
15 | | -export async function writeLogEntry( |
16 | | - entry: Omit<AgentEntry, "id" | "timestamp"> & { |
17 | | - id?: string; |
18 | | - timestamp?: string; |
19 | | - } |
20 | | -): Promise<AgentEntry> { |
21 | | - const full: AgentEntry = { |
22 | | - id: entry.id ?? `${Date.now()}-${entry.agent}-${Math.random().toString(36).slice(2, 8)}`, |
23 | | - timestamp: entry.timestamp ?? new Date().toISOString(), |
24 | | - agent: entry.agent, |
25 | | - trigger: entry.trigger, |
26 | | - action: entry.action, |
27 | | - summary: entry.summary, |
28 | | - outcome: entry.outcome, |
29 | | - links: entry.links, |
30 | | - skippedReason: entry.skippedReason, |
| 12 | +import type { Context } from "./sdk"; |
| 13 | +import type { AgentEntry } from "@/lib/agent-log"; |
| 14 | + |
| 15 | +const LOG_VFS_PATH = "/_meta/agent-log.json"; |
| 16 | + |
| 17 | +export type LogInput = Omit<AgentEntry, "id" | "timestamp"> & { |
| 18 | + id?: string; |
| 19 | + timestamp?: string; |
| 20 | +}; |
| 21 | + |
| 22 | +export async function writeLogEntry(ctx: Context, input: LogInput): Promise<AgentEntry> { |
| 23 | + const entry: AgentEntry = { |
| 24 | + id: input.id ?? `${Date.now()}-${input.agent}-${Math.random().toString(36).slice(2, 8)}`, |
| 25 | + timestamp: input.timestamp ?? new Date().toISOString(), |
| 26 | + agent: input.agent, |
| 27 | + trigger: input.trigger, |
| 28 | + action: input.action, |
| 29 | + summary: input.summary, |
| 30 | + outcome: input.outcome, |
| 31 | + links: input.links, |
| 32 | + skippedReason: input.skippedReason, |
31 | 33 | }; |
32 | 34 |
|
33 | | - const existing = await readLog(); |
34 | | - const without = existing.filter((e) => e.id !== full.id); |
35 | | - const next = [full, ...without]; |
36 | | - await fs.writeFile(LOG_PATH, JSON.stringify(next, null, 2) + "\n", "utf8"); |
37 | | - return full; |
38 | | -} |
| 35 | + const file = await ctx.files.read(LOG_VFS_PATH); |
| 36 | + const existing = (file?.body as AgentEntry[] | undefined) ?? []; |
| 37 | + const without = existing.filter((e) => e.id !== entry.id); |
| 38 | + const next = [entry, ...without]; |
| 39 | + |
| 40 | + await ctx.files.write(LOG_VFS_PATH, next, { |
| 41 | + message: `[${entry.agent}] ${entry.action}`, |
| 42 | + }); |
39 | 43 |
|
40 | | -async function readLog(): Promise<AgentEntry[]> { |
41 | | - try { |
42 | | - const raw = await fs.readFile(LOG_PATH, "utf8"); |
43 | | - return JSON.parse(raw) as AgentEntry[]; |
44 | | - } catch { |
45 | | - return []; |
46 | | - } |
| 44 | + return entry; |
47 | 45 | } |
0 commit comments