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

Viewing files at commit 95b0e11125d2Commit detailsView latest

packages/persistence/src/source-record.ts 187 lines · 5.0 KB · typescript
1
import { randomUUID } from 'node:crypto';
2
import {
3
  LearningScopeSchema,
4
  ProvenanceSchema,
5
  SourceRecordEnvelopeSchema,
6
  canonicalHash,
7
  type LearningScope,
8
  type RetentionClass,
9
  type SourceRecordEnvelope,
10
} from '../../protocol/src/index.js';
11
12
export type SourceRecordMetadata = Partial<
13
  Pick<
14
    SourceRecordEnvelope,
15
    | 'eventId'
16
    | 'schemaName'
17
    | 'schemaVersion'
18
    | 'entityRefs'
19
    | 'provenance'
20
    | 'visibility'
21
    | 'retentionClass'
22
    | 'learningScope'
23
    | 'reportedAt'
24
    | 'ingestedAt'
25
  >
26
>;
27
28
export type SourceRecordWriteMetadata = SourceRecordMetadata & { journal?: boolean };
29
30
export const SOURCE_JOURNAL_RECORD_KINDS = new Set([
31
  'agent',
32
  'agent_profile',
33
  'completion_report',
34
  'conflict',
35
  'consent',
36
  'contract',
37
  'counterparty_brief',
38
  'delegation',
39
  'event',
40
  'feedback',
41
  'interaction',
42
  'interaction_conclusion',
43
  'interaction_feedback',
44
  'learning_eligibility',
45
  'federated_interaction',
46
  'live_session_event',
47
  'live_session_state',
48
  'profile_delta',
49
  'receipt',
50
  'revocation',
51
  'shield_case',
52
  'shield_consultation',
53
  'shield_outcome',
54
]);
55
56
export function shouldJournalSourceRecord(kind: string): boolean {
57
  return SOURCE_JOURNAL_RECORD_KINDS.has(kind);
58
}
59
60
const ACCOUNT_RECORD_KINDS = new Set([
61
  'agent_profile',
62
  'installation',
63
  'presence',
64
  'project',
65
  'publication',
66
  'setup_request',
67
]);
68
69
const OPERATIONAL_RECORD_KINDS = new Set(['counterparty_brief', 'feedback_request', 'profile']);
70
71
const SHARED_RECORD_KINDS = new Set([
72
  'completion_report',
73
  'contract',
74
  'federated_interaction',
75
  'interaction_conclusion',
76
  'learning_eligibility',
77
  'live_session_event',
78
  'live_session_state',
79
  'receipt',
80
]);
81
82
const ENTITY_REFERENCE_FIELDS = [
83
  'agentId',
84
  'caseId',
85
  'contractHash',
86
  'counterpartyAgentId',
87
  'decisionId',
88
  'deploymentId',
89
  'interactionId',
90
  'consultationId',
91
  'outcomeId',
92
  'projectId',
93
  'reportId',
94
  'reportingAgentId',
95
  'requestId',
96
  'reviewerAgentId',
97
  'subjectAgentId',
98
] as const;
99
100
const REPORTED_AT_FIELDS = [
101
  'occurredAt',
102
  'completedAt',
103
  'decidedAt',
104
  'appliedAt',
105
  'generatedAt',
106
  'createdAt',
107
  'requestedAt',
108
  'updatedAt',
109
] as const;
110
111
function payloadObject(payload: unknown): Record<string, unknown> | undefined {
112
  return payload !== null && typeof payload === 'object' && !Array.isArray(payload)
113
    ? (payload as Record<string, unknown>)
114
    : undefined;
115
}
116
117
function defaultRetentionClass(kind: string): RetentionClass {
118
  if (ACCOUNT_RECORD_KINDS.has(kind)) return 'account';
119
  if (OPERATIONAL_RECORD_KINDS.has(kind)) return 'operational';
120
  return 'audit';
121
}
122
123
function inferredEntityRefs(payload: unknown): Record<string, string> {
124
  const object = payloadObject(payload);
125
  if (!object) return {};
126
  return Object.fromEntries(
127
    ENTITY_REFERENCE_FIELDS.flatMap((field) => {
128
      const value = object[field];
129
      return typeof value === 'string' && value ? [[field, value]] : [];
130
    }),
131
  );
132
}
133
134
function inferredProvenance(payload: unknown): SourceRecordEnvelope['provenance'] {
135
  const value = payloadObject(payload)?.provenance;
136
  const parsed = ProvenanceSchema.safeParse(value);
137
  return parsed.success ? parsed.data : 'observed';
138
}
139
140
function inferredLearningScope(payload: unknown): LearningScope {
141
  const object = payloadObject(payload);
142
  const candidate = object?.contributionMode ?? object?.learningScope;
143
  const parsed = LearningScopeSchema.safeParse(candidate);
144
  return parsed.success ? parsed.data : 'not_evaluated';
145
}
146
147
function inferredReportedAt(payload: unknown, fallback: string): string {
148
  const object = payloadObject(payload);
149
  if (!object) return fallback;
150
  for (const field of REPORTED_AT_FIELDS) {
151
    const value = object[field];
152
    if (typeof value === 'string' && Number.isFinite(Date.parse(value))) return value;
153
  }
154
  return fallback;
155
}
156
157
export function buildSourceRecordEnvelope(input: {
158
  operatorId: string;
159
  kind: string;
160
  recordId: string;
161
  payload: unknown;
162
  metadata?: SourceRecordMetadata;
163
}): SourceRecordEnvelope {
164
  const ingestedAt = input.metadata?.ingestedAt ?? new Date().toISOString();
165
  return SourceRecordEnvelopeSchema.parse({
166
    eventId: input.metadata?.eventId ?? randomUUID(),
167
    operatorId: input.operatorId,
168
    kind: input.kind,
169
    recordId: input.recordId,
170
    schemaName: input.metadata?.schemaName ?? `openclasp.${input.kind}`,
171
    schemaVersion: input.metadata?.schemaVersion ?? '1',
172
    payload: input.payload,
173
    payloadDigest: canonicalHash(input.payload),
174
    entityRefs: {
175
      ...inferredEntityRefs(input.payload),
176
      ...input.metadata?.entityRefs,
177
    },
178
    provenance: input.metadata?.provenance ?? inferredProvenance(input.payload),
179
    visibility:
180
      input.metadata?.visibility ??
181
      (SHARED_RECORD_KINDS.has(input.kind) ? 'shared_participants' : 'local_only'),
182
    retentionClass: input.metadata?.retentionClass ?? defaultRetentionClass(input.kind),
183
    learningScope: input.metadata?.learningScope ?? inferredLearningScope(input.payload),
184
    reportedAt: input.metadata?.reportedAt ?? inferredReportedAt(input.payload, ingestedAt),
185
    ingestedAt,
186
  });
187
}