feat(swads): add daily image report workflow

This commit is contained in:
Jeffrey Wu
2026-09-07 11:34:00 +08:00
parent f39f6ac881
commit 5c61371d7e
38 changed files with 2537 additions and 52 deletions
+18 -8
View File
@@ -14,13 +14,16 @@ import {
import type { ModelConnectionInput, SendMessageInput } from "@agent-studio/shared";
import { InMemoryApprovalBroker, type ApprovalBroker } from "./approval.js";
import type { FileAttachmentStore } from "./attachments.js";
import { HarnessError, redactValue, safeErrorMessage } from "./errors.js";
import { HarnessError, redactValue, redactServerSecret, safeErrorMessage } from "./errors.js";
import type { EventStore } from "./event-store.js";
import type { ModelConnectionStore } from "./model-store.js";
import { mapPiEvent } from "./pi-event-adapter.js";
import { SafeWorkspace } from "./safe-workspace.js";
import type { SkillCatalog } from "./skills.js";
import type { SkillIndexEntry } from "./skills.js";
import { SwadsGateway } from "./swads-gateway.js";
import { SwadsReportTools } from "./swads-tools.js";
import { ReportStore } from "./report-store.js";
import { createMockAdsMetricsTool, createSaveReportDraftTool } from "./tools.js";
export interface AgentSessionPort {
@@ -66,16 +69,19 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
private readonly catalog: SkillCatalog,
private readonly models: ModelConnectionStore,
private readonly approvals: ApprovalBroker,
private readonly gateway = new SwadsGateway(),
private readonly reports = new ReportStore(path.resolve(fixturePath, "../../workspace/reports"), path.resolve(fixturePath, "../../.agents/skills/swads-daily-report/assets/report.css")),
) {}
async create(input: AgentSessionFactoryInput): Promise<AgentSessionPort> {
const resolved = await this.models.createRuntime(input.sessionId);
const swads = new SwadsReportTools(this.gateway, this.reports, input.getRun);
const extension: InlineExtension = { name: "agent-studio-safety", hidden: true, factory: (pi) => {
const pendingSkills = new Map<string, SkillIndexEntry>();
pi.on("tool_call", async (event) => {
const run = input.getRun();
if (!run) return { block: true, reason: "No active Harness run" };
const known = new Set(["read", "grep", "find", "ls", "mock_ads_metrics", "save_report_draft"]);
const known = new Set(["read", "grep", "find", "ls", "mock_ads_metrics", "save_report_draft", "swads_cli", "render_swads_daily_report"]);
if (!known.has(event.toolName)) {
input.toolRequested(event.toolCallId, event.toolName, publicToolArguments(event.input, input.cwd));
return { block: true, reason: "Tool is not allowed by the Harness policy", terminate: true };
@@ -92,7 +98,7 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
const maybePath = (event.input as Record<string, unknown>).path;
if (typeof maybePath === "string") await input.workspace.assertReadPath(maybePath);
}
input.toolRequested(event.toolCallId, event.toolName, publicToolArguments(event.input, input.cwd, matchedSkill));
input.toolRequested(event.toolCallId, event.toolName, event.toolName === "render_swads_daily_report" ? { status: "requested" } : publicToolArguments(event.input, input.cwd, matchedSkill));
if (event.toolName === "save_report_draft") {
const decision = await this.approvals.request({ sessionId: input.sessionId, runId: run.runId, toolCallId: event.toolCallId, toolName: event.toolName, arguments: event.input, riskLevel: "medium" }, run.signal);
if (decision.status !== "approved") return { block: true, reason: `Human approval ${decision.status}` };
@@ -114,8 +120,8 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
agentDir: path.join(input.cwd, ".pi-agent"),
modelRuntime: resolved.runtime,
model: resolved.model,
tools: ["read", "grep", "find", "ls", "mock_ads_metrics", "save_report_draft"],
customTools: [createMockAdsMetricsTool(this.fixturePath), createSaveReportDraftTool(input.workspace)],
tools: ["read", "grep", "find", "ls", "mock_ads_metrics", "save_report_draft", "swads_cli", "render_swads_daily_report"],
customTools: [createMockAdsMetricsTool(this.fixturePath), createSaveReportDraftTool(input.workspace), ...swads.definitions()],
resourceLoader: loader,
sessionManager: SessionManager.inMemory(input.cwd),
settingsManager: SettingsManager.inMemory(),
@@ -197,6 +203,7 @@ export class InMemorySessionRegistry implements SessionRegistry {
if (!item) throw new HarnessError("ATTACHMENT_NOT_FOUND", "Attachment not found for this session", 404);
return item;
});
const expanded = await this.catalog.expandCommand(message.text);
const runId = `run_${randomUUID()}`;
const assistantMessageId = `msg_${randomUUID()}`;
const controller = new AbortController();
@@ -214,7 +221,10 @@ export class InMemorySessionRegistry implements SessionRegistry {
this.events.append(id, { type: "run.started", runId, payload: { modelId: agent.modelId } });
const images: ImageContent[] = await Promise.all(selected.map(async (item) => ({ type: "image" as const, mimeType: item.mediaType, data: await this.attachments.readBase64(id, item.attachmentId) })));
session.timeout = setTimeout(() => void this.cancel(id, "timeout"), this.runTimeoutMs);
void this.executeRun(session, message.text, images);
if (expanded.skill) {
for (const type of ["skill.requested", "skill.loaded"] as const) this.events.append(id, { type, runId, payload: { name: expanded.skill.name, filePath: expanded.skill.filePath, source: "command" } });
}
void this.executeRun(session, redactServerSecret(expanded.text), images);
return { runId };
}
@@ -226,8 +236,8 @@ export class InMemorySessionRegistry implements SessionRegistry {
cwd: session.cwd,
workspace: session.workspace,
getRun: () => session.currentRun ? { runId: session.currentRun.runId, signal: session.currentRun.controller.signal, assistantMessageId: session.currentRun.assistantMessageId } : undefined,
skillRequested: (skill, toolCallId) => { const run = session.currentRun; if (run) this.events.append(session.sessionId, { type: "skill.requested", runId: run.runId, payload: { name: skill.name, filePath: skill.filePath, toolCallId } }); },
skillLoaded: (skill, toolCallId) => { const run = session.currentRun; if (run) this.events.append(session.sessionId, { type: "skill.loaded", runId: run.runId, payload: { name: skill.name, filePath: skill.filePath, toolCallId } }); },
skillRequested: (skill, toolCallId) => { const run = session.currentRun; if (run) this.events.append(session.sessionId, { type: "skill.requested", runId: run.runId, payload: { name: skill.name, filePath: skill.filePath, toolCallId, source: "read" } }); },
skillLoaded: (skill, toolCallId) => { const run = session.currentRun; if (run) this.events.append(session.sessionId, { type: "skill.loaded", runId: run.runId, payload: { name: skill.name, filePath: skill.filePath, toolCallId, source: "read" } }); },
toolRequested: (toolCallId, toolName, argumentsValue) => { const run = session.currentRun; if (run && !session.requestedToolCalls.has(toolCallId)) { session.requestedToolCalls.add(toolCallId); this.events.append(session.sessionId, { type: "tool.requested", runId: run.runId, payload: { toolCallId, toolName, arguments: redactValue(argumentsValue) } }); } },
});
session.unsubscribe = session.agent.subscribe((event) => this.onPiEvent(session, event as AgentSessionEvent));