feat(swads): add daily image report workflow
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user