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

Commit 8a15fe87

8a15fe87b720Merged

aryaamin <[email protected]> · 2 months ago · 51 files · +4 added · ~47 modified

Description

Add guided first interaction onboarding

Details

Commit
8a15fe87b720
Parent
ce192002f08a

apps/api/src/app.tsmodifiedView file

@@ -24,11 +24,17 @@
2424 import { toA2AAgentCard } from '../../../packages/sidecar/src/index.js';
2525 import {
2626 approveAgentSetup,
27+ createDashboardAgent,
2728 createHostedProviderAgent,
2829 getOnboardingState,
2930 rejectAgentSetup,
3031 } from '../../../packages/persistence/src/onboarding.js';
3132 import { FixedWindowRateLimiter } from './security.js';
33+import {
34+ buildOwnerCompletionReport,
35+ buildOwnerFeedback,
36+ buildQuickstartInteraction,
37+} from './quickstart.js';
3238
3339 type DashboardRepository = Pick<
3440 HostedRepository,
@@ -630,6 +636,31 @@
630636 throw error;
631637 }
632638 });
639+ router.post('/v0.1/quickstart/agent', async (request) => {
640+ const owner = operatorId(request);
641+ if (!repository || !owner) throw new Error('Hosted persistence is not configured');
642+ const value = z
643+ .object({
644+ agentName: z.string().trim().min(1).max(100),
645+ projectName: z.string().trim().min(1).max(100).default('My agents'),
646+ description: z.string().trim().min(1).max(500),
647+ capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(20),
648+ limitations: z.array(z.string().trim().min(1).max(300)).max(20).default([]),
649+ })
650+ .strict()
651+ .parse(request.body);
652+ const created = await createDashboardAgent(repository, owner, value);
653+ const card = await repository.publishAgent(
654+ owner,
655+ buildPublicAgentCard(created.agent, publicBaseUrl(request)),
656+ );
657+ await repository.upsert(owner, 'publication', created.agent.agentId, {
658+ agentId: created.agent.agentId,
659+ published: true,
660+ updatedAt: card.updatedAt,
661+ });
662+ return { ...created, card };
663+ });
633664 router.post('/v0.1/agents', async (request) => {
634665 const owner = operatorId(request);
635666 const current = await scopedEngine(request);
@@ -836,6 +867,49 @@
836867 throw new Error('Federated interactions are not configured');
837868 return repository.listFederatedInteractions(owner);
838869 });
870+ router.post('/v0.1/federated-interactions/start', async (request) => {
871+ const owner = operatorId(request);
872+ if (!repository?.createFederatedInteraction || !owner)
873+ throw new Error('Federated interactions are not configured');
874+ const value = z
875+ .object({
876+ initiatorAgentId: z.string().min(1),
877+ responderAgentId: z.string().min(1),
878+ task: z.string().trim().min(10).max(2000),
879+ requestedOutcome: z.string().trim().min(3).max(1000),
880+ successCriterion: z.string().trim().min(3).max(1000),
881+ taskCategory: z.string().trim().min(1).max(100).optional(),
882+ deadline: z.string().datetime().optional(),
883+ })
884+ .strict()
885+ .refine((input) => !input.deadline || Date.parse(input.deadline) > Date.now(), {
886+ message: 'Deadline must be in the future',
887+ path: ['deadline'],
888+ })
889+ .parse(request.body);
890+ const [initiator, responder] = await Promise.all([
891+ repository.getPublishedAgent(value.initiatorAgentId),
892+ repository.getPublishedAgent(value.responderAgentId),
893+ ]);
894+ if (!initiator) throw new Error('Publish the initiating agent before starting an agreement');
895+ if (!responder) throw new Error('The selected counterparty is no longer available');
896+ if (repository.listFederatedInteractions) {
897+ const existing = (await repository.listFederatedInteractions(owner)).find(
898+ (candidate) =>
899+ ['pending', 'active'].includes(candidate.status) &&
900+ candidate.initiatorAgentId === value.initiatorAgentId &&
901+ candidate.responderAgentId === value.responderAgentId &&
902+ candidate.contract.purpose === value.task.slice(0, 500) &&
903+ candidate.contract.requestedOutcome === value.requestedOutcome &&
904+ candidate.contract.successCriteria[0] === value.successCriterion,
905+ );
906+ if (existing) return existing;
907+ }
908+ return repository.createFederatedInteraction(
909+ owner,
910+ buildQuickstartInteraction(initiator, responder, value),
911+ );
912+ });
839913 router.get('/v0.1/federated-interactions/:id', async (request) => {
840914 const owner = operatorId(request);
841915 if (!repository?.getFederatedInteraction || !owner)
@@ -867,6 +941,46 @@
867941 value.agentId,
868942 );
869943 });
944+ router.post('/v0.1/federated-interactions/:id/complete', async (request) => {
945+ const owner = operatorId(request);
946+ if (!repository?.getFederatedInteraction || !repository.submitCompletionReport || !owner)
947+ throw new Error('Interaction completion is not configured');
948+ const interactionId = (request.params as { id: string }).id;
949+ const value = z
950+ .object({
951+ agentId: z.string().min(1),
952+ outcome: z.enum(['success', 'partial', 'failure', 'cancelled']),
953+ summary: z.string().trim().min(3).max(2000),
954+ evidenceReferences: z.array(z.string().url().max(2048)).max(20).default([]),
955+ })
956+ .strict()
957+ .parse(request.body);
958+ const [interaction, rows] = await Promise.all([
959+ repository.getFederatedInteraction(owner, interactionId),
960+ repository.list(owner),
961+ ]);
962+ if (!interaction) throw new Error('Interaction not found');
963+ if (!['active', 'completed'].includes(interaction.status))
964+ throw new Error('The agreement must be active before reporting its outcome');
965+ const duplicate = rows.find(
966+ (row) =>
967+ row.kind === 'completion_report' &&
968+ row.payload?.interactionId === interactionId &&
969+ row.payload?.reportingAgentId === value.agentId,
970+ );
971+ if (duplicate) return { report: duplicate.payload, deduplicated: true };
972+ const agent = rows.find(
973+ (row) => row.kind === 'agent_profile' && row.recordId === value.agentId,
974+ )?.payload as AgentProfile | undefined;
975+ if (!agent || agent.status !== 'active') throw new Error('Active owned agent not found');
976+ const session = repository.getLiveSession
977+ ? await repository.getLiveSession(owner, interactionId, value.agentId)
978+ : undefined;
979+ const report = buildOwnerCompletionReport(interaction, agent, value, {
980+ ...(session?.activatedAt ? { startedAt: session.activatedAt } : {}),
981+ });
982+ return repository.submitCompletionReport(owner, value.agentId, report, 'oauth_account');
983+ });
870984 router.post('/v0.1/federated-interactions/:id/completion-reports', async (request) => {
871985 const owner = operatorId(request);
872986 if (!repository?.submitCompletionReport || !owner)
@@ -899,6 +1013,42 @@
8991013 : 'oauth_installation',
9001014 );
9011015 });
1016+ router.post('/v0.1/feedback-requests/:id/respond', async (request) => {
1017+ const owner = operatorId(request);
1018+ if (!repository?.listFeedbackRequests || !repository.submitInteractionFeedback || !owner)
1019+ throw new Error('Interaction feedback is not configured');
1020+ const value = z
1021+ .object({
1022+ agentId: z.string().min(1),
1023+ rating: z.number().int().min(1).max(5),
1024+ wouldWorkAgain: z.enum(['yes', 'no', 'unsure']),
1025+ privateComment: z.string().trim().max(1000).optional(),
1026+ })
1027+ .strict()
1028+ .parse(request.body);
1029+ const [requests, rows] = await Promise.all([
1030+ repository.listFeedbackRequests(owner, value.agentId),
1031+ repository.list(owner),
1032+ ]);
1033+ const duplicate = rows.find(
1034+ (row) =>
1035+ row.kind === 'interaction_feedback' &&
1036+ row.payload?.requestId === (request.params as { id: string }).id &&
1037+ row.payload?.reviewerAgentId === value.agentId,
1038+ );
1039+ if (duplicate) return { feedback: duplicate.payload, deduplicated: true };
1040+ const feedbackRequest = requests.find(
1041+ (candidate) => candidate.requestId === (request.params as { id: string }).id,
1042+ );
1043+ if (!feedbackRequest || feedbackRequest.status !== 'pending')
1044+ throw new Error('Pending feedback request not found');
1045+ const agent = rows.find(
1046+ (row) => row.kind === 'agent_profile' && row.recordId === value.agentId,
1047+ )?.payload as AgentProfile | undefined;
1048+ if (!agent || agent.status !== 'active') throw new Error('Active owned agent not found');
1049+ const feedback = buildOwnerFeedback(feedbackRequest, agent, value);
1050+ return repository.submitInteractionFeedback(owner, value.agentId, feedback, 'oauth_account');
1051+ });
9021052 router.get('/v0.1/feedback-requests', async (request) => {
9031053 const owner = operatorId(request);
9041054 if (!repository?.listFeedbackRequests || !owner)