@nettee/beacon 0.2.8 → 0.2.9
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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Beacon
|
|
2
2
|
|
|
3
|
-
Beacon is a single-host service that turns Feishu messages and configured schedules into isolated Pi agent runs. Each accepted Trigger is durably claimed, produces an explicit Final Outcome, and may `
|
|
3
|
+
Beacon is a single-host service that turns Feishu messages and configured schedules into isolated Pi agent runs. Each accepted Trigger is durably claimed, produces an explicit Final Outcome, and may `reply_text` / `reply_card` to a person, `notify_card` a group, or stay silent on the admin channel.
|
|
4
4
|
|
|
5
5
|
This repository currently implements the Feishu + Pi MVP described by the
|
|
6
6
|
[Zest Dev Spec](https://github.com/nettee/beacon/blob/main/specs/change/20260913-beacon-feishu-pi-mvp/spec.md).
|
|
@@ -67,8 +67,8 @@ back to `prompt.md`. If neither place has a complete pair, Profile load fails
|
|
|
67
67
|
and lists the missing paths. Both pairs may exist; the workspace pair wins.
|
|
68
68
|
|
|
69
69
|
Beacon prepends an English platform template for this Run: workspace limit,
|
|
70
|
-
inbound vs Schedule vs manual capabilities, the `
|
|
71
|
-
`notify_card` contract, and an optional `submit_feedback` tool.
|
|
70
|
+
inbound vs Schedule vs manual capabilities, the `reply_text` / `reply_card` /
|
|
71
|
+
`no_reply` / `notify_card` contract, and an optional `submit_feedback` tool.
|
|
72
72
|
|
|
73
73
|
Protect the home and secret file before running Beacon:
|
|
74
74
|
|
|
@@ -112,10 +112,11 @@ per-Run directories with mode `0700` when Pi starts.
|
|
|
112
112
|
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, a missing complete `persona.md` + `task.md` pair, escaped Profile markdown, permissive secret or runtime-environment permissions, and missing Profile credentials all fail startup. Beacon validates all Profiles before opening a Feishu connection.
|
|
113
113
|
|
|
114
114
|
Each Schedule uses a five-field cron expression and an IANA timezone. Profile
|
|
115
|
-
`admin.chat_id` is the private chat used for schedule `
|
|
116
|
-
for failure notices. Optional `notify.chat_id` on a
|
|
117
|
-
receives `notify_card`. Beacon deliberately does not
|
|
118
|
-
another destination. A newly discovered Schedule starts at
|
|
115
|
+
`admin.chat_id` is the private chat used for schedule `reply_text` /
|
|
116
|
+
`reply_card` / `no_reply` and for failure notices. Optional `notify.chat_id` on a
|
|
117
|
+
Schedule is the group that receives `notify_card`. Beacon deliberately does not
|
|
118
|
+
infer or fall back to another destination. A newly discovered Schedule starts at
|
|
119
|
+
the current time.
|
|
119
120
|
After sleep or restart, overdue occurrences are reconciled and coalesced to the
|
|
120
121
|
most recent one.
|
|
121
122
|
|
|
@@ -157,18 +158,26 @@ messages include the `run_id` whenever a Run record was created.
|
|
|
157
158
|
|
|
158
159
|
## Final Outcomes
|
|
159
160
|
|
|
160
|
-
An Agent closes the conversational channel with `
|
|
161
|
-
also call `notify_card`
|
|
162
|
-
|
|
163
|
-
- `
|
|
164
|
-
Schedules message the Profile admin.
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
|
|
161
|
+
An Agent closes the conversational channel with `reply_text`, `reply_card`, or
|
|
162
|
+
`no_reply`, and may also call `notify_card` when the Run has a notify target:
|
|
163
|
+
|
|
164
|
+
- `reply_text` sends plain text. Inbound Feishu messages quote-reply the user.
|
|
165
|
+
Schedules message the Profile admin.
|
|
166
|
+
- `reply_card` sends an interactive card on the same conversational channel
|
|
167
|
+
(inbound quote-reply, or Schedule admin chat). It closes that channel by
|
|
168
|
+
itself.
|
|
169
|
+
- `no_reply` finishes without messaging the conversational channel. Its
|
|
170
|
+
`reason` is stored for audit and is never sent. Do not use it on inbound
|
|
171
|
+
messages.
|
|
168
172
|
- `notify_card` posts an interactive card to the Schedule's configured group.
|
|
169
|
-
|
|
173
|
+
Only valid when the Run has a notify target.
|
|
174
|
+
|
|
175
|
+
Which of `reply_text` / `reply_card` / `notify_card` / `no_reply` to use for a
|
|
176
|
+
business result is decided by the Profile persona/task (or the situation), not
|
|
177
|
+
hard-coded by Beacon beyond channel availability (`notify_card` needs a notify
|
|
178
|
+
target; inbound cannot `no_reply`).
|
|
170
179
|
|
|
171
|
-
The card
|
|
180
|
+
The card tools accept a title, Feishu-compatible Markdown body, and up to five
|
|
172
181
|
HTTP(S) link buttons. The first button is styled as primary. The tools accept
|
|
173
182
|
these argument shapes:
|
|
174
183
|
|
|
@@ -194,9 +203,9 @@ these argument shapes:
|
|
|
194
203
|
Beacon binds destinations; the Agent never supplies a `chat_id`. Text remains
|
|
195
204
|
text. Cards are sent with Feishu's `interactive` message type and include the
|
|
196
205
|
card generation time. A schedule `no_reply` without `notify_card` records a
|
|
197
|
-
successful Run with no Delivery. Failures `
|
|
198
|
-
notify a group. Manual local triggers print reply text and any notify card
|
|
199
|
-
Markdown on stdout.
|
|
206
|
+
successful Run with no Delivery. Failures `reply_text` a short error and never
|
|
207
|
+
notify a group. Manual local triggers print reply text/card and any notify card
|
|
208
|
+
as Markdown on stdout.
|
|
200
209
|
|
|
201
210
|
## State and failure behavior
|
|
202
211
|
|
package/dist/outcome/content.js
CHANGED
|
@@ -16,10 +16,6 @@ const textReplySchema = z
|
|
|
16
16
|
const noReplySchema = z
|
|
17
17
|
.object({ kind: z.literal("no_reply"), reason: nonBlank })
|
|
18
18
|
.strict();
|
|
19
|
-
const replySchema = z.discriminatedUnion("kind", [
|
|
20
|
-
textReplySchema,
|
|
21
|
-
noReplySchema,
|
|
22
|
-
]);
|
|
23
19
|
const cardSchema = z
|
|
24
20
|
.object({
|
|
25
21
|
kind: z.literal("card"),
|
|
@@ -28,6 +24,11 @@ const cardSchema = z
|
|
|
28
24
|
buttons: z.array(buttonSchema).max(5).default([]),
|
|
29
25
|
})
|
|
30
26
|
.strict();
|
|
27
|
+
const replySchema = z.discriminatedUnion("kind", [
|
|
28
|
+
textReplySchema,
|
|
29
|
+
noReplySchema,
|
|
30
|
+
cardSchema,
|
|
31
|
+
]);
|
|
31
32
|
export const feedbackItemSchema = z
|
|
32
33
|
.object({
|
|
33
34
|
priority: z.enum(["high", "medium"]),
|
|
@@ -118,7 +119,7 @@ export function mergeOutcome(current, patch) {
|
|
|
118
119
|
};
|
|
119
120
|
}
|
|
120
121
|
/** Stable English summary when the Agent settles without closing reply/no_reply. */
|
|
121
|
-
export const MISSING_REPLY_OUTCOME_SUMMARY = "Expected exactly one of `
|
|
122
|
+
export const MISSING_REPLY_OUTCOME_SUMMARY = "Expected exactly one of `reply_text`, `reply_card`, or `no_reply`, but the Agent Runtime settled without submitting any";
|
|
122
123
|
export function isMissingReplyOutcomeError(error) {
|
|
123
124
|
if (!(error instanceof Error))
|
|
124
125
|
return false;
|
|
@@ -141,6 +142,8 @@ export function renderFinalOutcomeAsText(outcome) {
|
|
|
141
142
|
const parts = [];
|
|
142
143
|
if (outcome.reply.kind === "text")
|
|
143
144
|
parts.push(outcome.reply.text);
|
|
145
|
+
if (outcome.reply.kind === "card")
|
|
146
|
+
parts.push(renderCard(outcome.reply));
|
|
144
147
|
if (outcome.notify)
|
|
145
148
|
parts.push(renderCard(outcome.notify));
|
|
146
149
|
return parts.join("\n\n");
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export const inboundOutOfRoleReply = "这条消息不在我的职责范围内。";
|
|
2
2
|
export const finalOutcomeContractInstructions = [
|
|
3
|
-
"`
|
|
4
|
-
"`
|
|
5
|
-
"`
|
|
3
|
+
"`reply_text` sends plain text. For an inbound Feishu message, quote-reply that message. For a Schedule, message the Profile admin. For a manual Run, print to the operator. Do not fill `chat_id`.",
|
|
4
|
+
"`reply_card` sends a titled Markdown card on the conversational channel (inbound quote-reply, or Schedule admin chat). It closes that channel by itself. Do not also call `reply_text` or `no_reply` on the same Run. Do not fill `chat_id`.",
|
|
5
|
+
"`no_reply` finishes a Run without messaging the conversational channel. Its reason is stored for audit and is never sent. Do not use it on inbound Feishu messages.",
|
|
6
|
+
"`notify_card` posts a titled Markdown card to the Schedule's configured notify group. Call it only when this Run has a notify target. Do not fill `chat_id`.",
|
|
7
|
+
"Which tool to use for a given business result is decided by the Profile persona/task (or the situation), not by these platform lines alone.",
|
|
6
8
|
"Beacon ignores ordinary assistant final text for Delivery.",
|
|
7
9
|
];
|
|
8
10
|
export const feedbackContractInstructions = [
|
package/dist/run/orchestrator.js
CHANGED
|
@@ -77,7 +77,7 @@ export function formatFailureReplyText(runId, failure) {
|
|
|
77
77
|
if (failure?.code === "outcome_missing") {
|
|
78
78
|
const parts = [
|
|
79
79
|
`处理失败(run_id=${runId},code=outcome_missing):`,
|
|
80
|
-
"期望恰好调用一次 `
|
|
80
|
+
"期望恰好调用一次 `reply_text`、`reply_card` 或 `no_reply`,但 Agent 已结束且均未提交。",
|
|
81
81
|
];
|
|
82
82
|
const detail = failure.summary
|
|
83
83
|
? detailBeyondMissingSummary(failure.summary)
|
|
@@ -216,7 +216,7 @@ export class RunOrchestrator {
|
|
|
216
216
|
async deliverOutcome(triggerKey, create = true) {
|
|
217
217
|
const current = await this.record(triggerKey);
|
|
218
218
|
const outcome = current.finalOutcome.content;
|
|
219
|
-
if (outcome.reply.kind === "text") {
|
|
219
|
+
if (outcome.reply.kind === "text" || outcome.reply.kind === "card") {
|
|
220
220
|
await this.deliverField(triggerKey, "delivery", current.target, outcome.reply, create && current.delivery === undefined);
|
|
221
221
|
}
|
|
222
222
|
if (outcome.notify && current.notifyTarget) {
|
|
@@ -411,7 +411,8 @@ export class RunOrchestrator {
|
|
|
411
411
|
if (latest.finalOutcome &&
|
|
412
412
|
(latest.run.state === "succeeded" || latest.run.state === "failed")) {
|
|
413
413
|
const outcome = latest.finalOutcome.content;
|
|
414
|
-
const missingReply = outcome.reply.kind === "text"
|
|
414
|
+
const missingReply = (outcome.reply.kind === "text" || outcome.reply.kind === "card") &&
|
|
415
|
+
!latest.delivery;
|
|
415
416
|
const missingNotify = Boolean(outcome.notify) &&
|
|
416
417
|
Boolean(latest.notifyTarget) &&
|
|
417
418
|
!latest.notifyDelivery;
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { parseOutcomePatch } from "../outcome/content.js";
|
|
3
|
-
export function
|
|
3
|
+
export function replyTextFromToolParams(params) {
|
|
4
4
|
return parseOutcomePatch({
|
|
5
5
|
reply: { kind: "text", text: params.text },
|
|
6
6
|
});
|
|
7
7
|
}
|
|
8
|
+
export function replyCardFromToolParams(params) {
|
|
9
|
+
return parseOutcomePatch({
|
|
10
|
+
reply: {
|
|
11
|
+
kind: "card",
|
|
12
|
+
title: params.title,
|
|
13
|
+
content: params.content,
|
|
14
|
+
buttons: params.buttons ?? [],
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
8
18
|
export function noReplyFromToolParams(params) {
|
|
9
19
|
return parseOutcomePatch({
|
|
10
20
|
reply: { kind: "no_reply", reason: params.reason },
|
|
@@ -63,9 +73,9 @@ function acceptedResult() {
|
|
|
63
73
|
}
|
|
64
74
|
export default function registerOutcomeTools(pi) {
|
|
65
75
|
pi.registerTool({
|
|
66
|
-
name: "
|
|
67
|
-
label: "Reply",
|
|
68
|
-
description: "Send a plain-text reply. Inbound Feishu messages quote-reply the user. Schedules message the admin.
|
|
76
|
+
name: "reply_text",
|
|
77
|
+
label: "Reply Text",
|
|
78
|
+
description: "Send a plain-text reply on the conversational channel. Inbound Feishu messages quote-reply the user. Schedules message the admin. Manual Runs print to the operator.",
|
|
69
79
|
parameters: {
|
|
70
80
|
type: "object",
|
|
71
81
|
properties: {
|
|
@@ -83,6 +93,52 @@ export default function registerOutcomeTools(pi) {
|
|
|
83
93
|
return acceptedResult();
|
|
84
94
|
},
|
|
85
95
|
});
|
|
96
|
+
pi.registerTool({
|
|
97
|
+
name: "reply_card",
|
|
98
|
+
label: "Reply Card",
|
|
99
|
+
description: "Send a structured interactive card on the conversational channel (inbound quote-reply, or Schedule admin chat). Closes that channel by itself. Do not also call reply_text or no_reply on the same Run.",
|
|
100
|
+
parameters: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
title: {
|
|
104
|
+
type: "string",
|
|
105
|
+
minLength: 1,
|
|
106
|
+
description: "The card header title.",
|
|
107
|
+
},
|
|
108
|
+
content: {
|
|
109
|
+
type: "string",
|
|
110
|
+
minLength: 1,
|
|
111
|
+
description: "The card body in Feishu-compatible Markdown.",
|
|
112
|
+
},
|
|
113
|
+
buttons: {
|
|
114
|
+
type: "array",
|
|
115
|
+
maxItems: 5,
|
|
116
|
+
items: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
label: { type: "string", minLength: 1 },
|
|
120
|
+
url: { type: "string", pattern: "^https?://" },
|
|
121
|
+
},
|
|
122
|
+
required: ["label", "url"],
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ["title", "content"],
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
},
|
|
130
|
+
async execute(_toolCallId, params, signal) {
|
|
131
|
+
await invokeBeaconCli({
|
|
132
|
+
reply: {
|
|
133
|
+
kind: "card",
|
|
134
|
+
title: params.title,
|
|
135
|
+
content: params.content,
|
|
136
|
+
buttons: params.buttons ?? [],
|
|
137
|
+
},
|
|
138
|
+
}, signal);
|
|
139
|
+
return acceptedResult();
|
|
140
|
+
},
|
|
141
|
+
});
|
|
86
142
|
pi.registerTool({
|
|
87
143
|
name: "no_reply",
|
|
88
144
|
label: "No Reply",
|
|
@@ -107,7 +163,7 @@ export default function registerOutcomeTools(pi) {
|
|
|
107
163
|
pi.registerTool({
|
|
108
164
|
name: "notify_card",
|
|
109
165
|
label: "Notify Card",
|
|
110
|
-
description: "Post a structured Feishu card to the Schedule's configured notify group.
|
|
166
|
+
description: "Post a structured Feishu card to the Schedule's configured notify group. Only valid when this Run has a notify target. You must still close the conversational channel with reply_text, reply_card, or no_reply on the same Run.",
|
|
111
167
|
parameters: {
|
|
112
168
|
type: "object",
|
|
113
169
|
properties: {
|
|
@@ -33,29 +33,30 @@ export function buildAgentSystemPrompt(profile, trigger) {
|
|
|
33
33
|
].join("\n");
|
|
34
34
|
}
|
|
35
35
|
function thisRunCapabilityInstructions(trigger) {
|
|
36
|
+
const notifyLine = trigger.notify
|
|
37
|
+
? "You may also call `notify_card` at most once."
|
|
38
|
+
: "Do not call `notify_card`.";
|
|
36
39
|
if (trigger.kind === "feishu_message") {
|
|
37
40
|
return [
|
|
38
41
|
"This Run is an inbound Feishu message.",
|
|
39
|
-
"Close the conversational channel with
|
|
40
|
-
"Do not call `no_reply`.
|
|
42
|
+
"Close the conversational channel with exactly one of `reply_text` or `reply_card`.",
|
|
43
|
+
"Do not call `no_reply`.",
|
|
44
|
+
notifyLine,
|
|
41
45
|
];
|
|
42
46
|
}
|
|
43
|
-
const notifyLine = trigger.notify
|
|
44
|
-
? "You may also call `notify_card` at most once."
|
|
45
|
-
: "Do not call `notify_card`.";
|
|
46
47
|
if (trigger.kind === "schedule") {
|
|
47
48
|
const label = trigger.scheduleId
|
|
48
49
|
? `This Run is Schedule ${trigger.scheduleId}.`
|
|
49
50
|
: "This Run is a Schedule.";
|
|
50
51
|
return [
|
|
51
52
|
label,
|
|
52
|
-
"Close the conversational channel with exactly one of `
|
|
53
|
+
"Close the conversational channel with exactly one of `reply_text`, `reply_card`, or `no_reply`.",
|
|
53
54
|
notifyLine,
|
|
54
55
|
];
|
|
55
56
|
}
|
|
56
57
|
return [
|
|
57
58
|
"This Run is a manual operator trigger.",
|
|
58
|
-
"Close the conversational channel with exactly one of `
|
|
59
|
+
"Close the conversational channel with exactly one of `reply_text`, `reply_card`, or `no_reply`.",
|
|
59
60
|
notifyLine,
|
|
60
61
|
];
|
|
61
62
|
}
|