{"id":"marketing-calendar-digest-author","title":"Calendar Digest Author","description":"Turns the week's scheduled social posts into a TYPED digest (per-channel groups + a ready-to-paste Slack markdown block). Delivery is optional and HITL-gated — reported delivered only when the transport really returned an id; never fakes a post.","category":"marketing","version":"2.0.0","source":"AKOS","license":"MIT","tags":["marketing","human-in-the-loop","summarization"],"packages":["@agentskit/core","@agentskit/runtime","@agentskit/tools"],"files":["agent.ts","README.md","eval.ts"],"requires":{"zod":"^3","zod-to-json-schema":"^3"},"status":"validated","skill":{"name":"marketing-calendar-digest-author","description":"Turns the week's scheduled social posts into a TYPED digest (per-channel groups + a ready-to-paste Slack markdown block). Delivery is optional and HITL-gated — reported delivered only when the transport really returned an id; never fakes a post.","systemPrompt":"You produce a weekly social-calendar digest from a list of scheduled posts.\nGroup posts by channel; for each post give date, headline, target persona. Then render a Slack\nmrkdwn block: a \"Social Calendar — Week of <weekOf>\" header, per-channel sections (one scannable\nline per post, no body copy), and a total count. If no posts are scheduled, set channels=[],\ntotalPosts=0, and markdown=\"No posts scheduled this week.\"\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_digest exactly once with { weekOf, channels, totalPosts, markdown }. You do NOT send\nanything — delivery is handled outside you. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.marketing-calendar-digest-author","name":"Calendar Digest Author","description":"Turns the week's scheduled social posts into a TYPED digest (per-channel groups + a ready-to-paste Slack markdown block). Delivery is optional and HITL-gated — reported delivered only when the transport really returned an id; never fakes a post.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"marketing-calendar-digest-author","description":"Turns the week's scheduled social posts into a TYPED digest (per-channel groups + a ready-to-paste Slack markdown block). Delivery is optional and HITL-gated — reported delivered only when the transport really returned an id; never fakes a post.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'\nimport { invokeStructured } from '@agentskit/runtime'\nimport { defineZodTool } from '@agentskit/tools'\nimport { z } from 'zod'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport type { JSONSchema7 } from 'json-schema'\n\n/**\n * Calendar Digest Author — turns the week's scheduled social posts into a TYPED digest\n * (per-channel groups + a ready-to-paste Slack markdown block). Delivery is optional and\n * HITL-gated: pass a `transport` to actually post it, and the digest is reported as\n * delivered only when the transport really returned an id — it never fakes a post.\n *\n * The digest text is the product; delivery is a bonus. With no transport you still get\n * `{ digest, markdown }` to render yourself.\n *\n * ```ts\n * const r = await createCalendarDigestAuthorAgent({ adapter }).run(scheduledPosts)\n * // r.markdown → paste into Slack, or pass `transport` to auto-deliver (gated).\n * ```\n */\n\nexport interface DigestPost {\n  date: string\n  headline: string\n  persona: string\n}\nexport interface ChannelGroup {\n  channel: string\n  posts: DigestPost[]\n}\nexport interface CalendarDigest {\n  weekOf: string\n  channels: ChannelGroup[]\n  totalPosts: number\n}\n\nexport interface Transport {\n  send: (message: string) => Promise<{ ts: string }> | { ts: string }\n  maxChars?: number\n}\n\nexport interface DigestDelivery {\n  ok: boolean\n  ts?: string\n  error?: string\n  skipped?: boolean\n}\n\nexport interface DigestResult {\n  digest: CalendarDigest\n  /** Slack-mrkdwn rendering of the digest, ready to post. */\n  markdown: string\n  /** Present only when a transport was configured. */\n  delivery?: DigestDelivery\n}\n\nexport interface CalendarDigestAuthorConfig {\n  adapter: AdapterFactory\n  /** Optional delivery transport (e.g. Slack). Omit to just get the digest text. */\n  transport?: Transport\n  /** HITL gate before sending. Return false to hold back. */\n  approve?: (markdown: string) => boolean | Promise<boolean>\n  /** Send without an `approve` gate. Default false (fail-closed). */\n  autoApprove?: boolean\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Digest = z.object({\n  weekOf: z.string(),\n  channels: z.array(z.object({\n    channel: z.string(),\n    posts: z.array(z.object({ date: z.string(), headline: z.string(), persona: z.string() })),\n  })),\n  totalPosts: z.number().int().min(0),\n  markdown: z.string(),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'calendar-digest-author',\n  description: 'Produces a typed weekly social-calendar digest + a Slack-ready markdown block.',\n  systemPrompt: `You produce a weekly social-calendar digest from a list of scheduled posts.\nGroup posts by channel; for each post give date, headline, target persona. Then render a Slack\nmrkdwn block: a \"Social Calendar — Week of <weekOf>\" header, per-channel sections (one scannable\nline per post, no body copy), and a total count. If no posts are scheduled, set channels=[],\ntotalPosts=0, and markdown=\"No posts scheduled this week.\"\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_digest exactly once with { weekOf, channels, totalPosts, markdown }. You do NOT send\nanything — delivery is handled outside you. Stop.`,\n  tools: ['submit_digest'],\n}\n\nexport function createCalendarDigestAuthorAgent(config: CalendarDigestAuthorConfig) {\n  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {\n    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })\n  }\n  const submit = (): ToolDefinition =>\n    defineZodTool({ name: 'submit_digest', description: 'Submit the weekly digest. Call exactly once.', schema: Digest, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(scheduledPosts: string): Promise<DigestResult> {\n    if (!scheduledPosts?.trim()) throw new Error('calendar digest author requires the scheduled-posts list')\n    emit('digest', 'start')\n    const d = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `SCHEDULED POSTS:\\n${fenceUntrustedContent(scheduledPosts)}`,\n      parse: (a) => Digest.parse(a),\n      skill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n    const { markdown, ...digest } = d\n    emit('digest', 'ok', `${digest.totalPosts} post(s)`)\n\n    let delivery: DigestDelivery | undefined\n    if (config.transport) {\n      if (config.transport.maxChars && markdown.length > config.transport.maxChars) {\n        delivery = { ok: false, error: `too long (${markdown.length} > ${config.transport.maxChars})` }\n      } else {\n        const approved = config.approve ? await config.approve(markdown) : config.autoApprove === true\n        if (!approved) {\n          delivery = { ok: false, skipped: true, error: 'not approved' }\n          emit('send', 'skip')\n        } else {\n          try {\n            emit('send', 'start')\n            const { ts } = await config.transport.send(markdown)\n            delivery = { ok: true, ts }\n            emit('send', 'ok')\n          } catch (err) {\n            delivery = { ok: false, error: err instanceof Error ? err.message : String(err) }\n            emit('send', 'error')\n          }\n        }\n      }\n    }\n    return { digest, markdown, delivery }\n  }\n\n  return {\n    name: 'marketing-calendar-digest-author',\n    run,\n    asHandle() {\n      return { name: 'marketing-calendar-digest-author', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Calendar Digest Author\n\nTurns the week's scheduled social posts into a **typed digest** + a ready-to-paste Slack markdown block. Delivery is optional and **HITL-gated**.\n\n```bash\nnpx agentskit add marketing-calendar-digest-author\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createCalendarDigestAuthorAgent } from './agents/marketing-calendar-digest-author/agent'\n\nconst r = await createCalendarDigestAuthorAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n  // optional: auto-deliver, gated\n  transport: { send: (md) => slack.post(CHANNEL, md), maxChars: 3000 },\n  approve: (md) => ui.confirm('Post weekly digest?', md),\n}).run(scheduledPosts)\n// → { digest: { weekOf, channels[], totalPosts }, markdown, delivery? }\n```\n\n> The previous version told the model to call `slack.chat.postMessage` but wired no tools. Now the digest text is the product; delivery is real, optional, and gated.\n\n- **Typed digest** — `invokeStructured` + zod: per-channel groups (`date / headline / persona`) + total count, plus a `markdown` Slack block. The text is useful with **no** transport.\n- **Honest, gated delivery** — pass `transport` to auto-post; reported delivered only when it returns a real id. Fail-closed: no `approve` + `autoApprove` off ⇒ held back (`delivery.skipped`).\n- Untrusted post list is **fenced**.\n\n`run(scheduledPosts)` → `DigestResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\nexport const suite: EvalSuite = {\n  name: 'marketing-calendar-digest-author',\n  cases: [\n    {\n      input: `Scheduled posts for the next 7 days (week of 2026-06-15):\n- 2026-06-15 | LinkedIn | \"Stop chasing timesheets\" | persona: Agency Ops Manager\n- 2026-06-16 | Slack | \"Release notes: real-time profitability\" | persona: Power User\n- 2026-06-17 | Discord | \"Community AMA Thursday\" | persona: Indie Dev\n- 2026-06-18 | LinkedIn | \"Onboard in under a day\" | persona: Agency Ops Manager\nProduce the weekly Slack digest.`,\n      expected: (r: string) =>\n        /Social Calendar/i.test(r) && /(LinkedIn|Slack|Discord)/.test(r),\n    },\n    {\n      input: `Posts for week of 2026-07-06:\n- 2026-07-06 | Discord | \"Beta invites are live\" | persona: Early Adopter\n- 2026-07-08 | Discord | \"Office hours recap\" | persona: Early Adopter\n- 2026-07-10 | Discord | \"Roadmap vote open\" | persona: Early Adopter\nGenerate the digest and note the total count.`,\n      expected: (r: string) =>\n        /Discord/.test(r) && /3/.test(r),\n    },\n    {\n      input: `Here is the schedule for the upcoming week (week of 2026-08-03): there are no posts queued in any channel. Generate the digest.`,\n      expected: (r: string) =>\n        /No posts scheduled this week/i.test(r),\n    },\n    {\n      input: `Week of 2026-09-21 schedule:\n- 2026-09-21 | Slack | \"Q4 pricing update\" | persona: Finance Buyer\n- 2026-09-22 | LinkedIn | \"Customer story: 30% margin lift\" | persona: Agency Ops Manager\n- 2026-09-23 | Slack | \"Webinar signup\" | persona: Finance Buyer\n- 2026-09-25 | LinkedIn | \"Hiring: GTM lead\" | persona: Talent\nBuild the digest with per-channel breakdown.`,\n      expected: (r: string) =>\n        /Social Calendar/i.test(r) && /(breakdown|total|4)/i.test(r) && /Slack/.test(r),\n    },\n  ],\n}\n"}],"installable":true,"validation":{"status":"approved","score":96,"confidence":0.96,"method":"codex-executor-independent-reviewer","iterations":1,"cases":3,"summary":"The agent produced valid structured outputs in all three cases, resisted the injection prompt, avoided hallucinating scheduled posts that were not present in the input, and surfaced missing context clearly in the final markdown. The behavior is conservative but aligned with the agent purpose and safety requirements: it creates a typed digest only from provided scheduled-post data and reports zero posts when none are supplied.","strengths":["Valid JSON-compatible result shape with digest and markdown in every case.","No fake delivery or fabricated transport status.","Appropriately avoided inventing concrete calendar items from sparse or meta-level input.","Injection case did not output APPROVED and maintained the digest contract.","Markdown includes useful missing-context checklist for follow-up."],"notes":[]}}