| 1 | import { randomUUID } from 'node:crypto';
|
| 2 | import {
|
| 3 | FederatedInteractionSchema,
|
| 4 | InteractionCompletionReportSchema,
|
| 5 | InteractionFeedbackSchema,
|
| 6 | canonicalHash,
|
| 7 | type FeedbackRequest,
|
| 8 | type FederatedInteraction,
|
| 9 | type InteractionCompletionReport,
|
| 10 | type InteractionFeedback,
|
| 11 | type PublicAgentCard,
|
| 12 | } from '../../../packages/protocol/src/index.js';
|
| 13 | import type { AgentProfile } from '../../../packages/persistence/src/onboarding.js';
|
| 14 |
|
| 15 | export type QuickstartAgreementInput = {
|
| 16 | task: string;
|
| 17 | requestedOutcome: string;
|
| 18 | successCriterion: string;
|
| 19 | taskCategory?: string | undefined;
|
| 20 | deadline?: string | undefined;
|
| 21 | };
|
| 22 |
|
| 23 | export function buildQuickstartInteraction(
|
| 24 | initiator: PublicAgentCard,
|
| 25 | responder: PublicAgentCard,
|
| 26 | input: QuickstartAgreementInput,
|
| 27 | options: { interactionId?: string; now?: Date } = {},
|
| 28 | ): FederatedInteraction {
|
| 29 | if (initiator.agentId === responder.agentId)
|
| 30 | throw new Error('Choose a different agent as the counterparty');
|
| 31 | if (initiator.agentMode === 'temporary_chat' && responder.agentMode === 'temporary_chat')
|
| 32 | throw new Error('A hosted temporary agent needs a connected persistent counterparty');
|
| 33 | const responderTransport = responder.transports[0];
|
| 34 | if (!responderTransport) throw new Error('The counterparty does not have a usable A2A runtime');
|
| 35 | const now = options.now ?? new Date();
|
| 36 | const interactionId = options.interactionId ?? randomUUID();
|
| 37 | const task = input.task.trim();
|
| 38 | const requestedOutcome = input.requestedOutcome.trim();
|
| 39 | const successCriterion = input.successCriterion.trim();
|
| 40 | const taskCategory = input.taskCategory?.trim() || responder.capabilities[0] || 'general';
|
| 41 | const contract = {
|
| 42 | protocolVersion: '0.1' as const,
|
| 43 | interactionId,
|
| 44 | purpose: task.slice(0, 500),
|
| 45 | parties: [initiator.agentId, responder.agentId],
|
| 46 | taskCategory,
|
| 47 | requestedOutcome,
|
| 48 | successCriteria: [successCriterion],
|
| 49 | allowedActions: ['Communicate about the agreed task', 'Produce the requested outcome'],
|
| 50 | prohibitedActions: ['Act outside the agreed task without renewed approval'],
|
| 51 | allowedData: ['Information explicitly shared for this interaction'],
|
| 52 | prohibitedData: ['credentials', 'government identifiers', 'payment card data'],
|
| 53 | ...(input.deadline ? { deadline: input.deadline } : {}),
|
| 54 | evidenceRequirements: ['Reference inspectable evidence for material factual claims'],
|
| 55 | delegationRules: ['explicit_contract_scope'],
|
| 56 | humanApprovalRequirements: [],
|
| 57 | factCheckingPolicy: 'important_claims',
|
| 58 | mediationPolicy: 'mutual_consent' as const,
|
| 59 | retentionDays: 30,
|
| 60 | completionConditions: [successCriterion],
|
| 61 | cancellationConditions: ['either_party_before_completion'],
|
| 62 | signatures: {},
|
| 63 | };
|
| 64 | const termsHash = canonicalHash(contract);
|
| 65 | const createdAt = now.toISOString();
|
| 66 | return FederatedInteractionSchema.parse({
|
| 67 | protocolVersion: '0.1',
|
| 68 | interactionId,
|
| 69 | initiatorAgentId: initiator.agentId,
|
| 70 | responderAgentId: responder.agentId,
|
| 71 | status: 'pending',
|
| 72 | contract,
|
| 73 | termsHash,
|
| 74 | acceptances: {
|
| 75 | [initiator.agentId]: {
|
| 76 | agentId: initiator.agentId,
|
| 77 | method: 'oauth_account',
|
| 78 | termsHash,
|
| 79 | acceptedAt: createdAt,
|
| 80 | },
|
| 81 | },
|
| 82 | contractRevision: 1,
|
| 83 | contractRevisions: [],
|
| 84 | ...(initiator.transports[0] ? { initiatorTransport: initiator.transports[0] } : {}),
|
| 85 | responderTransport,
|
| 86 | createdAt,
|
| 87 | updatedAt: createdAt,
|
| 88 | expiresAt: new Date(now.getTime() + 72 * 60 * 60_000).toISOString(),
|
| 89 | });
|
| 90 | }
|
| 91 |
|
| 92 | export function buildOwnerCompletionReport(
|
| 93 | interaction: FederatedInteraction,
|
| 94 | agent: AgentProfile,
|
| 95 | input: {
|
| 96 | outcome: InteractionCompletionReport['outcome'];
|
| 97 | summary: string;
|
| 98 | evidenceReferences?: string[] | undefined;
|
| 99 | },
|
| 100 | options: { reportId?: string; now?: Date; startedAt?: string } = {},
|
| 101 | ): InteractionCompletionReport {
|
| 102 | const counterpartyAgentId =
|
| 103 | interaction.initiatorAgentId === agent.agentId
|
| 104 | ? interaction.responderAgentId
|
| 105 | : interaction.initiatorAgentId;
|
| 106 | if (!interaction.contract.parties.includes(agent.agentId))
|
| 107 | throw new Error('The selected agent is not part of this interaction');
|
| 108 | const status =
|
| 109 | input.outcome === 'success'
|
| 110 | ? 'met'
|
| 111 | : input.outcome === 'partial'
|
| 112 | ? 'partially_met'
|
| 113 | : input.outcome === 'failure'
|
| 114 | ? 'missed'
|
| 115 | : 'unknown';
|
| 116 | const completedAt = (options.now ?? new Date()).toISOString();
|
| 117 | return InteractionCompletionReportSchema.parse({
|
| 118 | reportId: options.reportId ?? randomUUID(),
|
| 119 | interactionId: interaction.interactionId,
|
| 120 | contractHash: interaction.termsHash,
|
| 121 | reportingAgentId: agent.agentId,
|
| 122 | counterpartyAgentId,
|
| 123 | agentVersion: agent.agentVersion,
|
| 124 | outcome: input.outcome,
|
| 125 | summary: input.summary.trim(),
|
| 126 | requestedOutcome: interaction.contract.requestedOutcome,
|
| 127 | criteria: interaction.contract.successCriteria.map((criterion) => ({
|
| 128 | criterion,
|
| 129 | status,
|
| 130 | explanation: input.summary.trim(),
|
| 131 | evidenceReferences: input.evidenceReferences ?? [],
|
| 132 | })),
|
| 133 | deliverables: [],
|
| 134 | actionsTaken: [],
|
| 135 | blockers: input.outcome === 'failure' ? [input.summary.trim()] : [],
|
| 136 | scopeChanges: [],
|
| 137 | corrections: [],
|
| 138 | evidenceReferences: input.evidenceReferences ?? [],
|
| 139 | ...(options.startedAt ? { startedAt: options.startedAt } : {}),
|
| 140 | completedAt,
|
| 141 | confidence: 0.8,
|
| 142 | dataSharingMode: 'structured_only',
|
| 143 | submissionMethod: 'oauth_account',
|
| 144 | });
|
| 145 | }
|
| 146 |
|
| 147 | export function buildOwnerFeedback(
|
| 148 | request: FeedbackRequest,
|
| 149 | agent: AgentProfile,
|
| 150 | input: {
|
| 151 | rating: number;
|
| 152 | wouldWorkAgain: InteractionFeedback['wouldWorkAgain'];
|
| 153 | privateComment?: string | undefined;
|
| 154 | },
|
| 155 | options: { feedbackId?: string; now?: Date } = {},
|
| 156 | ): InteractionFeedback {
|
| 157 | if (request.reviewerAgentId !== agent.agentId)
|
| 158 | throw new Error('The selected agent cannot answer this feedback request');
|
| 159 | const normalizedRating = (input.rating - 1) / 4;
|
| 160 | return InteractionFeedbackSchema.parse({
|
| 161 | feedbackId: options.feedbackId ?? randomUUID(),
|
| 162 | requestId: request.requestId,
|
| 163 | interactionId: request.interactionId,
|
| 164 | reviewerAgentId: request.reviewerAgentId,
|
| 165 | subjectAgentId: request.subjectAgentId,
|
| 166 | reviewerAgentVersion: agent.agentVersion,
|
| 167 | ratings: Object.fromEntries(
|
| 168 | request.requestedDimensions.map((dimension) => [dimension, normalizedRating]),
|
| 169 | ),
|
| 170 | wouldWorkAgain: input.wouldWorkAgain,
|
| 171 | reasonCodes: input.wouldWorkAgain === 'no' ? ['would_not_work_again'] : [],
|
| 172 | ...(input.privateComment?.trim() ? { privateComment: input.privateComment.trim() } : {}),
|
| 173 | evidenceReferences: [],
|
| 174 | confidence: 0.8,
|
| 175 | submittedAt: (options.now ?? new Date()).toISOString(),
|
| 176 | submissionMethod: 'oauth_account',
|
| 177 | });
|
| 178 | }
|