Add chief-of-staff scenario: Chief of Staff autopilot using Node.js SDK - #333
Add chief-of-staff scenario: Chief of Staff autopilot using Node.js SDK#333Yogeshp-MSFT (Yogeshp-MSFT) wants to merge 6 commits into
Conversation
Autonomous Microsoft Agent 365 teammate that runs a leader's operating rhythm. Captures decisions and action items from Teams meetings into Planner, sends a daily Brief card, follows up with owners, books unblock meetings, escalates non-responsive owners, and lets people close tasks by chat. Capabilities: Capture, Daily Brief, Follow-up, Extension request, Blocker meeting, Escalation, Task complete (Planner + chat), Recall. Deterministic TypeScript orchestration around gpt-4o (Azure OpenAI) for capture extraction and recall chit-chat. Every other flow is deterministic TypeScript with idempotent Adaptive Card verbs and file-backed state that survives restarts. See scenarios/chief-of-staff/README.md for end-to-end setup and scenarios/chief-of-staff/DESIGN.md for architecture + per-flow sequence diagrams.
There was a problem hiding this comment.
Pull request overview
Adds a new Node.js/TypeScript “Chief of Staff” scenario sample under scenarios/chief-of-staff/, showcasing an autonomous Agent 365 teammate that orchestrates meeting capture, Planner tasking, brief/follow-up cards, and proactive workflows in Teams, backed by Azure OpenAI and Graph.
Changes:
- Introduces a full in-process runtime (Express endpoint + AgentApplication handlers + cron/poller scheduler) for capture/brief/follow-up/escalation/task-complete flows.
- Adds Graph integration utilities (app-only token acquisition, Planner config auto-resolution, transcript/insights polling) plus file-backed persistent state.
- Adds end-to-end documentation and tenant bootstrap scripts for provisioning a standalone Graph worker app and validating permissions.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| scenarios/chief-of-staff/README.md | End-to-end setup, verification flows, troubleshooting, deployment guidance. |
| scenarios/chief-of-staff/DESIGN.md | Architecture/design details for the scenario sample. |
| scenarios/chief-of-staff/.env.template | Full environment-variable template for local/dev/prod configuration. |
| scenarios/chief-of-staff/.gitignore | Excludes secrets, build output, manifests, logs, and persisted runtime state. |
| scenarios/chief-of-staff/package.json | Node scripts + dependencies (Agents SDK, A365 extensions, Graph/MSAL, cron, express). |
| scenarios/chief-of-staff/tsconfig.json | TypeScript compiler configuration for building to dist/. |
| scenarios/chief-of-staff/ToolingManifest.json | Dev-mode MCP server registration for Teams/Mail/Calendar tools. |
| scenarios/chief-of-staff/compare_grants.ps1 | Diagnostic script to compare auth grants between agent instance SPs. |
| scenarios/chief-of-staff/scripts/bootstrap-graph-app.ps1 | One-shot provisioning for the standalone Graph worker app + consent. |
| scenarios/chief-of-staff/src/index.ts | Express host wiring (/api/health, /api/messages) + dotenv + global error hooks. |
| scenarios/chief-of-staff/src/agent.ts | AgentApplication event routing (messages, invokes, notifications) + fast-ack for card submits. |
| scenarios/chief-of-staff/src/client.ts | OpenAI Agents client setup, tool wiring, MCP tool registration, observability integration. |
| scenarios/chief-of-staff/src/openai-config.ts | Azure OpenAI client configuration + @openai/agents defaults. |
| scenarios/chief-of-staff/src/startup-check.ts | Startup banner validating critical env/config and runtime mode. |
| scenarios/chief-of-staff/src/scheduler.ts | Cron + poller orchestration, proactive TurnContext recreation, follow-up escalation sweep. |
| scenarios/chief-of-staff/src/util/logger.ts | Central log utility with level gating and basic redaction/truncation. |
| scenarios/chief-of-staff/src/util/httpLogger.ts | Optional axios interceptor-based HTTP tracing with header redaction. |
| scenarios/chief-of-staff/src/state/persistentMap.ts | File-backed persistent map with debounced atomic writes + shutdown flush. |
| scenarios/chief-of-staff/src/state/pendingCaptureStore.ts | Persistent in-flight capture tracking + retention pruning + retry cadence helpers. |
| scenarios/chief-of-staff/src/state/followupStore.ts | Persistent follow-up tracking + retention window for cooldown behavior. |
| scenarios/chief-of-staff/src/state/conversationRefs.ts | Persistent per-user ConversationReference cache for proactive card DMs. |
| scenarios/chief-of-staff/src/graph/graphAppToken.ts | App-only Graph token acquisition via MSAL confidential client. |
| scenarios/chief-of-staff/src/graph/plannerConfig.ts | Auto-resolve Planner plan/bucket from team identifier + negative caching. |
| scenarios/chief-of-staff/src/graph/plannerPoller.ts | Poller that detects task completion transitions with persisted baseline. |
| scenarios/chief-of-staff/src/graph/plannerTools.ts | Planner Graph-backed tool surface for the LLM/runtime. |
| scenarios/chief-of-staff/src/graph/peopleTools.ts | Directory/people utilities + tools (attendees list, user search, team membership gate). |
| scenarios/chief-of-staff/src/graph/meetingWatcher.ts | Calendar-driven meeting discovery to seed capture retries. |
| scenarios/chief-of-staff/src/graph/meetingArtifactsFetch.ts | Transcript + Copilot insights fetchers (v1.0 + beta fallback) and transcript content fetch. |
| scenarios/chief-of-staff/src/graph/transcriptPoller.ts | Orchestrator to advance per-meeting capture state to READY and emit payloads. |
| scenarios/chief-of-staff/src/cos/capture.ts | Capture flow prompt assembly + Planner task creation orchestration via tools. |
| scenarios/chief-of-staff/src/cos/brief.ts | Deterministic daily/weekly brief generation using Graph + proactive Adaptive Cards. |
| scenarios/chief-of-staff/src/cos/followup.ts | Deterministic follow-up selection + cooldown logic + proactive check-in cards. |
| scenarios/chief-of-staff/src/cos/taskComplete.ts | Task completion handler that DMs assignees/leader and clears follow-up state. |
| scenarios/chief-of-staff/src/cos/escalate.ts | Escalation “scan + propose options” flow implemented as an LLM-driven routine. |
| scenarios/chief-of-staff/src/cards/proactiveSend.ts | Bot Framework proactive card sender using cached ConversationReferences. |
| scenarios/chief-of-staff/src/cards/briefTool.ts | send_brief_card tool + Adaptive Card rendering (structured + legacy-string paths). |
| scenarios/chief-of-staff/src/cards/followupCards.ts | Adaptive Card builders and direct-send helpers for follow-up/escalation/task assignment. |
| scenarios/chief-of-staff/src/cards/actionRouter.ts | Adaptive Card action routing / keyword fallback handling. |
Comments suppressed due to low confidence (1)
scenarios/chief-of-staff/ToolingManifest.json:31
- ToolingManifest.json has trailing blank lines after the closing brace. This tends to cause noisy diffs and can violate repo formatting checks in some pipelines.
}
- meetingWatcher.ts: force calendarView to return UTC times via Prefer: outlook.timezone="UTC" header. Without it, Graph returns the mailbox owner's default TZ and the naive Date.parse(...+ 'Z') parsing silently computes wrong endMs / giveUpAfter timestamps. - meetingWatcher.ts: remove the no-op organizerAad: ... ? undefined : undefined line inside qualifying.push (dead code, always evaluated to undefined). - cos/taskComplete.ts: fix stale header comment — this handler is now triggered by the in-process scheduler + plannerPoller, not by a Power Automate email. - cos/escalate.ts: same header-comment fix — triggered by CRON_ESCALATE in scheduler.ts, not by Power Automate email. - README §1: clarify that runEscalate is also LLM-driven (draft re-plan proposals for the leader), alongside capture extraction + recall. Deferred (would change functionality — kept intentionally): - scheduler.ts::cacheConversationReference — first-inbound-user caching is by design; scoping to LEADER_UPN only would break proactive sends triggered from owner conversations. - meetingWatcher.ts organizer-scope — leader-as-attendee (not organizer) is intentional so delegates/EAs can schedule meetings for the leader and still get captured (existing code comment documents this).
8e27030 to
0b08498
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (2)
scenarios/chief-of-staff/src/graph/meetingWatcher.ts:10
- The header comment says qualifying meetings are “organized by the leader” and “already ended”, but the implementation qualifies any meeting where both the leader and CoS are on the invite (regardless of organizer) and explicitly allows in-progress/upcoming meetings. This is also a mismatch with the PR description (“leader-organised meeting”). Either tighten the filter to organizer==leader and ended-only, or update the docs/README/PR description to match the intended behavior.
// env), keep only events that are:
// - Teams online meetings (isOnlineMeeting = true)
// - organized by the leader (organizer.emailAddress.address == LEADER_UPN)
// - CoS agent is an invited attendee
// - already ended
scenarios/chief-of-staff/src/graph/meetingWatcher.ts:73
- This comment says the calendarView results are filtered to “organizer == leader AND CoS is invited”, but the code below actually checks only that both leader and CoS are participants (organizer OR attendee). Please update the comment so future changes don’t accidentally reintroduce the organizer-only constraint (or update the code if organizer-only is intended).
// calendarView is the correct endpoint for expanded recurring events.
// We read the CALENDAR of graphOwnerUpn (leader or cos-agent, per env)
// and FILTER events where organizer == leader AND CoS is invited.
- logger.ts::safe() — replace `if (!meta)` with a nullish check so legitimate diagnostic values (0, false, '') are no longer silently dropped from logs. - scheduler.ts::runOnCachedRef — validate botAppId before calling `adapter.continueConversation`. Without the check an unset `agent_id`/`clientId` would surface as a cryptic MSAL/OBO error deep inside the SDK; now we log a clear `skipped` line and return. - agent.ts::handleUserMessage — same botAppId validation on the fast-ack card-submit path. If botAppId is missing we now fall back to the synchronous path (with a specific error message) instead of losing the card submit into a background failure. Deferred (would change functional/state-machine behavior — user asked for safe fixes only): - transcriptPoller.ts::advanceCapture — Copilot correctly flagged that `markCaptureComplete` fires before the scheduler runs runCapture(), so a transient failure permanently dedupes the capture. Fixing this cleanly requires moving the mark-complete transition into the scheduler (across a module boundary) and reasoning about attempt counters + retry semantics. Left for a follow-up PR.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (2)
scenarios/chief-of-staff/src/scheduler.ts:228
- The Planner completion poller interval can overlap with itself if fireInAuthedContext/pollForCompletedTasks/runTaskComplete takes longer than POLL_TASKS_MS, because setInterval doesn't await. That overlap can cause duplicate detections or extra Graph load. The meeting poll has an in-flight guard; Planner poll should too.
// ── Poll: completed Planner tasks → runTaskComplete ──
intervalIds.push(
setInterval(async () => {
await fireInAuthedContext(deps, 'planner-poll', async (ctx, state, client) => {
const done = await pollForCompletedTasks({
scenarios/chief-of-staff/src/graph/meetingWatcher.ts:10
- Header comment says meetings must be "organized by the leader" and "already ended", but the implementation qualifies meetings when both leader+CoS are on the invite and can include in-progress/upcoming meetings. This mismatch will confuse maintainers and reviewers.
// env), keep only events that are:
// - Teams online meetings (isOnlineMeeting = true)
// - organized by the leader (organizer.emailAddress.address == LEADER_UPN)
// - CoS agent is an invited attendee
// - already ended
Additive-only improvements ported from patterns in the Chief-of-Staff sample (PR microsoft#333). Zero behaviour change to existing handlers. * src/util/logger.ts (new) — level-based logger with LOG_LEVEL, timestamped scope tags, and automatic secret redaction. Available for future use; existing console.log calls left in place. * src/util/httpLogger.ts (new) — global axios interceptor for outbound Jira / Graph / MCP tracing. Off by default, gated behind LOG_HTTP=true. * src/startup-check.ts (new) — one-shot boot banner printing every relevant env var with [MISSING] markers so misconfig surfaces before the first handler runs. * src/index.ts — wire installHttpLogging + printStartupBanner immediately after configDotenv(); add unhandledRejection and uncaughtException handlers so a stray connector 502 no longer tears down the scheduler. * docs/design.md — replace stub with full design doc: 12 numbered sections including per-flow mermaid sequence diagrams, determinism boundary, concurrency guarantees, and extension points. * README.md — trim architecture / reconcile rules / warn thresholds / calendar path (moved to design.md); add TOC, First live proof smoke test, Troubleshooting matrix, and Deploy to Azure sections. * .env.template — document LOG_LEVEL and LOG_HTTP toggles.
…e strips fields pendingCaptureStore.markCaptureComplete() sets transcriptContent, insightsActionItems and insightsMeetingNotes to undefined for memory savings. Because store.get(eventId) returns the same object reference as the local `cap` variable, that mutation happens in place - so building the return object AFTER mark-complete lands undefined values. Capture consumers then see an empty transcript and the LLM hallucinates a fetch error. Reorder the READY branch: build `result` from `cap` first, then call markCaptureComplete(cap.eventId), then `return result`. Adds a short comment above the snapshot explaining the aliasing so this does not regress. No behavioural change on the retry / give-up branches; only affects the success path in advanceCapture().
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (5)
scenarios/chief-of-staff/src/scheduler.ts:247
- Unlike the meeting poller, the Planner completion poller doesn’t guard against overlapping ticks. If Graph is slow and a tick runs longer than POLL_TASKS_MS, concurrent polls can double-detect the same transition and fire runTaskComplete twice (duplicate DMs). Add an in-flight guard similar to meetingPollInFlight.
// ── Poll: completed Planner tasks → runTaskComplete ──
intervalIds.push(
setInterval(async () => {
await fireInAuthedContext(deps, 'planner-poll', async (ctx, state, client) => {
scenarios/chief-of-staff/src/startup-check.ts:202
- The startup banner says missing GRAPH_APP_* will fall back to agentic OBO, but several flows/tools in this sample call acquireAppOnlyGraphToken() and will throw if the standalone worker env vars aren’t set. The banner should reflect that these flows require the worker (or explicitly document which calls actually fall back).
scenarios/chief-of-staff/compare_grants.ps1:1 - This PowerShell script is missing the standard Microsoft copyright header used by other repo .ps1 scripts (e.g., scripts/e2e/Start-Agent.ps1). Please add it at the top of the file.
# ──────────────────────────────────────────────────────────────────────────
# Compare delegated permission grants and app-role assignments between two
# Agent 365 agent instance service principals — useful when diagnosing
# "why does agent A see tool X but agent B doesn't?".
scenarios/chief-of-staff/scripts/bootstrap-graph-app.ps1:1
- This PowerShell script is missing the standard Microsoft copyright header used by other repo .ps1 scripts (e.g., scripts/e2e/Start-Agent.ps1). Please add it at the top of the file.
# bootstrap-graph-app.ps1
#
# One-shot provisioning of the standalone Graph worker app for cos-agent.
scenarios/chief-of-staff/DESIGN.md:59
- DESIGN.md says state is intentionally in-memory (Map only), but the implementation in this sample uses file-backed persistence (PersistentMap, STATE_DIR) for restart safety. Update the design principles text so it matches the actual behavior and guidance.
5. **State is intentionally in-memory for the MVP.** All three stores
(`followupStore`, `pendingCaptureStore`, `conversationRefs`) are
`Map` instances. Fine for a demo; swap for persistent storage before
production (see §13).
…lates Every turn now refreshes the observability token cache for BOTH the blueprint and agentic-instance identities. The exporter partitions span groups by (agentId, tenantId); groups without a cached token are silently skipped, which left the M365 admin center Activity tab empty even though batch-level agent365-export succeeded events fired. Also installs the ObservabilityHostingManager middleware on the shared CloudAdapter so BaggageMiddleware + OutputLoggingMiddleware run per turn, and adds a botAppId null-check in the handleUserMessage card-submit async continuation. DESIGN.md 11.2 expanded to describe the three-layer wiring (client.ts tracer, index.ts middleware install, agent.ts per-turn refresh) plus the silent-skip gotcha and log-evidence markers for verification.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.
Suppressed comments (6)
scenarios/chief-of-staff/.env.template:81
- This template claims GRAPH_APP_* can be left blank to fall back to agentic OBO, but the current implementation uses the standalone Graph worker for Planner/Graph calls and will fail without it. The template should match the actual requirement.
# ─── Standalone Graph worker app (recommended) ───
# When all three are set, EVERY Graph API call uses this app's client
# credentials (application permissions) instead of the agentic OBO chain.
# Simpler consent, cleaner reproduction. See README §2b.
# Leave blank to fall back to the agentic OBO exchange.
scenarios/chief-of-staff/src/graph/meetingWatcher.ts:47
- MeetingWatcherOptions comments say leaderUpn must be the organizer and cosAgentUpn must be an attendee, but discoverQualifyingMeetings currently checks for either being organizer OR attendee. Update these field docs to avoid misconfiguring callers.
/** UPN who must be the event organizer for it to qualify. */
leaderUpn: string;
/** UPN who must be in event attendees for it to qualify (the CoS agent). */
cosAgentUpn: string;
scenarios/chief-of-staff/src/startup-check.ts:203
- The startup banner says missing GRAPH_APP_* will fall back to agentic OBO, but several Graph paths in this sample (e.g., Planner tools) unconditionally use the standalone Graph worker. This message is misleading and can cause confusing runtime failures when scheduler/pollers start.
scenarios/chief-of-staff/README.md:35 - README says Capture triggers only for leader-organised meetings, but the current meeting watcher qualifies any meeting where both the leader and CoS are on the invite (even if a delegate organized it). Please update this line (or change the implementation) so docs and behavior match.
| **Capture** — extracts action items + decisions from Teams meetings and creates Planner tasks | Calendar poller finds a leader-organised meeting the CoS was invited to, transcript is fetched, LLM extracts |
scenarios/chief-of-staff/src/graph/plannerPoller.ts:52
- The function comment says the first poll after boot only seeds the baseline, but when a baseline is hydrated from disk (firstPollDone=true) the first poll will emit transitions. Update the doc comment to reflect the actual behavior.
/**
* Poll Planner for tasks that just became 100% complete. Empty on failure.
* First poll after boot only seeds the baseline — no completions reported
* (otherwise every already-done task would fire on first tick).
*/
scenarios/chief-of-staff/src/graph/meetingWatcher.ts:10
- The header comment describes filtering to leader-organized and already-ended meetings, but the implementation qualifies meetings as long as both leader+CoS are on the invite and allows upcoming/in-progress meetings. Please align the comment (and associated docs) with the actual behavior.
// env), keep only events that are:
// - Teams online meetings (isOnlineMeeting = true)
// - organized by the leader (organizer.emailAddress.address == LEADER_UPN)
// - CoS agent is an invited attendee
// - already ended
…AC Activity
Extract A365 observability wiring into src/observability.ts as a single source of truth for identity resolution, token cache warming, and OTEL baggage. Runtime agent identity now comes from activity.recipient.agenticAppId (the agentic instance) rather than the blueprint id; blueprint kept as span metadata only. Adds runWithObservabilityContext() so proactive / scheduled turns (card-submit continuation, invoke continuation, scheduler crons) carry the same tenant + agent + caller baggage that inbound /api/messages turns get from the hosting middleware. buildUserDetails is now async and falls back to a Graph /users/{aad} lookup (cached via graph/peopleTools.ts) so user.email carries the real UPN on Teams-channel activities, which don't populate from.agenticUserId. New ClientOptions.humanInitiated flag lets scheduler-created clients suppress caller attribution so an autonomous cron doesn't report the last inbound user. .env.template adds agent365Observability__agentInstanceId with clear comments distinguishing runtime vs blueprint id. DESIGN.md 11.2 fully rewritten to describe the four-layer wiring plus the three gotchas (blueprint-vs-instance, Teams-no-UPN, proactive-bypasses-baggage) and the tenant policy that hashes UPNs even when we emit them correctly. README troubleshooting adds two rows for empty MAC Activity tab and hashed UPN column.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.
Suppressed comments (8)
scenarios/chief-of-staff/.env.template:84
- .env.template says leaving GRAPH_APP_* blank will fall back to the agentic OBO exchange for "EVERY Graph API call", but many code paths (Planner/Brief/Follow-up, plannerTools helpers, plannerConfig resolution) call acquireAppOnlyGraphToken() directly and will fail if the worker app isn't configured. Either implement a real fallback everywhere or update the template to set the expectation correctly.
# ─── Standalone Graph worker app (recommended) ───
# When all three are set, EVERY Graph API call uses this app's client
# credentials (application permissions) instead of the agentic OBO chain.
# Simpler consent, cleaner reproduction. See README §2b.
# Leave blank to fall back to the agentic OBO exchange.
GRAPH_APP_ID=
GRAPH_APP_SECRET=
GRAPH_TENANT_ID=
scenarios/chief-of-staff/src/cards/actionRouter.ts:123
- proposeMeetingSlots() claims to propose times in DISPLAY_TZ, but the ISO timestamps are only correct for Asia/Kolkata. For any other IANA zone (including the BRIEF_DISPLAY_TZ examples in .env.template), this schedules meetings at the wrong wall-clock time because it treats the requested local hour as UTC.
for (const hour of HOURS) {
if (slots.length >= 3) break;
// IST is UTC+5:30 → subtract 330 min to get equivalent UTC instant.
// For non-IST TZs, use the local time as UTC (acceptable demo drift).
const offsetMin = DISPLAY_TZ === 'Asia/Kolkata' ? 330 : 0;
scenarios/chief-of-staff/src/cards/actionRouter.ts:629
- The owner DM in book_meeting hard-codes the leader name as "Alex", which will be incorrect for other deployments and can confuse recipients.
const ownerMsg =
`📅 **Meeting scheduled to unblock: ${taskTitle}**\n\n` +
`Time: ${timeslot}\n` +
`The leader (Alex) picked this slot. Calendar invite coming next.` +
(timeslotIso ? `\n\n_ISO start: ${timeslotIso}_` : '');
scenarios/chief-of-staff/src/graph/meetingWatcher.ts:10
- The file header comment says meetings are filtered to "organized by the leader" and "already ended", but the implementation qualifies meetings whenever both leader+CoS are on the invite (regardless of organizer) and allows in-progress/upcoming meetings (retry loop handles readiness). This mismatch makes it harder to understand and maintain the capture logic.
// env), keep only events that are:
// - Teams online meetings (isOnlineMeeting = true)
// - organized by the leader (organizer.emailAddress.address == LEADER_UPN)
// - CoS agent is an invited attendee
// - already ended
scenarios/chief-of-staff/src/graph/plannerPoller.ts:52
- The pollForCompletedTasks() docstring says the first poll "only seeds the baseline" and reports no completions, but the code explicitly treats a hydrated baseline (lastProgress.size > 0) as a normal comparison where completions during downtime will fire on the next poll. The comment should reflect that conditional behavior.
/**
* Poll Planner for tasks that just became 100% complete. Empty on failure.
* First poll after boot only seeds the baseline — no completions reported
* (otherwise every already-done task would fire on first tick).
*/
scenarios/chief-of-staff/src/index.ts:65
- Keeping the process alive after an uncaughtException is unsafe in Node.js because the runtime may be in an undefined state (potentially corrupting in-memory stores and scheduled work). Prefer logging and exiting so the process supervisor can restart cleanly.
process.on('uncaughtException', (err) => {
console.error('[process] uncaughtException — keeping process alive.', {
message: err?.message,
stack: err?.stack,
});
});
scenarios/chief-of-staff/README.md:35
- The README describes Capture as triggering only for "leader-organised" meetings, but meetingWatcher.ts qualifies meetings when both leader+CoS are on the invite (regardless of organizer). Either the code or the docs should be updated so setup/expectations match actual behavior.
| Capability | Trigger |
|---|---|
| **Capture** — extracts action items + decisions from Teams meetings and creates Planner tasks | Calendar poller finds a leader-organised meeting the CoS was invited to, transcript is fetched, LLM extracts |
| **Daily Brief** — Adaptive Card DM to the leader with priorities, watch items, upcoming meetings | Cron (default 8 AM weekdays, gated behind `BRIEF_ENABLED`) |
scenarios/chief-of-staff/DESIGN.md:55
- Design principle #5 claims state is "intentionally in-memory" and stores are Map instances, but the implementation uses PersistentMap with a file-backed STATE_DIR and hydration/flush logic. This inconsistency can mislead readers about restart behavior and deployment requirements.
4. **One path to Graph.** Every Graph call uses the standalone
cos-graph-worker application-permission token. No delegated /
agentic-OBO Graph path exists — blueprint and agent-instance
identities get no extra Graph scopes. See §7.
5. **State is intentionally in-memory for the MVP.** All three stores
(`followupStore`, `pendingCaptureStore`, `conversationRefs`) are
`Map` instances. Fine for a demo; swap for persistent storage before
production (see §13).
Summary
Adds a new sample under
scenarios/chief-of-staff/— an autonomous MicrosoftAgent 365 teammate that runs a leader's operating rhythm. The agent lives in
Microsoft Teams, watches the leader's calendar for meetings it's been invited
to, captures action items and decisions from the transcript into Planner,
sends a daily Brief card, follows up with owners, books unblock meetings,
escalates non-responsive owners, and lets people close tasks by chat.
Built on
@microsoft/agents-hosting+@openai/agents+ Azure OpenAI gpt-4o.What the sample demonstrates
BRIEF_ENABLED)On track/Need more time/I'm blocked)extendblockedpercentComplete: 100planner_list_tasks+mcp_CalendarToolsEverything is deterministic except the two flows that explicitly need
natural-language understanding (capture extraction + recall/chit-chat). All
routing, dedup, date math, and Planner writes are TypeScript.
Design highlights
cos-graph-workerapp for every Graph call. SeeDESIGN.md §7.meeting-poll orchestrator uses an in-flight guard against overlapping ticks.
PersistentMap<V>survives restarts;in-flight captures and follow-ups don't need to be re-triggered.
scripts/bootstrap-graph-app.ps1creates theworker app, grants all 9 Graph app-role permissions, admin-consents them
tenant-wide, and rotates a client secret in one command.
Files
scenarios/chief-of-staff/src/agent.ts,src/scheduler.ts,src/cards/,src/cos/,src/graph/,src/state/,src/util/README.md(end-to-end setup) +DESIGN.md(architecture, per-flow sequence diagrams, complete env-var reference)scripts/bootstrap-graph-app.ps1(Graph worker provisioning) +compare_grants.ps1(diagnostic).env/.env.bak/chat.json/log.txt/a365.*.config.json/.cos-state//manifest//dist//node_modules/— all covered by.gitignore(43 patterns)Testing performed
npm run build+npm run devboot cleanly on Node 20 (Windows + Linux)README.md §8tested against a fresh M365 tenant with Copilot license
README.md §8.8that already-processedmeetings are not re-captured after a restart (state-file dedup)
tsc --noEmit— passes cleanly across all 30 TS fileslicenses (MIT / Apache-2.0 / ISC / BSD / BlueOak / 0BSD); the only
non-permissive is
@microsoft/m365agentsplayground(Microsoft Pre-ReleaseLicense), which is a devDep and identical to what the existing
nodejs/openai/sample-agentusesReviewer notes
Folder location — I placed this at
scenarios/chief-of-staff/at repo root.The existing convention in this repo is
nodejs/openai/<sample>/,dotnet/w365-computer-use/<sample>/, etc. Happy to move this undernodejs/openai/chief-of-staff/(or wherever fits best) if the currentlocation doesn't align with your intent for a "scenarios" umbrella. Let me
know and I'll
git mv+ amend + force-push.Repo boilerplate — README ends with the standard Support / Contributing
/ Additional Resources / Trademarks / License sections, matching the
nodejs/openai/sample-agenttemplate.SECURITY.mdandLICENSE.mdlinks point at the repo-root files via
../../from this sample folder.Checklist
.env.template,README.md,DESIGN.md,package.json,tsconfig.json,ToolingManifest.json,scripts/,src/).gitignorepresent and comprehensive(Graph SP, Agent 365 Tools resource, Agent 365 Runtime scope) or
<PLACEHOLDER>valuestsc --noEmit§8includes step-by-step verification for every capabilitybootstrap-graph-app.ps1,compare_grants.ps1)use
<PLACEHOLDER>values, not real IDs