@segmentstream/agent 0.1.1 → 0.1.3
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 +20 -19
- package/dist/extension/config.d.ts +25 -1
- package/dist/extension/config.mjs +59 -1
- package/dist/extension/extension.d.ts +3 -3
- package/dist/extension/extension.mjs +1 -1
- package/dist/extension/lib/telegram/inbound.d.ts +1 -1
- package/dist/extension/lib/telegram/inbound.mjs +16 -3
- package/dist/extension/model.d.ts +6 -13
- package/dist/extension/model.mjs +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,31 +18,32 @@ npm install @segmentstream/agent eve@0.76.3
|
|
|
18
18
|
|
|
19
19
|
## Set up
|
|
20
20
|
|
|
21
|
-
The workspace's `agent/` directory is an eve app in eve's
|
|
22
|
-
`ontology/`:
|
|
21
|
+
The workspace's `agent/` directory is an eve app in eve's documented layout, a package of its own
|
|
22
|
+
like `ontology/`: the package files at the root, the authored files under `agent/agent/`:
|
|
23
23
|
|
|
24
24
|
```text
|
|
25
25
|
agent/
|
|
26
26
|
├── package.json # eve and @segmentstream/agent, both pinned; "build": "eve build"
|
|
27
27
|
├── segmentstream.json # what the extension mounts, and what the platform reads at deploy
|
|
28
|
-
|
|
29
|
-
├──
|
|
30
|
-
|
|
31
|
-
└──
|
|
28
|
+
└── agent/ # eve's agent directory
|
|
29
|
+
├── agent.ts # model, reasoning, limits
|
|
30
|
+
├── instructions.md # who the company is, how the agent behaves
|
|
31
|
+
└── extensions/
|
|
32
|
+
└── segmentstream.ts # mounts this package with segmentstream.json
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
**`extensions/segmentstream.ts`** mounts the extension. The file name is the mount's namespace, so
|
|
35
|
+
**`agent/agent/extensions/segmentstream.ts`** mounts the extension. The file name is the mount's namespace, so
|
|
35
36
|
every contribution is named `segmentstream__…`:
|
|
36
37
|
|
|
37
38
|
```ts
|
|
38
39
|
import { segmentstream } from "@segmentstream/agent/extension";
|
|
39
40
|
|
|
40
|
-
import config from "
|
|
41
|
+
import config from "../../segmentstream.json";
|
|
41
42
|
|
|
42
43
|
export default segmentstream(config);
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
**`agent.ts`** chooses the model once per session:
|
|
46
|
+
**`agent/agent/agent.ts`** chooses the model once per session:
|
|
46
47
|
|
|
47
48
|
```ts
|
|
48
49
|
import { defineAgent } from "eve";
|
|
@@ -50,17 +51,15 @@ import { segmentstream } from "@segmentstream/agent";
|
|
|
50
51
|
|
|
51
52
|
export default defineAgent({
|
|
52
53
|
defaultTools: false,
|
|
53
|
-
model: segmentstream.model(
|
|
54
|
-
default: "spacexai/grok-4.7",
|
|
55
|
-
byRole: { finance: "anthropic/claude-opus-5.5" },
|
|
56
|
-
preview: "google/gemini-flash",
|
|
57
|
-
}),
|
|
54
|
+
model: segmentstream.model(),
|
|
58
55
|
reasoning: "medium",
|
|
59
56
|
limits: { maxTokenCostUsdPerSession: 2 },
|
|
60
57
|
});
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
`segmentstream.model()` resolves on `session.started
|
|
60
|
+
`segmentstream.model()` resolves on `session.started` from `models.chat` in `agent/segmentstream.json`
|
|
61
|
+
(`{ "default", "byRole", "preview" }`), where the platform reads it too; an inline
|
|
62
|
+
`segmentstream.model({ default, byRole, preview })` works the same. On a preview deployment it returns `preview`
|
|
64
63
|
when set. Otherwise it returns the model of the first `byRole` entry whose role the member holds, in
|
|
65
64
|
the order written, and `default` when none matches. The roles come from the member's pairing, not
|
|
66
65
|
from the message. The model is never switched mid-session, which keeps the provider's prompt cache.
|
|
@@ -117,14 +116,16 @@ share a group chat's session never share a client.
|
|
|
117
116
|
proxy. The token stays in SegmentStream.
|
|
118
117
|
- **Webhook.** A webhook is accepted only with `X-Telegram-Bot-Api-Secret-Token` equal to
|
|
119
118
|
`SEGMENTSTREAM_AGENT_WEBHOOK_SECRET`, compared in constant time. The platform generates the
|
|
120
|
-
secret and registers the webhook after each production deployment
|
|
119
|
+
secret and registers the webhook after each production deployment, and sets the bot's menu
|
|
120
|
+
button, shown in private chats, to open the workspace app.
|
|
121
121
|
- **Production only.** Outside `VERCEL_ENV=production` the route answers 200 and does nothing, so a
|
|
122
122
|
preview never answers a real chat. Without `channels.telegram` it answers 404.
|
|
123
123
|
- **Pairing.** Messages from bots and channel posts are dropped. The sender's Telegram account is
|
|
124
124
|
looked up with the ontology service on every message, so a revocation stops at once. An unknown
|
|
125
|
-
sender who writes privately gets a
|
|
126
|
-
group, the agent only asks an unknown sender to
|
|
127
|
-
|
|
125
|
+
sender who writes privately gets a message with a **Connect account** button that opens
|
|
126
|
+
`/agent/connect?code=…`, valid for ten minutes. In a group, the agent only asks an unknown sender to
|
|
127
|
+
message it privately, with a button to the bot's chat, because anyone in the group could open a
|
|
128
|
+
pairing link posted there. A revoked pairing or an inactive member gets a short refusal.
|
|
128
129
|
- **Who the agent is.** A paired sender's turn runs with the principal id `telegram:<user>`
|
|
129
130
|
(`telegram:<chat>:<user>` in groups) and the attributes `memberId`, `memberName`, `roles`,
|
|
130
131
|
`chatId`, `chatType` and `username`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The extension's configuration: `agent/segmentstream.json`, mounted by
|
|
3
|
-
* `agent/extensions/segmentstream.ts` and read by the platform at deploy.
|
|
3
|
+
* `agent/agent/extensions/segmentstream.ts` and read by the platform at deploy.
|
|
4
4
|
*
|
|
5
5
|
* eve validates a mount's configuration with a Standard Schema. The schema below is written by
|
|
6
6
|
* hand, so the package adds no second schema library next to the Effect Schema the rest of the
|
|
@@ -30,6 +30,16 @@ export interface SegmentStreamAgentConfigInput {
|
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
32
|
readonly models?: {
|
|
33
|
+
/**
|
|
34
|
+
* The chat model `segmentstream.model()` chooses per session: `default` for everyone, one per
|
|
35
|
+
* member role in `byRole` (the first role the member holds wins, in the order written) and
|
|
36
|
+
* `preview` for preview deployments.
|
|
37
|
+
*/
|
|
38
|
+
readonly chat?: {
|
|
39
|
+
readonly default: string;
|
|
40
|
+
readonly byRole?: Readonly<Record<string, string>>;
|
|
41
|
+
readonly preview?: string;
|
|
42
|
+
};
|
|
33
43
|
/** A Gateway transcription model, or `false` to leave voice messages untranscribed. */
|
|
34
44
|
readonly stt?: string | boolean;
|
|
35
45
|
/** The Gateway decision model of the inbound router. */
|
|
@@ -44,12 +54,26 @@ export interface TelegramSettings {
|
|
|
44
54
|
readonly connection: string;
|
|
45
55
|
readonly policy: TelegramPolicy;
|
|
46
56
|
}
|
|
57
|
+
/** The chat models `segmentstream.model()` chooses between, as AI Gateway model ids. */
|
|
58
|
+
export interface ChatModelChoice {
|
|
59
|
+
/** The model for every session no other rule picks. */
|
|
60
|
+
readonly default: string;
|
|
61
|
+
/**
|
|
62
|
+
* A model per member role. The first entry whose role the member holds wins, in the order
|
|
63
|
+
* written, so put the more specific roles first.
|
|
64
|
+
*/
|
|
65
|
+
readonly byRole?: Readonly<Record<string, string>>;
|
|
66
|
+
/** The model of preview deployments, so a preview never spends on a large model. */
|
|
67
|
+
readonly preview?: string;
|
|
68
|
+
}
|
|
47
69
|
/** The validated configuration with defaults applied. It stays JSON-serializable. */
|
|
48
70
|
export interface SegmentStreamAgentConfig {
|
|
49
71
|
readonly channels: {
|
|
50
72
|
readonly telegram: TelegramSettings | null;
|
|
51
73
|
};
|
|
52
74
|
readonly models: {
|
|
75
|
+
/** `null` when `agent.ts` passes the choice to `segmentstream.model()` itself. */
|
|
76
|
+
readonly chat: ChatModelChoice | null;
|
|
53
77
|
readonly stt: string | false;
|
|
54
78
|
readonly decision: string;
|
|
55
79
|
};
|
|
@@ -72,6 +72,58 @@ function modelId(input) {
|
|
|
72
72
|
}
|
|
73
73
|
return input.value;
|
|
74
74
|
}
|
|
75
|
+
function chatModelChoice(value, issues) {
|
|
76
|
+
const path = ["models", "chat"];
|
|
77
|
+
const chat = section({
|
|
78
|
+
value,
|
|
79
|
+
path,
|
|
80
|
+
allowed: [
|
|
81
|
+
"default",
|
|
82
|
+
"byRole",
|
|
83
|
+
"preview"
|
|
84
|
+
],
|
|
85
|
+
issues
|
|
86
|
+
});
|
|
87
|
+
if (chat === void 0) return null;
|
|
88
|
+
const fallback = "";
|
|
89
|
+
if (chat.default === void 0) issues.push({
|
|
90
|
+
message: "is required",
|
|
91
|
+
path: [...path, "default"]
|
|
92
|
+
});
|
|
93
|
+
const defaultModel = modelId({
|
|
94
|
+
value: chat.default,
|
|
95
|
+
fallback,
|
|
96
|
+
path: [...path, "default"],
|
|
97
|
+
issues
|
|
98
|
+
});
|
|
99
|
+
const byRole = section({
|
|
100
|
+
value: chat.byRole,
|
|
101
|
+
path: [...path, "byRole"],
|
|
102
|
+
allowed: Object.keys(isRecord(chat.byRole) ? chat.byRole : {}),
|
|
103
|
+
issues
|
|
104
|
+
});
|
|
105
|
+
const roles = Object.fromEntries(Object.entries(byRole ?? {}).map(([role, model]) => [role, modelId({
|
|
106
|
+
value: model,
|
|
107
|
+
fallback,
|
|
108
|
+
path: [
|
|
109
|
+
...path,
|
|
110
|
+
"byRole",
|
|
111
|
+
role
|
|
112
|
+
],
|
|
113
|
+
issues
|
|
114
|
+
})]));
|
|
115
|
+
const preview = chat.preview === void 0 ? void 0 : modelId({
|
|
116
|
+
value: chat.preview,
|
|
117
|
+
fallback,
|
|
118
|
+
path: [...path, "preview"],
|
|
119
|
+
issues
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
default: defaultModel,
|
|
123
|
+
...byRole === void 0 ? {} : { byRole: roles },
|
|
124
|
+
...preview === void 0 ? {} : { preview }
|
|
125
|
+
};
|
|
126
|
+
}
|
|
75
127
|
function validateConfig(input) {
|
|
76
128
|
const issues = [];
|
|
77
129
|
const root = section({
|
|
@@ -93,7 +145,11 @@ function validateConfig(input) {
|
|
|
93
145
|
const models = section({
|
|
94
146
|
value: root?.models,
|
|
95
147
|
path: ["models"],
|
|
96
|
-
allowed: [
|
|
148
|
+
allowed: [
|
|
149
|
+
"chat",
|
|
150
|
+
"stt",
|
|
151
|
+
"decision"
|
|
152
|
+
],
|
|
97
153
|
issues
|
|
98
154
|
});
|
|
99
155
|
const ontology = section({
|
|
@@ -103,6 +159,7 @@ function validateConfig(input) {
|
|
|
103
159
|
issues
|
|
104
160
|
});
|
|
105
161
|
const telegram = telegramSettings(channels?.telegram, issues);
|
|
162
|
+
const chat = chatModelChoice(models?.chat, issues);
|
|
106
163
|
const stt = models?.stt === false ? false : modelId({
|
|
107
164
|
value: models?.stt,
|
|
108
165
|
fallback: DEFAULT_STT_MODEL,
|
|
@@ -124,6 +181,7 @@ function validateConfig(input) {
|
|
|
124
181
|
return { value: {
|
|
125
182
|
channels: { telegram },
|
|
126
183
|
models: {
|
|
184
|
+
chat,
|
|
127
185
|
stt,
|
|
128
186
|
decision
|
|
129
187
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { manifestOf } from "./config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { type DynamicModel, type ModelChoice } from "./model.js";
|
|
3
3
|
export declare const segmentstream: import("eve/extension").ExtensionHandle<{
|
|
4
4
|
readonly "~standard": {
|
|
5
5
|
readonly version: 1;
|
|
@@ -11,9 +11,9 @@ export declare const segmentstream: import("eve/extension").ExtensionHandle<{
|
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
13
|
}> & {
|
|
14
|
-
model:
|
|
14
|
+
model: (choice?: ModelChoice) => DynamicModel;
|
|
15
15
|
manifestOf: typeof manifestOf;
|
|
16
16
|
};
|
|
17
17
|
export default segmentstream;
|
|
18
|
-
export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, manifestOf, type SegmentStreamAgentConfig, type SegmentStreamAgentConfigInput, type TelegramPolicy, type TelegramSettings, } from "./config.js";
|
|
18
|
+
export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, manifestOf, type ChatModelChoice, type SegmentStreamAgentConfig, type SegmentStreamAgentConfigInput, type TelegramPolicy, type TelegramSettings, } from "./config.js";
|
|
19
19
|
export { chooseModel, model, type DynamicModel, type ModelChoice } from "./model.js";
|
|
@@ -9,7 +9,7 @@ import { chooseModel, model } from "./model.mjs";
|
|
|
9
9
|
import { defineExtension } from "eve/extension";
|
|
10
10
|
const extension = defineExtension({ config: configSchema });
|
|
11
11
|
const segmentstream = Object.assign(extension, {
|
|
12
|
-
model,
|
|
12
|
+
model: (choice) => model(choice ?? (() => extension.config.models.chat)),
|
|
13
13
|
manifestOf
|
|
14
14
|
});
|
|
15
15
|
export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, chooseModel, segmentstream as default, segmentstream, manifestOf, model };
|
|
@@ -10,7 +10,7 @@ import { type Transcriber } from "./voice.js";
|
|
|
10
10
|
* eve's Telegram `onMessage` hook, in the order `agent-design.md` gives:
|
|
11
11
|
*
|
|
12
12
|
* 1. The hard gate, with no model: bots, channel posts and senders without a live pairing never
|
|
13
|
-
* start a turn. An unknown sender who writes to the bot privately gets a pairing
|
|
13
|
+
* start a turn. An unknown sender who writes to the bot privately gets a pairing button.
|
|
14
14
|
* 2. Voice becomes text, so the router and the agent read what was said.
|
|
15
15
|
* 3. The router answers or observes. An observed message goes to the chat's ring and stops here.
|
|
16
16
|
* 4. An answered message dispatches as the paired member, with the ring ahead of it as context.
|
|
@@ -8,8 +8,15 @@ import { telegramPrincipal } from "./principal.mjs";
|
|
|
8
8
|
import { observedChatKey, observedContext } from "./observed.mjs";
|
|
9
9
|
import { isAddressed, routeMessage } from "./router.mjs";
|
|
10
10
|
import { voiceNote, voiceOf, voiceText } from "./voice.mjs";
|
|
11
|
-
const UNKNOWN_PRIVATE = "I don't know
|
|
11
|
+
const UNKNOWN_PRIVATE = "Hi, I don't know you yet. Connect your Telegram account to the workspace and I'll act as you. The button works for ten minutes.";
|
|
12
|
+
const UNKNOWN_IN_GROUP = "I don't know you yet. Message me privately to connect your account.";
|
|
12
13
|
const REFUSED = "Your access to this agent was revoked, or your membership is inactive. Ask a workspace administrator.";
|
|
14
|
+
function buttonTo(text, url) {
|
|
15
|
+
return { inline_keyboard: [[{
|
|
16
|
+
text,
|
|
17
|
+
url
|
|
18
|
+
}]] };
|
|
19
|
+
}
|
|
13
20
|
function display(user) {
|
|
14
21
|
if (user.username !== void 0) return `@${user.username}`;
|
|
15
22
|
const name = [user.firstName, user.lastName].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
@@ -27,7 +34,10 @@ function observedMessage(message, text) {
|
|
|
27
34
|
};
|
|
28
35
|
}
|
|
29
36
|
function unknownInGroup(bot) {
|
|
30
|
-
return
|
|
37
|
+
return bot?.username === void 0 ? { text: UNKNOWN_IN_GROUP } : {
|
|
38
|
+
text: UNKNOWN_IN_GROUP,
|
|
39
|
+
reply_markup: buttonTo("Message me", `https://t.me/${bot.username}`)
|
|
40
|
+
};
|
|
31
41
|
}
|
|
32
42
|
async function answerUnpaired(input) {
|
|
33
43
|
const { context, message, lookup, dependencies } = input;
|
|
@@ -45,7 +55,10 @@ async function answerUnpaired(input) {
|
|
|
45
55
|
externalId: message.from.id,
|
|
46
56
|
...sender === void 0 ? {} : { display: sender }
|
|
47
57
|
});
|
|
48
|
-
await context.telegram.post(
|
|
58
|
+
await context.telegram.post({
|
|
59
|
+
text: UNKNOWN_PRIVATE,
|
|
60
|
+
reply_markup: buttonTo("Connect account", dependencies.connectUrl(pairing.code))
|
|
61
|
+
});
|
|
49
62
|
}
|
|
50
63
|
async function handleInboundMessage(input) {
|
|
51
64
|
const { context, dependencies } = input;
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { type AgentModelDefinition, type AgentStaticModelDefinition } from "eve";
|
|
2
2
|
import type { SessionAuthContext } from "eve/context";
|
|
3
|
+
import type { ChatModelChoice } from "./config.js";
|
|
3
4
|
import { type Environment } from "./lib/environment.js";
|
|
4
5
|
/** The models `segmentstream.model()` chooses between, as AI Gateway model ids. */
|
|
5
|
-
export
|
|
6
|
-
/** The model for every session no other rule picks. */
|
|
7
|
-
readonly default: string;
|
|
8
|
-
/**
|
|
9
|
-
* A model per member role. The first entry whose role the member holds wins, in the order
|
|
10
|
-
* written, so put the more specific roles first.
|
|
11
|
-
*/
|
|
12
|
-
readonly byRole?: Readonly<Record<string, string>>;
|
|
13
|
-
/** The model of preview deployments, so a preview never spends on a large model. */
|
|
14
|
-
readonly preview?: string;
|
|
15
|
-
}
|
|
6
|
+
export type ModelChoice = ChatModelChoice;
|
|
16
7
|
/** The dynamic form of an agent's `model`, which `defineAgent` accepts as the whole field. */
|
|
17
8
|
export type DynamicModel = Exclude<AgentModelDefinition, AgentStaticModelDefinition>;
|
|
18
9
|
/** The one model for a session started by `principal`. */
|
|
@@ -24,6 +15,8 @@ export declare function chooseModel(input: {
|
|
|
24
15
|
/**
|
|
25
16
|
* The agent's model, chosen once when a session starts from the member's roles and `VERCEL_ENV`.
|
|
26
17
|
* Choosing once keeps the provider's prompt cache: a switch mid-session re-reads the history at
|
|
27
|
-
* full price.
|
|
18
|
+
* full price. The choice is given here, or read when the session starts, from `models.chat` of
|
|
19
|
+
* `agent/segmentstream.json` through the mounted extension; a session without either fails to
|
|
20
|
+
* start with a message naming the missing setting.
|
|
28
21
|
*/
|
|
29
|
-
export declare function model(choice: ModelChoice): DynamicModel;
|
|
22
|
+
export declare function model(choice: ModelChoice | (() => ModelChoice | null)): DynamicModel;
|
package/dist/extension/model.mjs
CHANGED
|
@@ -15,9 +15,13 @@ function chooseModel(input) {
|
|
|
15
15
|
return choice.default;
|
|
16
16
|
}
|
|
17
17
|
function model(choice) {
|
|
18
|
-
return defineDynamic({ events: { "session.started": (_event, context) =>
|
|
19
|
-
choice
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
return defineDynamic({ events: { "session.started": (_event, context) => {
|
|
19
|
+
const chosen = typeof choice === "function" ? choice() : choice;
|
|
20
|
+
if (chosen === null) throw new Error("segmentstream.model() has no model to choose from: set models.chat in agent/segmentstream.json or pass the choice to segmentstream.model().");
|
|
21
|
+
return chooseModel({
|
|
22
|
+
choice: chosen,
|
|
23
|
+
principal: context.session.auth.current ?? context.session.auth.initiator
|
|
24
|
+
});
|
|
25
|
+
} } });
|
|
22
26
|
}
|
|
23
27
|
export { chooseModel, model };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Mount a SegmentStream workspace's agent on eve: the ontology as tools, acting as the member who writes, and Telegram through the workspace's connection.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|