The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 8a15fe87b720Commit detailsView latest

apps/api/src/quickstart.ts 178 lines · 6.6 KB · typescript
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
}