@sigrun/ext-gateway 0.1.16 → 0.1.19
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/CHANGELOG.md +23 -1
- package/README.md +12 -0
- package/dist/engine.js +2 -1
- package/dist/inbound/pipeline.d.ts +6 -0
- package/dist/inbound/pipeline.js +11 -0
- package/dist/inbound/tokens.d.ts +5 -0
- package/dist/interactions/answer.d.ts +7 -0
- package/dist/interactions/answer.js +23 -0
- package/dist/interactions/chat.js +14 -10
- package/dist/interactions/render.d.ts +1 -1
- package/dist/interactions/render.js +27 -1
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.1.17] - 2026-11-05
|
|
9
|
+
### Fixed
|
|
10
|
+
- `@earendil-works/pi-ai` is declared as a peer. `dist/tools.js` imports
|
|
11
|
+
`Type` from it at runtime but the manifest listed only chord and
|
|
12
|
+
pi-durable, so a standalone install (outside a sigrun host that links
|
|
13
|
+
the module) could not resolve it. The host link masked this in
|
|
14
|
+
practice; the declaration now matches the import.
|
|
15
|
+
|
|
16
|
+
## [0.1.18] - 2026-11-05
|
|
17
|
+
|
|
18
|
+
- Decline button and declined outcomes in chat: a declinable interaction record renders
|
|
19
|
+
a danger-styled Decline button (any plan kind; budgeted against the channel's
|
|
20
|
+
`buttons.max`, falling back to a numbered list with just the Decline row at the
|
|
21
|
+
boundary), `declineFromChat` (write-access re-checked, terminal peek,
|
|
22
|
+
`host.declineInteraction`), declinedVia-aware settlement edits, and the
|
|
23
|
+
`Reply here to decline.` line for no-button channels. Companion to sigrun-core's
|
|
24
|
+
ask-user release line; the `@sigrun/shared` peer gains the 0.9.0 line.
|
|
25
|
+
|
|
26
|
+
## [0.1.19] - 2026-11-06
|
|
27
|
+
|
|
28
|
+
- The @sigrun/shared peer gains the 0.10.0 line (companion to sigrun-core
|
|
29
|
+
0.10.0); check-manifest's allowed line widens with it. No runtime change.
|
|
30
|
+
|
|
9
31
|
## [Unreleased]
|
|
10
32
|
|
|
11
33
|
## [0.1.15] - 2026-11-04
|
package/README.md
CHANGED
|
@@ -171,6 +171,18 @@ long-text field accepts a reply to its prompt. Other forms, including
|
|
|
171
171
|
all forms containing secrets, point to the webui instead of collecting
|
|
172
172
|
values over chat.
|
|
173
173
|
|
|
174
|
+
A record the owning extension marked `declinable` also carries a Decline
|
|
175
|
+
button: a danger-styled row minted independently of the answer plan (a
|
|
176
|
+
webui plan gets just the Decline row after its pointer; a choice plan
|
|
177
|
+
gets the option rows followed by the Decline row; when the options plus
|
|
178
|
+
the Decline row would exceed the channel's button limit, the choice
|
|
179
|
+
renders as a numbered list with just the Decline row). On a channel without
|
|
180
|
+
button capability the prompt instead says `Reply here to decline.` — a
|
|
181
|
+
plain reply that declines is core's message-decline (no gateway code).
|
|
182
|
+
Decline taps take the same write-access re-check as answers and settle
|
|
183
|
+
the record `skipped` with `declinedVia: "button"`; outcome lines read
|
|
184
|
+
`declined by alice` (button) or `declined by alice (replied in chat)`.
|
|
185
|
+
|
|
174
186
|
Button taps, numbered replies, and reply-to text answers require a linked
|
|
175
187
|
sender with current write access to the conversation. Numbered answers
|
|
176
188
|
match only when exactly one choice interaction is pending in that chat.
|
package/dist/engine.js
CHANGED
|
@@ -45,7 +45,7 @@ import { hashId } from "./log.js";
|
|
|
45
45
|
import { collectAttachments } from "./inbound/media.js";
|
|
46
46
|
import { PromptStore } from "./interactions/prompts.js";
|
|
47
47
|
import { createInteractionNotifier, onInteractionSettled } from "./interactions/chat.js";
|
|
48
|
-
import { answerFromChat, createInteractionAnswerStage } from "./interactions/answer.js";
|
|
48
|
+
import { answerFromChat, createInteractionAnswerStage, declineFromChat } from "./interactions/answer.js";
|
|
49
49
|
import { receiptForInteraction } from "./interactions/receipts.js";
|
|
50
50
|
/** The engine's own timer defaults (the real world). */
|
|
51
51
|
function defaultTimers() {
|
|
@@ -456,6 +456,7 @@ export function createEngine(host, deps = {}) {
|
|
|
456
456
|
streams,
|
|
457
457
|
channelOf,
|
|
458
458
|
interactionAnswer: async (answer) => await answerFromChat(interactionDeps(), answer),
|
|
459
|
+
interactionDecline: async (target) => await declineFromChat(interactionDeps(), target),
|
|
459
460
|
stages: {
|
|
460
461
|
interactionAnswer: createInteractionAnswerStage({
|
|
461
462
|
...interactionDeps(),
|
|
@@ -96,6 +96,12 @@ export interface PipelineDeps {
|
|
|
96
96
|
readonly values: Record<string, JsonValue>;
|
|
97
97
|
readonly user: string | undefined;
|
|
98
98
|
}) => Promise<string>;
|
|
99
|
+
/** Ask-user Task 6: the write-gated decline seam (Decline button taps). */
|
|
100
|
+
readonly interactionDecline?: (target: {
|
|
101
|
+
readonly conversationId: number;
|
|
102
|
+
readonly callId: string;
|
|
103
|
+
readonly user: string | undefined;
|
|
104
|
+
}) => Promise<string>;
|
|
99
105
|
/** Cross-channel lookup for `/pair` approval notices. Stand-alone
|
|
100
106
|
* pipelines fall back to their own adapter. */
|
|
101
107
|
readonly channelOf?: (channel: string) => ChannelContribution | undefined;
|
package/dist/inbound/pipeline.js
CHANGED
|
@@ -528,6 +528,17 @@ export function createPipeline(deps) {
|
|
|
528
528
|
if (action === undefined)
|
|
529
529
|
return;
|
|
530
530
|
const tapper = await access.identities.userFor(config, channelId, event.sender.id);
|
|
531
|
+
if (action.kind === "interaction-decline") {
|
|
532
|
+
if (deps.interactionDecline === undefined)
|
|
533
|
+
return;
|
|
534
|
+
const reply = await deps.interactionDecline({
|
|
535
|
+
conversationId: action.conversationId,
|
|
536
|
+
callId: action.callId,
|
|
537
|
+
user: tapper?.user,
|
|
538
|
+
});
|
|
539
|
+
await sendNotice(event.chat, reply, undefined);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
531
542
|
if (action.kind === "interaction") {
|
|
532
543
|
if (deps.interactionAnswer === undefined)
|
|
533
544
|
return;
|
package/dist/inbound/tokens.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export type TokenAction = {
|
|
|
27
27
|
/** The root-form choice field (U6 records are not decision-specific). */
|
|
28
28
|
readonly field?: string;
|
|
29
29
|
readonly optionId: string;
|
|
30
|
+
} | {
|
|
31
|
+
/** Decline a declinable interaction (ask-user Task 6): a human's "no". */
|
|
32
|
+
readonly kind: "interaction-decline";
|
|
33
|
+
readonly conversationId: number;
|
|
34
|
+
readonly callId: string;
|
|
30
35
|
};
|
|
31
36
|
/** The button-token table, one instance per engine. */
|
|
32
37
|
export declare class ButtonTokens {
|
|
@@ -25,6 +25,12 @@ export interface ChatAnswer {
|
|
|
25
25
|
}
|
|
26
26
|
/** Answer after proving the named chat user can still write the conversation. */
|
|
27
27
|
export declare function answerFromChat(deps: AnswerDeps, answer: ChatAnswer): Promise<string>;
|
|
28
|
+
/** Decline a declinable interaction (the write-access + terminal guards mirror answerFromChat). */
|
|
29
|
+
export declare function declineFromChat(deps: Pick<AnswerDeps, "host" | "context">, target: {
|
|
30
|
+
conversationId: number;
|
|
31
|
+
callId: string;
|
|
32
|
+
user: string | undefined;
|
|
33
|
+
}): Promise<string>;
|
|
28
34
|
/** One shared human-readable outcome for settlement edits and stale taps. */
|
|
29
35
|
export declare function outcomeOf(record: {
|
|
30
36
|
readonly status: string;
|
|
@@ -32,6 +38,7 @@ export declare function outcomeOf(record: {
|
|
|
32
38
|
readonly values: Readonly<Record<string, JsonValue>>;
|
|
33
39
|
};
|
|
34
40
|
readonly answeredByUser?: string;
|
|
41
|
+
readonly declinedVia?: "button" | "message";
|
|
35
42
|
}): string;
|
|
36
43
|
export declare function summarizeValues(values: Readonly<Record<string, JsonValue>>): string;
|
|
37
44
|
/** Stage 8: consume a numbered or reply-to answer, else continue admission. */
|
|
@@ -34,12 +34,35 @@ export async function answerFromChat(deps, answer) {
|
|
|
34
34
|
}
|
|
35
35
|
return `Answered: ${summarizeValues(answer.values)}.`;
|
|
36
36
|
}
|
|
37
|
+
/** Decline a declinable interaction (the write-access + terminal guards mirror answerFromChat). */
|
|
38
|
+
export async function declineFromChat(deps, target) {
|
|
39
|
+
if (target.user === undefined)
|
|
40
|
+
return "You do not have write access to this conversation.";
|
|
41
|
+
const meta = await deps.host.conversations.meta(target.conversationId);
|
|
42
|
+
const access = await deps.host.access.accessOf({ user: target.user }, { owner: meta.owner, sharing: meta.sharing });
|
|
43
|
+
if (access !== "write")
|
|
44
|
+
return "You do not have write access to this conversation.";
|
|
45
|
+
const context = deps.context ?? { abortSignal: new AbortController().signal };
|
|
46
|
+
const record = await deps.host.peekInteraction(target.conversationId, target.callId, context);
|
|
47
|
+
if (record !== undefined && TERMINAL.has(record.status)) {
|
|
48
|
+
return `Already ${outcomeOf(record)}.`;
|
|
49
|
+
}
|
|
50
|
+
if (record === undefined)
|
|
51
|
+
return "That interaction is no longer available.";
|
|
52
|
+
const declined = await deps.host.declineInteraction(target.conversationId, target.callId, context, { user: target.user });
|
|
53
|
+
if (!declined)
|
|
54
|
+
return "Already settled.";
|
|
55
|
+
return "Declined.";
|
|
56
|
+
}
|
|
37
57
|
/** One shared human-readable outcome for settlement edits and stale taps. */
|
|
38
58
|
export function outcomeOf(record) {
|
|
39
59
|
const who = record.answeredByUser === undefined ? "" : ` by ${record.answeredByUser}`;
|
|
40
60
|
if (record.status === "answered") {
|
|
41
61
|
return `answered${who}: ${summarizeValues(record.answer?.values ?? {})}`;
|
|
42
62
|
}
|
|
63
|
+
if (record.status === "skipped" && record.declinedVia !== undefined) {
|
|
64
|
+
return `declined${who}${record.declinedVia === "message" ? " (replied in chat)" : ""}`;
|
|
65
|
+
}
|
|
43
66
|
return `${record.status}${who}`;
|
|
44
67
|
}
|
|
45
68
|
export function summarizeValues(values) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* durable ledger entries; delivery receipts populate PromptStore refs so
|
|
4
4
|
* settlement can edit the exact platform messages.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { outcomeOf } from "./answer.js";
|
|
7
7
|
import { planInteraction, renderInteraction, truncateInteractionText } from "./render.js";
|
|
8
8
|
/** The gateway notifier: interaction.pending plus the ordinary home notifier. */
|
|
9
9
|
export function createInteractionNotifier(deps) {
|
|
@@ -68,7 +68,7 @@ async function notifyInteraction(event, deps) {
|
|
|
68
68
|
conversationId: event.conversationId,
|
|
69
69
|
conversationName: (await deps.host.conversations.meta(event.conversationId)).name ?? `Conversation ${event.conversationId}`,
|
|
70
70
|
...(event.url === undefined ? {} : { link: event.url }),
|
|
71
|
-
issue: (action) => deps.tokens.issue(action.kind === "interaction"
|
|
71
|
+
issue: (action) => deps.tokens.issue(action.kind === "interaction" || action.kind === "interaction-decline"
|
|
72
72
|
? { ...action, conversationId: event.conversationId }
|
|
73
73
|
: action),
|
|
74
74
|
});
|
|
@@ -120,14 +120,18 @@ export async function onInteractionSettled(event, deps, context) {
|
|
|
120
120
|
}
|
|
121
121
|
/** The outcome summary the settlement edit/notice carries. */
|
|
122
122
|
function outcomeText(event) {
|
|
123
|
-
|
|
124
|
-
// SAFETY: the stored answer's values are validated JsonValue
|
|
125
|
-
// record's answer shape)
|
|
126
|
-
// the contract cannot import the json module's runtime shape.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
// Delegates to outcomeOf (ONE wording for live taps and settlement
|
|
124
|
+
// edits); SAFETY: the stored answer's values are validated JsonValue
|
|
125
|
+
// (the record's answer shape) — the payload types them loosely
|
|
126
|
+
// because the contract cannot import the json module's runtime shape.
|
|
127
|
+
return outcomeOf({
|
|
128
|
+
status: event.status,
|
|
129
|
+
...(event.answer === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: { answer: { values: event.answer } }),
|
|
132
|
+
...(event.answeredByUser === undefined ? {} : { answeredByUser: event.answeredByUser }),
|
|
133
|
+
...(event.declinedVia === undefined ? {} : { declinedVia: event.declinedVia }),
|
|
134
|
+
});
|
|
131
135
|
}
|
|
132
136
|
/**
|
|
133
137
|
* Enqueue settlement edits and per-binding fallback outcome notices
|
|
@@ -26,7 +26,7 @@ export interface RenderInteractionOptions {
|
|
|
26
26
|
readonly conversationId?: number;
|
|
27
27
|
/** Mint the opaque transport-private action carried by each button. */
|
|
28
28
|
readonly issue: (action: TokenAction & {
|
|
29
|
-
readonly kind: "interaction";
|
|
29
|
+
readonly kind: "interaction" | "interaction-decline";
|
|
30
30
|
}) => string;
|
|
31
31
|
}
|
|
32
32
|
/** Plan the one answer mechanism the record's root form supports in chat. */
|
|
@@ -27,7 +27,11 @@ export function planInteraction(record, caps) {
|
|
|
27
27
|
kind: "choice",
|
|
28
28
|
field: choice.name,
|
|
29
29
|
options: choice.options,
|
|
30
|
-
|
|
30
|
+
// The Decline row shares the button budget: a declinable
|
|
31
|
+
// record needs one more slot, so a maxed-out choice falls
|
|
32
|
+
// back to the numbered list (its Decline row still renders).
|
|
33
|
+
buttons: choice.options.length + (record.declinable === true ? 1 : 0) <=
|
|
34
|
+
(caps.buttons?.max ?? 0),
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -85,6 +89,28 @@ export function renderInteraction(record, caps, opts) {
|
|
|
85
89
|
if (opts.link !== undefined)
|
|
86
90
|
lines.push(opts.link);
|
|
87
91
|
}
|
|
92
|
+
// The Decline affordance is minted OUTSIDE the plan (any declinable
|
|
93
|
+
// record, independent of plan.kind): a choice plan appends the row
|
|
94
|
+
// after its option rows; a webui plan gets the row after its pointer.
|
|
95
|
+
// A channel without buttons gets a line — a reply-to decline is
|
|
96
|
+
// core's message-decline (no gateway code).
|
|
97
|
+
if (record.declinable === true) {
|
|
98
|
+
if (caps.buttons !== undefined) {
|
|
99
|
+
const declineRow = [{
|
|
100
|
+
label: truncateLabel("Decline", caps.buttons.labelMax),
|
|
101
|
+
data: opts.issue({
|
|
102
|
+
kind: "interaction-decline",
|
|
103
|
+
conversationId: opts.conversationId ?? 0,
|
|
104
|
+
callId: record.callId,
|
|
105
|
+
}),
|
|
106
|
+
style: "danger",
|
|
107
|
+
}];
|
|
108
|
+
buttons = buttons === undefined ? [declineRow] : [...buttons, declineRow];
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
lines.push("Reply here to decline.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
88
114
|
// The body projection is plain text; the capability formatter still
|
|
89
115
|
// runs so native/plain channels get the same §8.6 treatment as finals.
|
|
90
116
|
const formatted = renderChunks(lines.join("\n"), caps).join("\n");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrun/ext-gateway",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Gateway engine extension for sigrun (pi-durable): chat-transport supervision
|
|
3
|
+
"version": "0.1.19",
|
|
4
|
+
"description": "Gateway engine extension for sigrun (pi-durable): chat-transport supervision — channel adapters, inbound pipeline, delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.19.0"
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@earendil-works/chord": "1.3.0 || 1.4.0",
|
|
29
|
+
"@earendil-works/pi-ai": "1.3.0 || 1.4.0",
|
|
29
30
|
"@earendil-works/pi-durable": "1.3.0 || 1.4.0",
|
|
30
|
-
"@sigrun/shared": "0.3.0 || 0.4.0 || 0.4.1 || 0.4.2 || 0.5.0 || 0.6.0 || 0.6.1 || 0.7.0 || 0.8.0"
|
|
31
|
+
"@sigrun/shared": "0.3.0 || 0.4.0 || 0.4.1 || 0.4.2 || 0.5.0 || 0.6.0 || 0.6.1 || 0.7.0 || 0.8.0 || 0.9.0 || 0.10.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@earendil-works/chord": "1.4.0",
|
|
35
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
34
36
|
"@earendil-works/pi-durable": "1.4.0",
|
|
35
37
|
"@sigrun/shared": "file:../sigrun/shared",
|
|
36
38
|
"@sigrun/sigrun": "file:../sigrun",
|
|
@@ -52,4 +54,4 @@
|
|
|
52
54
|
"access": "public",
|
|
53
55
|
"provenance": true
|
|
54
56
|
}
|
|
55
|
-
}
|
|
57
|
+
}
|