@noodleseed/one 0.173.0 → 0.175.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/solutions-activity.js +1 -1
- package/dist/commands/solutions-activity.js.map +1 -1
- package/dist/commands/solutions-drafts.d.ts.map +1 -1
- package/dist/commands/solutions-drafts.js +36 -9
- package/dist/commands/solutions-drafts.js.map +1 -1
- package/dist/commands/solutions-native-lifecycle.d.ts +5 -0
- package/dist/commands/solutions-native-lifecycle.d.ts.map +1 -0
- package/dist/commands/solutions-native-lifecycle.js +77 -0
- package/dist/commands/solutions-native-lifecycle.js.map +1 -0
- package/dist/commands/solutions-ops-args.d.ts +42 -0
- package/dist/commands/solutions-ops-args.d.ts.map +1 -0
- package/dist/commands/solutions-ops-args.js +47 -0
- package/dist/commands/solutions-ops-args.js.map +1 -0
- package/dist/commands/solutions-ops.d.ts.map +1 -1
- package/dist/commands/solutions-ops.js +9 -46
- package/dist/commands/solutions-ops.js.map +1 -1
- package/dist/commands/solutions-workspace.d.ts +5 -0
- package/dist/commands/solutions-workspace.d.ts.map +1 -0
- package/dist/commands/solutions-workspace.js +157 -0
- package/dist/commands/solutions-workspace.js.map +1 -0
- package/dist/commands/whatsapp-output.d.ts.map +1 -1
- package/dist/commands/whatsapp-output.js +20 -4
- package/dist/commands/whatsapp-output.js.map +1 -1
- package/node_modules/@noodle-borg/admission-limits/dist/public-record-admission.d.ts +16 -9
- package/node_modules/@noodle-borg/admission-limits/dist/public-record-admission.js +5 -2
- package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +9 -0
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +5 -5
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +9 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +13 -1
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +5 -3
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +5 -4
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +6 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +7 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +14 -4
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-capture.js +104 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-judgment.js +93 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-ledger.d.ts +105 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-ledger.js +183 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-model-call.js +38 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-parsers.d.ts +22 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-parsers.js +109 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-review.d.ts +21 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-review.js +44 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-spec.d.ts +18 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-spec.js +86 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-store.d.ts +11 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/collection-store.js +18 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +20 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +38 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-runtime.js +3 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +5 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +5 -0
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/authoring/dist/interaction.d.ts +49 -0
- package/node_modules/@noodle-borg/authoring/dist/interaction.js +25 -0
- package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
- package/node_modules/@noodle-borg/authoring/dist/server.js +4 -0
- package/node_modules/@noodle-borg/authoring/package.json +6 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-drafts.js +7 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-native-lifecycle.js +43 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-records.js +154 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-workspace.js +95 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +4 -149
- package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +2 -1
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +33 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
- package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
- package/node_modules/@noodle-borg/compiler/dist/compile.js +5 -0
- package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
- package/node_modules/@noodle-borg/compiler/dist/index.d.ts +2 -2
- package/node_modules/@noodle-borg/compiler/dist/index.js +1 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-schema.d.ts +41 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-schema.js +62 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-validation.js +220 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +60 -4
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +7 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +9 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +58 -10
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-atomic-state.d.ts +8 -0
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-atomic-state.js +136 -0
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.d.ts +7 -0
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.js +16 -24
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-organization-store.d.ts +4 -2
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-organization-store.js +8 -6
- package/node_modules/@noodle-borg/control-plane/dist/mcp-subdomain-claims.d.ts +2 -1
- package/node_modules/@noodle-borg/control-plane/dist/mcp-subdomain-claims.js +13 -8
- package/node_modules/@noodle-borg/control-plane/dist/organization-agreements.d.ts +13 -4
- package/node_modules/@noodle-borg/control-plane/dist/organization-agreements.js +23 -7
- package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +2 -1
- package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/module/dist/platform-human-identity.d.ts +5 -2
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +16 -4
- package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +3 -6
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/service/dist/application-activity.js +85 -63
- package/node_modules/@noodle-borg/service/dist/application-drafts/contracts.js +9 -0
- package/node_modules/@noodle-borg/service/dist/application-drafts/paths.js +2 -2
- package/node_modules/@noodle-borg/service/dist/application-runtime-target.js +1 -1
- package/node_modules/@noodle-borg/service/dist/business-information/contracts.d.ts +18 -4
- package/node_modules/@noodle-borg/service/dist/business-information/in-memory-business-pages.js +3 -5
- package/node_modules/@noodle-borg/service/dist/business-information/in-memory-installation-lifecycle.js +8 -6
- package/node_modules/@noodle-borg/service/dist/business-information/in-memory-native-lifecycle.js +80 -0
- package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +27 -31
- package/node_modules/@noodle-borg/service/dist/business-information/memory-native-retention.js +29 -0
- package/node_modules/@noodle-borg/service/dist/business-information/model.js +9 -2
- package/node_modules/@noodle-borg/service/dist/business-information/native-query.js +3 -1
- package/node_modules/@noodle-borg/service/dist/business-information/native-record-lifecycle.d.ts +27 -0
- package/node_modules/@noodle-borg/service/dist/business-information/native-record-lifecycle.js +63 -0
- package/node_modules/@noodle-borg/service/dist/business-information/principal-authority.d.ts +3 -1
- package/node_modules/@noodle-borg/service/dist/business-information/principal-authority.js +9 -4
- package/node_modules/@noodle-borg/service/dist/business-information/staff-authority.d.ts +16 -0
- package/node_modules/@noodle-borg/service/dist/business-information/staff-authority.js +60 -0
- package/node_modules/@noodle-borg/service/dist/business-information/workspace-permissions.js +18 -0
- package/node_modules/@noodle-borg/service/dist/business-information-runtime.js +1 -0
- package/node_modules/@noodle-borg/service/dist/business-onboarding.js +16 -3
- package/node_modules/@noodle-borg/service/dist/business-workspaces/contracts.d.ts +110 -0
- package/node_modules/@noodle-borg/service/dist/business-workspaces/contracts.js +52 -0
- package/node_modules/@noodle-borg/service/dist/business-workspaces/paths.js +17 -0
- package/node_modules/@noodle-borg/service/dist/business-workspaces/permissions.d.ts +7 -0
- package/node_modules/@noodle-borg/service/dist/business-workspaces/store.d.ts +102 -0
- package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +70 -8
- package/node_modules/@noodle-borg/service/dist/channels/capability-report.js +73 -0
- package/node_modules/@noodle-borg/service/dist/channels/collection-turn.js +232 -0
- package/node_modules/@noodle-borg/service/dist/channels/composition.js +1 -0
- package/node_modules/@noodle-borg/service/dist/channels/messaging-eligibility.js +45 -0
- package/node_modules/@noodle-borg/service/dist/channels/reply-buttons.js +23 -0
- package/node_modules/@noodle-borg/service/dist/channels/runtime.js +127 -33
- package/node_modules/@noodle-borg/service/dist/native-record-connector.js +19 -4
- package/node_modules/@noodle-borg/service/dist/routes/application-drafts.js +44 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +4 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-authoring-dispatch.js +23 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-access.js +39 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +2 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information-connections.js +10 -4
- package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +11 -6
- package/node_modules/@noodle-borg/service/dist/routes/business-information-installation.js +3 -1
- package/node_modules/@noodle-borg/service/dist/routes/business-information-notice.js +6 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information-pages.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/business-information-record-create.js +7 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information-record-reads.js +2 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information-records.js +332 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-settings.js +6 -3
- package/node_modules/@noodle-borg/service/dist/routes/business-information-staff.js +9 -5
- package/node_modules/@noodle-borg/service/dist/routes/business-information.js +12 -356
- package/node_modules/@noodle-borg/service/dist/routes/business-native-lifecycle.js +46 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-workspaces.js +119 -0
- package/node_modules/@noodle-borg/service/dist/routes/service-probes.js +2 -1
- package/node_modules/@noodle-borg/service/dist/service.js +9 -2
- package/node_modules/@noodle-borg/service/dist/web-capabilities.js +3 -1
- package/node_modules/@noodle-borg/service/package.json +2 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/application-draft-validation.d.ts +59 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-draft-validation.js +38 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +144 -50
- package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +44 -4
- package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.d.ts +258 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.js +118 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +2 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +2 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/native-record-lifecycle.d.ts +81 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/native-record-lifecycle.js +39 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +26 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +24 -1
- package/package.json +2 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { CHANNEL_RETENTION_MS } from './channel-types.js';
|
|
2
|
+
export class CollectionError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code) {
|
|
5
|
+
super(code);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'CollectionError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const TERMINAL = new Set(['completed', 'cancelled', 'expired']);
|
|
11
|
+
const iso = (now) => new Date(now).toISOString();
|
|
12
|
+
export function openCollection(spec, input) {
|
|
13
|
+
const fields = {};
|
|
14
|
+
for (const field of spec.fields)
|
|
15
|
+
fields[field.key] = { status: 'missing', attempts: 0 };
|
|
16
|
+
return {
|
|
17
|
+
id: input.id,
|
|
18
|
+
bindingId: input.bindingId,
|
|
19
|
+
participantId: input.participantId,
|
|
20
|
+
interactionId: spec.interactionId,
|
|
21
|
+
action: spec.action,
|
|
22
|
+
phase: 'collecting',
|
|
23
|
+
fields,
|
|
24
|
+
values: {},
|
|
25
|
+
createdAt: iso(input.now),
|
|
26
|
+
updatedAt: iso(input.now),
|
|
27
|
+
expiresAt: iso(input.now + (input.retentionMs ?? CHANNEL_RETENTION_MS)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Non-consent fields a capture turn may still fill or repair. */
|
|
31
|
+
export function neededFields(spec, ledger) {
|
|
32
|
+
return spec.fields.filter((field) => field.control !== 'consent' && ledger.fields[field.key]?.status !== 'captured');
|
|
33
|
+
}
|
|
34
|
+
function complete(spec, ledger) {
|
|
35
|
+
return spec.fields.every((field) => field.control === 'consent' ||
|
|
36
|
+
field.optional === true ||
|
|
37
|
+
ledger.fields[field.key]?.status === 'captured');
|
|
38
|
+
}
|
|
39
|
+
/** A spec without a consent control needs no consent turn: fields complete straight to confirmation. */
|
|
40
|
+
function consentRequired(spec) {
|
|
41
|
+
return spec.fields.some((field) => field.control === 'consent');
|
|
42
|
+
}
|
|
43
|
+
function requireOpen(ledger) {
|
|
44
|
+
if (TERMINAL.has(ledger.phase) || ledger.phase === 'executing')
|
|
45
|
+
throw new CollectionError('phase_invalid');
|
|
46
|
+
}
|
|
47
|
+
/** Recompute the phase after fields changed; a changed value always drops the pending proposal. */
|
|
48
|
+
function settle(spec, ledger, now) {
|
|
49
|
+
const { proposal: _dropped, ...rest } = ledger;
|
|
50
|
+
const phase = !complete(spec, ledger)
|
|
51
|
+
? 'collecting'
|
|
52
|
+
: ledger.consent?.given || !consentRequired(spec)
|
|
53
|
+
? 'awaiting_confirmation'
|
|
54
|
+
: 'awaiting_consent';
|
|
55
|
+
return { ...rest, phase, updatedAt: iso(now) };
|
|
56
|
+
}
|
|
57
|
+
function close(ledger, phase, now) {
|
|
58
|
+
const { proposal: _continuation, ...rest } = ledger;
|
|
59
|
+
return { ...rest, phase, values: {}, updatedAt: iso(now) };
|
|
60
|
+
}
|
|
61
|
+
export function applyCapture(spec, ledger, outcome, now) {
|
|
62
|
+
requireOpen(ledger);
|
|
63
|
+
const fields = { ...ledger.fields };
|
|
64
|
+
const values = { ...ledger.values };
|
|
65
|
+
let touched = false;
|
|
66
|
+
for (const field of spec.fields) {
|
|
67
|
+
const result = outcome[field.key];
|
|
68
|
+
if (result === undefined || field.control === 'consent')
|
|
69
|
+
continue;
|
|
70
|
+
touched = true;
|
|
71
|
+
const attempts = (fields[field.key]?.attempts ?? 0) + 1;
|
|
72
|
+
if (result.status === 'captured') {
|
|
73
|
+
fields[field.key] = { status: 'captured', attempts };
|
|
74
|
+
values[field.key] = result.value;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
fields[field.key] = { status: result.status, reason: result.reason, attempts };
|
|
78
|
+
delete values[field.key];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// A turn that changes nothing keeps the pending proposal; only a new value invalidates it.
|
|
82
|
+
if (!touched)
|
|
83
|
+
return { ...ledger, updatedAt: iso(now) };
|
|
84
|
+
return settle(spec, { ...ledger, fields, values }, now);
|
|
85
|
+
}
|
|
86
|
+
/** The user said a stored value is wrong; keep it so a partial correction can be applied. */
|
|
87
|
+
export function rejectField(spec, ledger, key, now) {
|
|
88
|
+
requireOpen(ledger);
|
|
89
|
+
const current = ledger.fields[key];
|
|
90
|
+
if (current === undefined)
|
|
91
|
+
throw new CollectionError('field_unknown');
|
|
92
|
+
return settle(spec, {
|
|
93
|
+
...ledger,
|
|
94
|
+
fields: {
|
|
95
|
+
...ledger.fields,
|
|
96
|
+
[key]: { status: 'rejected_by_user', reason: 'rejected', attempts: current.attempts },
|
|
97
|
+
},
|
|
98
|
+
}, now);
|
|
99
|
+
}
|
|
100
|
+
export function applyConsent(spec, ledger, decision, questionVersion, now) {
|
|
101
|
+
if (ledger.phase !== 'awaiting_consent')
|
|
102
|
+
throw new CollectionError('phase_invalid');
|
|
103
|
+
const consent = { given: decision === 'given', questionVersion, at: iso(now) };
|
|
104
|
+
if (decision === 'declined')
|
|
105
|
+
return close({ ...ledger, consent }, 'cancelled', now);
|
|
106
|
+
const fields = { ...ledger.fields };
|
|
107
|
+
const values = { ...ledger.values };
|
|
108
|
+
for (const field of spec.fields) {
|
|
109
|
+
if (field.control !== 'consent')
|
|
110
|
+
continue;
|
|
111
|
+
fields[field.key] = { status: 'captured', attempts: (fields[field.key]?.attempts ?? 0) + 1 };
|
|
112
|
+
values[field.key] = true;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...ledger,
|
|
116
|
+
consent,
|
|
117
|
+
fields,
|
|
118
|
+
values,
|
|
119
|
+
phase: 'awaiting_confirmation',
|
|
120
|
+
updatedAt: iso(now),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function attachProposal(spec, ledger, proposal, now) {
|
|
124
|
+
if (ledger.phase !== 'awaiting_confirmation' ||
|
|
125
|
+
(consentRequired(spec) && ledger.consent?.given !== true))
|
|
126
|
+
throw new CollectionError('phase_invalid');
|
|
127
|
+
return {
|
|
128
|
+
...ledger,
|
|
129
|
+
proposal: {
|
|
130
|
+
...proposal,
|
|
131
|
+
createdAt: iso(now),
|
|
132
|
+
expiresAt: iso(now + spec.confirmationExpiryMs),
|
|
133
|
+
},
|
|
134
|
+
updatedAt: iso(now),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function pendingProposal(ledger, now) {
|
|
138
|
+
const proposal = ledger.proposal;
|
|
139
|
+
if (proposal === undefined || Date.parse(proposal.expiresAt) <= now)
|
|
140
|
+
return undefined;
|
|
141
|
+
return proposal;
|
|
142
|
+
}
|
|
143
|
+
export function applyConfirmation(ledger, act, now) {
|
|
144
|
+
if (act === 'cancel')
|
|
145
|
+
return cancelCollection(ledger, now);
|
|
146
|
+
if (ledger.phase !== 'awaiting_confirmation')
|
|
147
|
+
throw new CollectionError('phase_invalid');
|
|
148
|
+
if (ledger.proposal === undefined)
|
|
149
|
+
throw new CollectionError('proposal_missing');
|
|
150
|
+
if (pendingProposal(ledger, now) === undefined)
|
|
151
|
+
throw new CollectionError('proposal_expired');
|
|
152
|
+
return { ...ledger, phase: 'executing', updatedAt: iso(now) };
|
|
153
|
+
}
|
|
154
|
+
export function completeCollection(ledger, now) {
|
|
155
|
+
if (ledger.phase !== 'executing')
|
|
156
|
+
throw new CollectionError('phase_invalid');
|
|
157
|
+
const done = close(ledger, 'completed', now);
|
|
158
|
+
return ledger.proposal === undefined ? done : { ...done, executedProposalId: ledger.proposal.id };
|
|
159
|
+
}
|
|
160
|
+
/** The participant asked to change something before saying what: the proposal and its acts lapse, every value stays. */
|
|
161
|
+
export function withdrawProposal(ledger, now) {
|
|
162
|
+
requireOpen(ledger);
|
|
163
|
+
const { proposal: _withdrawn, ...rest } = ledger;
|
|
164
|
+
return { ...rest, updatedAt: iso(now) };
|
|
165
|
+
}
|
|
166
|
+
export function cancelCollection(ledger, now) {
|
|
167
|
+
if (TERMINAL.has(ledger.phase))
|
|
168
|
+
throw new CollectionError('phase_invalid');
|
|
169
|
+
return close(ledger, 'cancelled', now);
|
|
170
|
+
}
|
|
171
|
+
/** Returns the same ledger when nothing has expired, so callers can detect a change by identity. */
|
|
172
|
+
export function expireCollection(ledger, now) {
|
|
173
|
+
if (TERMINAL.has(ledger.phase))
|
|
174
|
+
return ledger;
|
|
175
|
+
if (Date.parse(ledger.expiresAt) <= now)
|
|
176
|
+
return close(ledger, 'expired', now);
|
|
177
|
+
if (ledger.proposal !== undefined && pendingProposal(ledger, now) === undefined) {
|
|
178
|
+
const { proposal: _expired, ...rest } = ledger;
|
|
179
|
+
return { ...rest, updatedAt: iso(now) };
|
|
180
|
+
}
|
|
181
|
+
return ledger;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=collection-ledger.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { requestModelCompletion } from './model-request.js';
|
|
2
|
+
export const COLLECTION_MODEL_TIMEOUT_MS = 30_000;
|
|
3
|
+
const MAX_RESPONSE_BYTES = 1 << 16;
|
|
4
|
+
const MAX_COMPLETION_TOKENS = 512;
|
|
5
|
+
/**
|
|
6
|
+
* One tool-free JSON-mode request with exactly two messages: the task and the user's text. Transport
|
|
7
|
+
* and admission errors propagate to the caller; output that fails the schema reads as `undefined`.
|
|
8
|
+
*/
|
|
9
|
+
export async function boundedJsonCall(deps, system, user, schema) {
|
|
10
|
+
const signal = AbortSignal.any([deps.signal, AbortSignal.timeout(COLLECTION_MODEL_TIMEOUT_MS)].filter((candidate) => candidate !== undefined));
|
|
11
|
+
const completion = await requestModelCompletion({
|
|
12
|
+
binding: deps.binding,
|
|
13
|
+
messages: [
|
|
14
|
+
{ role: 'system', content: system },
|
|
15
|
+
{ role: 'user', content: user },
|
|
16
|
+
],
|
|
17
|
+
tools: [],
|
|
18
|
+
toolChoice: 'none',
|
|
19
|
+
jsonOutput: true,
|
|
20
|
+
fetcher: deps.fetcher,
|
|
21
|
+
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
22
|
+
maxCompletionTokens: MAX_COMPLETION_TOKENS,
|
|
23
|
+
signal,
|
|
24
|
+
});
|
|
25
|
+
const content = completion.choices[0]?.message.content;
|
|
26
|
+
if (typeof content !== 'string')
|
|
27
|
+
return undefined;
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(content);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const result = schema.safeParse(parsed);
|
|
36
|
+
return result.success ? result.data : undefined;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=collection-model-call.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic readers for typed collection controls (ADR 0240 decision 5). A correctly typed value
|
|
3
|
+
* is read here and never reaches a model; the same rules validate whatever a model does return.
|
|
4
|
+
*/
|
|
5
|
+
export type ParsedControl = 'email' | 'phone' | 'url';
|
|
6
|
+
export interface ParsedSpan {
|
|
7
|
+
readonly control: ParsedControl;
|
|
8
|
+
/** Normalised value: lowercase domain or host, `+` and digits for phones, https for bare hosts. */
|
|
9
|
+
readonly value: string;
|
|
10
|
+
readonly start: number;
|
|
11
|
+
readonly end: number;
|
|
12
|
+
}
|
|
13
|
+
/** Validate and normalise one whole value with the control's rules; `undefined` means not that type. */
|
|
14
|
+
export declare function normaliseTypedValue(control: ParsedControl, raw: string): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Every candidate of the requested controls, in text order. Emails claim their span before URLs and
|
|
17
|
+
* URLs before phones, so digits inside an address are never read as a number.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scanTypedValues(text: string, controls: readonly ParsedControl[]): readonly ParsedSpan[];
|
|
20
|
+
/** Replace each span with the caller's placeholder; used before any utterance reaches a model. */
|
|
21
|
+
export declare function redactSpans(text: string, spans: readonly ParsedSpan[], placeholder: (span: ParsedSpan) => string): string;
|
|
22
|
+
//# sourceMappingURL=collection-parsers.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+/g;
|
|
2
|
+
const ABSOLUTE_URL = /https?:\/\/[^\s<>"'`]+/gi;
|
|
3
|
+
const BARE_HOST = /(?<![\w@./-])(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?:\/[^\s<>"'`]*)?/g;
|
|
4
|
+
/** Plain digit runs with the separators people type; at least seven digits (dates match: accepted). */
|
|
5
|
+
const PHONE = /\+?\(?\d[\d\s().-]{5,}\d/g;
|
|
6
|
+
const URL_TRAILING = /[.,;:!?)\]]+$/;
|
|
7
|
+
const HOST = /^(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?::\d{2,5})?$/;
|
|
8
|
+
/** Bare tokens ending in a file extension are attachments people mention, not websites. */
|
|
9
|
+
const FILE_EXTENSIONS = new Set([
|
|
10
|
+
'pdf',
|
|
11
|
+
'doc',
|
|
12
|
+
'docx',
|
|
13
|
+
'xls',
|
|
14
|
+
'xlsx',
|
|
15
|
+
'ppt',
|
|
16
|
+
'pptx',
|
|
17
|
+
'csv',
|
|
18
|
+
'txt',
|
|
19
|
+
'png',
|
|
20
|
+
'jpg',
|
|
21
|
+
'jpeg',
|
|
22
|
+
'gif',
|
|
23
|
+
'zip',
|
|
24
|
+
'mp3',
|
|
25
|
+
'mp4',
|
|
26
|
+
]);
|
|
27
|
+
const SCAN_ORDER = ['email', 'url', 'phone'];
|
|
28
|
+
function normaliseEmail(raw) {
|
|
29
|
+
const match = raw.match(EMAIL);
|
|
30
|
+
if (match?.length !== 1 || match[0] !== raw)
|
|
31
|
+
return undefined;
|
|
32
|
+
const at = raw.lastIndexOf('@');
|
|
33
|
+
return `${raw.slice(0, at)}@${raw.slice(at + 1).toLowerCase()}`;
|
|
34
|
+
}
|
|
35
|
+
function normalisePhone(raw) {
|
|
36
|
+
if (!/^\+?[\d\s().-]+$/.test(raw))
|
|
37
|
+
return undefined;
|
|
38
|
+
const digits = raw.replace(/\D/g, '');
|
|
39
|
+
if (digits.length < 7 || digits.length > 15)
|
|
40
|
+
return undefined;
|
|
41
|
+
return `${raw.startsWith('+') ? '+' : ''}${digits}`;
|
|
42
|
+
}
|
|
43
|
+
function normaliseUrl(raw) {
|
|
44
|
+
const trimmed = raw.replace(URL_TRAILING, '');
|
|
45
|
+
const absolute = /^https?:\/\//i.test(trimmed);
|
|
46
|
+
const candidate = absolute ? trimmed : `https://${trimmed}`;
|
|
47
|
+
const parts = /^(https?):\/\/([^/?#\s]+)(.*)$/i.exec(candidate);
|
|
48
|
+
const scheme = parts?.[1];
|
|
49
|
+
const rawHost = parts?.[2];
|
|
50
|
+
if (scheme === undefined || rawHost === undefined || !HOST.test(rawHost))
|
|
51
|
+
return undefined;
|
|
52
|
+
const host = rawHost.toLowerCase();
|
|
53
|
+
if (!absolute && FILE_EXTENSIONS.has(host.slice(host.lastIndexOf('.') + 1)))
|
|
54
|
+
return undefined;
|
|
55
|
+
return `${scheme.toLowerCase()}://${host}${parts?.[3] ?? ''}`;
|
|
56
|
+
}
|
|
57
|
+
/** Validate and normalise one whole value with the control's rules; `undefined` means not that type. */
|
|
58
|
+
export function normaliseTypedValue(control, raw) {
|
|
59
|
+
const value = raw.trim();
|
|
60
|
+
if (value.length === 0 || value.length > 2048)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (control === 'email')
|
|
63
|
+
return normaliseEmail(value);
|
|
64
|
+
if (control === 'phone')
|
|
65
|
+
return normalisePhone(value);
|
|
66
|
+
return normaliseUrl(value);
|
|
67
|
+
}
|
|
68
|
+
function matches(text, control) {
|
|
69
|
+
const patterns = control === 'email' ? [EMAIL] : control === 'url' ? [ABSOLUTE_URL, BARE_HOST] : [PHONE];
|
|
70
|
+
const found = [];
|
|
71
|
+
for (const pattern of patterns) {
|
|
72
|
+
for (const match of text.matchAll(pattern)) {
|
|
73
|
+
const raw = control === 'url' ? match[0].replace(URL_TRAILING, '') : match[0];
|
|
74
|
+
if (raw.length > 0)
|
|
75
|
+
found.push({ start: match.index, end: match.index + raw.length });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return found.sort((a, b) => a.start - b.start);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Every candidate of the requested controls, in text order. Emails claim their span before URLs and
|
|
82
|
+
* URLs before phones, so digits inside an address are never read as a number.
|
|
83
|
+
*/
|
|
84
|
+
export function scanTypedValues(text, controls) {
|
|
85
|
+
const spans = [];
|
|
86
|
+
for (const control of SCAN_ORDER) {
|
|
87
|
+
if (!controls.includes(control))
|
|
88
|
+
continue;
|
|
89
|
+
for (const { start, end } of matches(text, control)) {
|
|
90
|
+
if (spans.some((span) => start < span.end && end > span.start))
|
|
91
|
+
continue;
|
|
92
|
+
const value = normaliseTypedValue(control, text.slice(start, end));
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
spans.push({ control, value, start, end });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return spans.sort((a, b) => a.start - b.start);
|
|
98
|
+
}
|
|
99
|
+
/** Replace each span with the caller's placeholder; used before any utterance reaches a model. */
|
|
100
|
+
export function redactSpans(text, spans, placeholder) {
|
|
101
|
+
let out = '';
|
|
102
|
+
let cursor = 0;
|
|
103
|
+
for (const span of [...spans].sort((a, b) => a.start - b.start)) {
|
|
104
|
+
out += text.slice(cursor, span.start) + placeholder(span);
|
|
105
|
+
cursor = span.end;
|
|
106
|
+
}
|
|
107
|
+
return out + text.slice(cursor);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=collection-parsers.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CollectionConsent, CollectionFieldStatus, CollectionLedger, CollectionPhase, CollectionSpec } from './collection-ledger.js';
|
|
2
|
+
/** What the conversation model may see: statuses, reason codes and non-private values only. */
|
|
3
|
+
export interface CollectionModelView {
|
|
4
|
+
readonly phase: CollectionPhase;
|
|
5
|
+
readonly consent?: 'given' | 'declined';
|
|
6
|
+
readonly fields: readonly {
|
|
7
|
+
readonly key: string;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly status: CollectionFieldStatus;
|
|
10
|
+
readonly reason?: string;
|
|
11
|
+
}[];
|
|
12
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
13
|
+
readonly pendingProposal: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The exact review the confirmation binds to: one `Title: value` line per non-consent field in spec
|
|
17
|
+
* order, then the contact permission line. The caller renders this string; a model never restates it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderReview(spec: CollectionSpec, values: Readonly<Record<string, unknown>>, consent: CollectionConsent | undefined): string;
|
|
20
|
+
export declare function ledgerForModel(spec: CollectionSpec, ledger: CollectionLedger, now: number): CollectionModelView;
|
|
21
|
+
//# sourceMappingURL=collection-review.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { pendingProposal } from './collection-ledger.js';
|
|
2
|
+
function formatValue(value) {
|
|
3
|
+
if (value === undefined || value === null || value === '')
|
|
4
|
+
return 'none';
|
|
5
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The exact review the confirmation binds to: one `Title: value` line per non-consent field in spec
|
|
9
|
+
* order, then the contact permission line. The caller renders this string; a model never restates it.
|
|
10
|
+
*/
|
|
11
|
+
export function renderReview(spec, values, consent) {
|
|
12
|
+
const lines = spec.fields
|
|
13
|
+
.filter((field) => field.control !== 'consent')
|
|
14
|
+
.map((field) => `${field.title}: ${formatValue(values[field.key])}`);
|
|
15
|
+
if (spec.fields.some((field) => field.control === 'consent'))
|
|
16
|
+
lines.push(`Contact permission: ${consent?.given === true ? 'yes' : 'no'}`);
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
19
|
+
export function ledgerForModel(spec, ledger, now) {
|
|
20
|
+
const values = {};
|
|
21
|
+
const fields = spec.fields
|
|
22
|
+
.filter((field) => field.control !== 'consent')
|
|
23
|
+
.map((field) => {
|
|
24
|
+
const state = ledger.fields[field.key] ?? { status: 'missing', attempts: 0 };
|
|
25
|
+
if (state.status === 'captured' && field.private !== true && field.key in ledger.values)
|
|
26
|
+
values[field.key] = ledger.values[field.key];
|
|
27
|
+
return {
|
|
28
|
+
key: field.key,
|
|
29
|
+
title: field.title,
|
|
30
|
+
status: state.status,
|
|
31
|
+
...(state.reason === undefined ? {} : { reason: state.reason }),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
phase: ledger.phase,
|
|
36
|
+
...(ledger.consent === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { consent: ledger.consent.given ? 'given' : 'declined' }),
|
|
39
|
+
fields,
|
|
40
|
+
values,
|
|
41
|
+
pendingProposal: pendingProposal(ledger, now) !== undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=collection-review.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type RuntimeArtifact } from '@noodle-borg/compiler';
|
|
2
|
+
import { type CollectionLedger, type CollectionSpec } from './collection-ledger.js';
|
|
3
|
+
/** Platform default confirmation expiry for the WhatsApp profile (ADR 0240 decision 9). */
|
|
4
|
+
export declare const WHATSAPP_CONFIRMATION_EXPIRY_MS = 600000;
|
|
5
|
+
/**
|
|
6
|
+
* Derive the runtime collection spec from an opener's compiled `collect` interaction and its action's
|
|
7
|
+
* input schema (ADR 0240 decision 5). Field titles, options and validation come from the action
|
|
8
|
+
* schema, so the review and every candidate check use the same contract the action itself enforces;
|
|
9
|
+
* the consent question is fixed runtime wording, never authored prose.
|
|
10
|
+
*/
|
|
11
|
+
export declare function collectionSpecFor(artifact: RuntimeArtifact, opener: string, profileExpiryMs: number): CollectionSpec | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Seed a freshly opened ledger from the opener's output through the interaction's `initialValues`.
|
|
14
|
+
* A seeded value that is a declared field is captured only if the action schema accepts it; a seeded
|
|
15
|
+
* input that is not a field rides along in the ledger values so the prepared action still receives it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function seedCollection(artifact: RuntimeArtifact, spec: CollectionSpec, ledger: CollectionLedger, output: unknown, now: number): CollectionLedger;
|
|
18
|
+
//# sourceMappingURL=collection-spec.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { validateJsonSchema } from '@noodle-borg/compiler';
|
|
2
|
+
import { applyCapture, } from './collection-ledger.js';
|
|
3
|
+
/** Platform default confirmation expiry for the WhatsApp profile (ADR 0240 decision 9). */
|
|
4
|
+
export const WHATSAPP_CONFIRMATION_EXPIRY_MS = 600_000;
|
|
5
|
+
/**
|
|
6
|
+
* Derive the runtime collection spec from an opener's compiled `collect` interaction and its action's
|
|
7
|
+
* input schema (ADR 0240 decision 5). Field titles, options and validation come from the action
|
|
8
|
+
* schema, so the review and every candidate check use the same contract the action itself enforces;
|
|
9
|
+
* the consent question is fixed runtime wording, never authored prose.
|
|
10
|
+
*/
|
|
11
|
+
export function collectionSpecFor(artifact, opener, profileExpiryMs) {
|
|
12
|
+
const interaction = artifact.toolInteractions?.[opener];
|
|
13
|
+
const action = artifact.tools.find((tool) => tool.name === interaction?.action);
|
|
14
|
+
if (interaction === undefined || action === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const properties = objectProperties(action.inputSchema);
|
|
17
|
+
const fields = [];
|
|
18
|
+
for (const field of interaction.fields) {
|
|
19
|
+
const schema = properties[field.key];
|
|
20
|
+
if (schema === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
const options = Array.isArray(schema.enum)
|
|
23
|
+
? schema.enum.filter((option) => typeof option === 'string')
|
|
24
|
+
: undefined;
|
|
25
|
+
fields.push({
|
|
26
|
+
key: field.key,
|
|
27
|
+
title: typeof schema.title === 'string' && schema.title ? schema.title : humanise(field.key),
|
|
28
|
+
control: field.control,
|
|
29
|
+
...(field.private === true ? { private: true } : {}),
|
|
30
|
+
...(field.optional === true ? { optional: true } : {}),
|
|
31
|
+
...(field.control === 'select' && options !== undefined ? { options } : {}),
|
|
32
|
+
schema,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const brand = artifact.server.branding?.name ?? artifact.server.title;
|
|
36
|
+
const authored = interaction.confirmationExpirySeconds;
|
|
37
|
+
return {
|
|
38
|
+
interactionId: opener,
|
|
39
|
+
action: action.name,
|
|
40
|
+
fields,
|
|
41
|
+
consentQuestion: `Do you agree to ${brand} contacting you about this request? Yes or no.`,
|
|
42
|
+
successMessage: interaction.outcome.success,
|
|
43
|
+
confirmationExpiryMs: authored === undefined ? profileExpiryMs : Math.min(profileExpiryMs, authored * 1000),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Seed a freshly opened ledger from the opener's output through the interaction's `initialValues`.
|
|
48
|
+
* A seeded value that is a declared field is captured only if the action schema accepts it; a seeded
|
|
49
|
+
* input that is not a field rides along in the ledger values so the prepared action still receives it.
|
|
50
|
+
*/
|
|
51
|
+
export function seedCollection(artifact, spec, ledger, output, now) {
|
|
52
|
+
const sources = artifact.toolInteractions?.[spec.interactionId]?.initialValues ?? {};
|
|
53
|
+
const record = typeof output === 'object' && output !== null && !Array.isArray(output)
|
|
54
|
+
? output
|
|
55
|
+
: {};
|
|
56
|
+
const outcome = {};
|
|
57
|
+
const extra = {};
|
|
58
|
+
for (const [target, source] of Object.entries(sources)) {
|
|
59
|
+
const value = record[source.fromOutput];
|
|
60
|
+
if (value === undefined || value === null)
|
|
61
|
+
continue;
|
|
62
|
+
const field = spec.fields.find((candidate) => candidate.key === target);
|
|
63
|
+
if (field === undefined)
|
|
64
|
+
extra[target] = value;
|
|
65
|
+
else
|
|
66
|
+
outcome[target] =
|
|
67
|
+
validateJsonSchema(field.schema, value).length === 0
|
|
68
|
+
? { status: 'captured', value }
|
|
69
|
+
: { status: 'invalid', reason: 'schema' };
|
|
70
|
+
}
|
|
71
|
+
const seeded = applyCapture(spec, { ...ledger, values: { ...extra, ...ledger.values } }, outcome, now);
|
|
72
|
+
return seeded;
|
|
73
|
+
}
|
|
74
|
+
function humanise(key) {
|
|
75
|
+
const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ');
|
|
76
|
+
return words.charAt(0).toUpperCase() + words.slice(1).toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
function objectProperties(schema) {
|
|
79
|
+
const properties = schema?.properties;
|
|
80
|
+
if (typeof properties !== 'object' || properties === null || Array.isArray(properties))
|
|
81
|
+
return {};
|
|
82
|
+
return Object.fromEntries(Object.entries(properties).flatMap(([key, value]) => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
83
|
+
? [[key, value]]
|
|
84
|
+
: []));
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=collection-spec.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChannelTransaction } from './channel-store.js';
|
|
2
|
+
import type { CollectionLedger } from './collection-ledger.js';
|
|
3
|
+
/**
|
|
4
|
+
* One sealed `interaction` row per participant inside the binding scope: the fixed id is what makes
|
|
5
|
+
* "at most one open collection" true, and the row carries only the phase and expiry in the clear.
|
|
6
|
+
*/
|
|
7
|
+
export declare function collectionRowId(participantId: string): string;
|
|
8
|
+
export declare function loadCollection(tx: ChannelTransaction, bindingId: string, participantId: string): Promise<CollectionLedger | undefined>;
|
|
9
|
+
export declare function saveCollection(tx: ChannelTransaction, ledger: CollectionLedger): Promise<void>;
|
|
10
|
+
export declare function deleteCollection(tx: ChannelTransaction, bindingId: string, participantId: string): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=collection-store.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { channelRow, channelValue } from './channel-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One sealed `interaction` row per participant inside the binding scope: the fixed id is what makes
|
|
4
|
+
* "at most one open collection" true, and the row carries only the phase and expiry in the clear.
|
|
5
|
+
*/
|
|
6
|
+
export function collectionRowId(participantId) {
|
|
7
|
+
return `interaction:${participantId}`;
|
|
8
|
+
}
|
|
9
|
+
export async function loadCollection(tx, bindingId, participantId) {
|
|
10
|
+
return channelValue(tx, bindingId, collectionRowId(participantId));
|
|
11
|
+
}
|
|
12
|
+
export async function saveCollection(tx, ledger) {
|
|
13
|
+
await tx.put(ledger.bindingId, channelRow(collectionRowId(ledger.participantId), 'interaction', ledger, Date.parse(ledger.updatedAt), { state: ledger.phase, expiresAt: Date.parse(ledger.expiresAt) }));
|
|
14
|
+
}
|
|
15
|
+
export async function deleteCollection(tx, bindingId, participantId) {
|
|
16
|
+
await tx.remove(bindingId, collectionRowId(participantId));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=collection-store.js.map
|
|
@@ -2,7 +2,25 @@ import { type RuntimeArtifact } from '@noodle-borg/compiler';
|
|
|
2
2
|
import { type ExecuteDeps, type InvocationContext } from '@noodle-borg/runtime';
|
|
3
3
|
import type { AssistantToolDispatch } from './assistant-interactive.js';
|
|
4
4
|
import { type MessagingTurnContext } from './assistant-turn-context.js';
|
|
5
|
-
|
|
5
|
+
import { type CollectionLedger } from './collection-ledger.js';
|
|
6
|
+
/**
|
|
7
|
+
* How a messaging turn opens natural collection when the model calls a `collect` opener (ADR 0240).
|
|
8
|
+
* The transport owns the participant's ledger custody; the dispatcher only opens and seeds.
|
|
9
|
+
*/
|
|
10
|
+
export interface MessagingCollectionPort {
|
|
11
|
+
/** The participant's open ledger, if any: an opener never opens a second one. */
|
|
12
|
+
readonly open: CollectionLedger | undefined;
|
|
13
|
+
readonly now: number;
|
|
14
|
+
readonly retentionMs: number;
|
|
15
|
+
readonly confirmationExpiryMs: number;
|
|
16
|
+
save(ledger: CollectionLedger): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read-only transport adapter over the same authorized runtime. A `collect` opener is the one
|
|
20
|
+
* exception: it runs as a read, then opens the participant's collection and tells the model which
|
|
21
|
+
* fields are needed; its confirmed action is never callable here and executes only through the
|
|
22
|
+
* confirmed proposal path.
|
|
23
|
+
*/
|
|
6
24
|
export declare function dispatchMessagingReadTool(input: {
|
|
7
25
|
readonly artifact: RuntimeArtifact;
|
|
8
26
|
readonly tool: RuntimeArtifact['tools'][number];
|
|
@@ -11,5 +29,6 @@ export declare function dispatchMessagingReadTool(input: {
|
|
|
11
29
|
readonly context: InvocationContext;
|
|
12
30
|
readonly session: MessagingTurnContext;
|
|
13
31
|
readonly claimTool: (name: string) => Promise<boolean>;
|
|
32
|
+
readonly collection?: MessagingCollectionPort;
|
|
14
33
|
}): Promise<AssistantToolDispatch>;
|
|
15
34
|
//# sourceMappingURL=messaging-tool-dispatch.d.ts.map
|
|
@@ -1,24 +1,42 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { requiresToolConfirmation } from '@noodle-borg/compiler';
|
|
2
3
|
import { evaluateToolAuthorization } from '@noodle-borg/protocol';
|
|
3
4
|
import { executeToolInteractive, } from '@noodle-borg/runtime';
|
|
4
5
|
import { assistantModelToolOncePerSession } from './assistant-guide.js';
|
|
5
6
|
import { assistantSafeOutput } from './assistant-presentation.js';
|
|
6
7
|
import { withAssistantTurnExecutionAuthority, } from './assistant-turn-context.js';
|
|
7
|
-
|
|
8
|
+
import { openCollection } from './collection-ledger.js';
|
|
9
|
+
import { ledgerForModel } from './collection-review.js';
|
|
10
|
+
import { collectionSpecFor, seedCollection } from './collection-spec.js';
|
|
11
|
+
const COLLECTION_GUIDANCE = 'The platform is collecting these fields in this conversation: ask naturally for the ones still missing, in any order and without counters. It validates values, asks for consent, shows the review and confirms. Never mention a form, never ask for or repeat private values, and never claim anything was sent.';
|
|
12
|
+
/**
|
|
13
|
+
* Read-only transport adapter over the same authorized runtime. A `collect` opener is the one
|
|
14
|
+
* exception: it runs as a read, then opens the participant's collection and tells the model which
|
|
15
|
+
* fields are needed; its confirmed action is never callable here and executes only through the
|
|
16
|
+
* confirmed proposal path.
|
|
17
|
+
*/
|
|
8
18
|
export async function dispatchMessagingReadTool(input) {
|
|
9
19
|
const denied = (code) => ({
|
|
10
20
|
kind: 'event',
|
|
11
21
|
event: 'error',
|
|
12
22
|
data: { code, retryable: false },
|
|
13
23
|
});
|
|
24
|
+
const collection = input.artifact.toolInteractions?.[input.tool.name] === undefined ? undefined : input.collection;
|
|
14
25
|
if (input.tool.annotations?.readOnlyHint !== true ||
|
|
15
26
|
requiresToolConfirmation(input.tool.annotations) ||
|
|
16
|
-
input.tool._meta?.ui)
|
|
27
|
+
(input.tool._meta?.ui && collection === undefined))
|
|
17
28
|
return denied('messaging_action_unsupported');
|
|
18
29
|
if (!evaluateToolAuthorization(input.tool.authorization, input.session.caller).allow)
|
|
19
30
|
return denied('tool_forbidden');
|
|
20
31
|
if (assistantModelToolOncePerSession(input.tool) && !(await input.claimTool(input.tool.name)))
|
|
21
32
|
return denied('invalid_model_tool_call');
|
|
33
|
+
const spec = collection === undefined
|
|
34
|
+
? undefined
|
|
35
|
+
: collectionSpecFor(input.artifact, input.tool.name, collection.confirmationExpiryMs);
|
|
36
|
+
if (collection !== undefined && spec === undefined)
|
|
37
|
+
return denied('messaging_action_unsupported');
|
|
38
|
+
if (collection?.open !== undefined && spec !== undefined)
|
|
39
|
+
return collectionResult(spec, collection.open, collection.now, 'already_open');
|
|
22
40
|
const result = await executeToolInteractive(input.artifact, input.tool.name, input.arguments, {
|
|
23
41
|
...withAssistantTurnExecutionAuthority(input.executeDeps, input.artifact, input.session),
|
|
24
42
|
caller: input.session.caller,
|
|
@@ -26,9 +44,27 @@ export async function dispatchMessagingReadTool(input) {
|
|
|
26
44
|
});
|
|
27
45
|
if (result.status !== 'completed')
|
|
28
46
|
return denied(result.status === 'failed' ? result.error.code : 'messaging_action_unsupported');
|
|
47
|
+
if (collection !== undefined && spec !== undefined) {
|
|
48
|
+
const ledger = seedCollection(input.artifact, spec, openCollection(spec, {
|
|
49
|
+
id: randomUUID(),
|
|
50
|
+
bindingId: input.session.bindingId,
|
|
51
|
+
participantId: input.session.participantId,
|
|
52
|
+
now: collection.now,
|
|
53
|
+
retentionMs: collection.retentionMs,
|
|
54
|
+
}), result.output, collection.now);
|
|
55
|
+
await collection.save(ledger);
|
|
56
|
+
return collectionResult(spec, ledger, collection.now, 'opened');
|
|
57
|
+
}
|
|
29
58
|
return {
|
|
30
59
|
kind: 'tool_result',
|
|
31
60
|
output: assistantSafeOutput(input.tool.outputSchema, result.output),
|
|
32
61
|
};
|
|
33
62
|
}
|
|
63
|
+
/** The model sees the ledger view, never the opener's own output, which may carry website copy. */
|
|
64
|
+
function collectionResult(spec, ledger, now, collection) {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'tool_result',
|
|
67
|
+
output: { collection, ...ledgerForModel(spec, ledger, now), guidance: COLLECTION_GUIDANCE },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
34
70
|
//# sourceMappingURL=messaging-tool-dispatch.js.map
|