vessels 0.6.0 → 0.8.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/index.js +134 -27
- package/package.json +29 -23
- package/template/agent/README.md +111 -0
- package/template/agent/_env.example +23 -0
- package/template/agent/_gitignore +4 -0
- package/template/agent/package.json +24 -0
- package/template/agent/src/agent.ts +564 -0
- package/template/agent/src/index.ts +131 -0
- package/template/agent/src/protocol.ts +148 -0
- package/template/agent/src/role.ts +21 -0
- package/template/agent/src/store.ts +185 -0
- package/template/agent/src/tools.ts +90 -0
- package/template/agent/src/vessels-tools.ts +545 -0
- package/template/agent/tsconfig.json +17 -0
package/dist/index.js
CHANGED
|
@@ -6,9 +6,10 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, cpSync, renameSync } from "fs";
|
|
10
10
|
import { homedir } from "os";
|
|
11
|
-
import { join } from "path";
|
|
11
|
+
import { join, dirname, resolve } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
12
13
|
import * as readline from "readline/promises";
|
|
13
14
|
|
|
14
15
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
@@ -4061,7 +4062,7 @@ var InteractionTypeSchema = external_exports.enum([
|
|
|
4061
4062
|
"choice",
|
|
4062
4063
|
"checklist",
|
|
4063
4064
|
"text_input",
|
|
4064
|
-
"
|
|
4065
|
+
"questions"
|
|
4065
4066
|
]);
|
|
4066
4067
|
var ApprovalInteractionSchema = external_exports.object({
|
|
4067
4068
|
type: external_exports.literal("approval"),
|
|
@@ -4104,14 +4105,31 @@ var TextInputInteractionSchema = external_exports.object({
|
|
|
4104
4105
|
submitLabel: external_exports.string().optional(),
|
|
4105
4106
|
metadata: external_exports.record(external_exports.unknown()).optional()
|
|
4106
4107
|
});
|
|
4107
|
-
var
|
|
4108
|
-
|
|
4108
|
+
var QuestionOptionSchema = external_exports.object({
|
|
4109
|
+
id: external_exports.string().min(1),
|
|
4110
|
+
label: external_exports.string().min(1),
|
|
4111
|
+
/** Optional one-line explanation of what this option means. */
|
|
4112
|
+
description: external_exports.string().optional()
|
|
4113
|
+
});
|
|
4114
|
+
var QuestionSchema = external_exports.object({
|
|
4115
|
+
/** Stable id used to key this question's answer in the response. */
|
|
4116
|
+
id: external_exports.string().min(1),
|
|
4117
|
+
/** The question text shown to the human. */
|
|
4118
|
+
question: external_exports.string().min(1),
|
|
4119
|
+
/** Optional short chip label (≤12 chars) — e.g. "Date", "Guests". */
|
|
4120
|
+
header: external_exports.string().max(24).optional(),
|
|
4121
|
+
options: external_exports.array(QuestionOptionSchema).min(2).max(4),
|
|
4122
|
+
/** Allow selecting more than one option (checkboxes instead of radios). */
|
|
4123
|
+
multiSelect: external_exports.boolean().optional(),
|
|
4124
|
+
/** Offer a free-text "Other" field alongside the options (default true). */
|
|
4125
|
+
allowOther: external_exports.boolean().optional()
|
|
4126
|
+
});
|
|
4127
|
+
var QuestionsInteractionSchema = external_exports.object({
|
|
4128
|
+
type: external_exports.literal("questions"),
|
|
4129
|
+
/** Overall heading / context for the batch (the surface prompt). */
|
|
4109
4130
|
prompt: external_exports.string().min(1),
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
approveLabel: external_exports.string().optional(),
|
|
4113
|
-
rejectLabel: external_exports.string().optional(),
|
|
4114
|
-
reasonRequiredOnReject: external_exports.boolean().optional(),
|
|
4131
|
+
questions: external_exports.array(QuestionSchema).min(1).max(4),
|
|
4132
|
+
submitLabel: external_exports.string().optional(),
|
|
4115
4133
|
metadata: external_exports.record(external_exports.unknown()).optional()
|
|
4116
4134
|
});
|
|
4117
4135
|
var InteractionSchema = external_exports.discriminatedUnion("type", [
|
|
@@ -4119,7 +4137,7 @@ var InteractionSchema = external_exports.discriminatedUnion("type", [
|
|
|
4119
4137
|
ChoiceInteractionSchema,
|
|
4120
4138
|
ChecklistInteractionSchema,
|
|
4121
4139
|
TextInputInteractionSchema,
|
|
4122
|
-
|
|
4140
|
+
QuestionsInteractionSchema
|
|
4123
4141
|
]);
|
|
4124
4142
|
var AgentActivityTypeSchema = external_exports.enum(["thinking", "searching", "tool_use", "browsing", "processing"]);
|
|
4125
4143
|
var AgentTodoStatusSchema = external_exports.enum(["pending", "in_progress", "done"]);
|
|
@@ -4127,19 +4145,24 @@ var AgentTodoInputSchema = external_exports.object({
|
|
|
4127
4145
|
label: external_exports.string().min(1).max(200),
|
|
4128
4146
|
status: AgentTodoStatusSchema.optional()
|
|
4129
4147
|
});
|
|
4148
|
+
var AgentActivityStatusInputSchema = external_exports.enum(["working", "awaiting_input"]);
|
|
4130
4149
|
var AgentActivitySchema = external_exports.object({
|
|
4131
4150
|
type: AgentActivityTypeSchema.optional(),
|
|
4132
4151
|
label: external_exports.string().max(200).optional(),
|
|
4133
|
-
todos: external_exports.array(AgentTodoInputSchema).max(50).optional()
|
|
4134
|
-
|
|
4135
|
-
|
|
4152
|
+
todos: external_exports.array(AgentTodoInputSchema).max(50).optional(),
|
|
4153
|
+
status: AgentActivityStatusInputSchema.optional()
|
|
4154
|
+
}).refine((d) => d.type != null || d.todos != null || d.status != null, {
|
|
4155
|
+
message: "agentActivity requires `type` (a step), `todos` (a plan), or `status`"
|
|
4136
4156
|
});
|
|
4137
4157
|
var CardFieldSchema = external_exports.object({
|
|
4138
4158
|
label: external_exports.string().min(1),
|
|
4139
4159
|
value: external_exports.string()
|
|
4140
4160
|
});
|
|
4141
4161
|
var CardSchema = external_exports.object({
|
|
4142
|
-
|
|
4162
|
+
// Optional: a glance-facts card under a surface takes its heading from the
|
|
4163
|
+
// surface `title`, so a card title is redundant there. Still allowed (e.g. a
|
|
4164
|
+
// standalone card on a bubble, or a pinned card).
|
|
4165
|
+
title: external_exports.string().min(1).optional(),
|
|
4143
4166
|
fields: external_exports.array(CardFieldSchema)
|
|
4144
4167
|
});
|
|
4145
4168
|
var AttachmentSchema = external_exports.discriminatedUnion("type", [
|
|
@@ -4147,6 +4170,8 @@ var AttachmentSchema = external_exports.discriminatedUnion("type", [
|
|
|
4147
4170
|
external_exports.object({ type: external_exports.literal("file"), url: external_exports.string().url(), filename: external_exports.string().optional() })
|
|
4148
4171
|
]);
|
|
4149
4172
|
var VesselStatusSchema = external_exports.enum(["active", "waiting", "resolved"]);
|
|
4173
|
+
var DisplaySchema = external_exports.enum(["bubble", "document"]);
|
|
4174
|
+
var KindSchema = external_exports.enum(["bubble", "surface"]);
|
|
4150
4175
|
var PushPayloadSchema = external_exports.object({
|
|
4151
4176
|
message: external_exports.string().min(1).max(1e4).optional(),
|
|
4152
4177
|
vessel: external_exports.string().optional(),
|
|
@@ -4159,13 +4184,28 @@ var PushPayloadSchema = external_exports.object({
|
|
|
4159
4184
|
"metadata exceeds 16KB limit"
|
|
4160
4185
|
).optional(),
|
|
4161
4186
|
pinCard: CardSchema.nullable().optional(),
|
|
4187
|
+
/** @deprecated `waiting` is now system-derived from the message's interaction; you don't set status. Still accepted for back-compat. */
|
|
4162
4188
|
vesselStatus: VesselStatusSchema.optional(),
|
|
4163
4189
|
labels: external_exports.array(external_exports.string().min(1).max(50)).max(10).optional(),
|
|
4164
4190
|
attachments: external_exports.array(AttachmentSchema).max(10).optional(),
|
|
4165
4191
|
suggestions: external_exports.array(external_exports.string().min(1).max(500)).max(5).optional(),
|
|
4166
|
-
agentActivity: AgentActivitySchema.optional()
|
|
4167
|
-
|
|
4168
|
-
|
|
4192
|
+
agentActivity: AgentActivitySchema.optional(),
|
|
4193
|
+
/**
|
|
4194
|
+
* Live token-stream buffer — an ephemeral monospace block the human watches
|
|
4195
|
+
* fill in real time (set it on the message you create, then keep replacing it
|
|
4196
|
+
* via `PATCH /messages/:id`, and clear it with `null` when done). It is a live
|
|
4197
|
+
* window, not a transcript: send the tail you want shown (the SDK trims to the
|
|
4198
|
+
* last 8000 chars). Plaintext, like agentActivity. Vanishes when cleared.
|
|
4199
|
+
*/
|
|
4200
|
+
tokenStream: external_exports.string().max(8e3).optional(),
|
|
4201
|
+
/** Bubble (chat) vs surface (composed artifact). Defaults from interaction/card. */
|
|
4202
|
+
kind: KindSchema.optional(),
|
|
4203
|
+
/** Surface heading. Ignored on bubbles. */
|
|
4204
|
+
title: external_exports.string().max(200).optional(),
|
|
4205
|
+
/** @deprecated legacy presentation hint — use `kind`. 'document' → surface. */
|
|
4206
|
+
display: DisplaySchema.optional()
|
|
4207
|
+
}).refine((d) => d.message || d.agentActivity || d.tokenStream, {
|
|
4208
|
+
message: "One of message, agentActivity, or tokenStream is required"
|
|
4169
4209
|
});
|
|
4170
4210
|
var PushManyPayloadSchema = external_exports.object({
|
|
4171
4211
|
vessels: external_exports.array(external_exports.string().min(1)).min(1).max(100),
|
|
@@ -4174,20 +4214,33 @@ var PushManyPayloadSchema = external_exports.object({
|
|
|
4174
4214
|
card: CardSchema.optional(),
|
|
4175
4215
|
interaction: InteractionSchema.optional(),
|
|
4176
4216
|
pinCard: CardSchema.nullable().optional(),
|
|
4217
|
+
/** @deprecated `waiting` is now system-derived from the message's interaction; you don't set status. Still accepted for back-compat. */
|
|
4177
4218
|
vesselStatus: VesselStatusSchema.optional(),
|
|
4178
4219
|
attachments: external_exports.array(AttachmentSchema).max(10).optional(),
|
|
4179
4220
|
suggestions: external_exports.array(external_exports.string().min(1).max(500)).max(5).optional(),
|
|
4180
4221
|
metadata: external_exports.record(external_exports.unknown()).refine(
|
|
4181
4222
|
(v) => JSON.stringify(v).length < 16e3,
|
|
4182
4223
|
"metadata exceeds 16KB limit"
|
|
4183
|
-
).optional()
|
|
4224
|
+
).optional(),
|
|
4225
|
+
kind: KindSchema.optional(),
|
|
4226
|
+
title: external_exports.string().max(200).optional(),
|
|
4227
|
+
/** @deprecated use `kind`. */
|
|
4228
|
+
display: DisplaySchema.optional()
|
|
4184
4229
|
});
|
|
4185
4230
|
var MessagePatchSchema = external_exports.object({
|
|
4186
4231
|
content: external_exports.string().min(1).max(1e4).optional(),
|
|
4187
4232
|
card: CardSchema.nullable().optional(),
|
|
4188
4233
|
attachments: external_exports.array(AttachmentSchema).max(10).nullable().optional(),
|
|
4189
4234
|
suggestions: external_exports.array(external_exports.string().min(1).max(500)).max(5).nullable().optional(),
|
|
4190
|
-
agentActivity: AgentActivitySchema.nullable().optional()
|
|
4235
|
+
agentActivity: AgentActivitySchema.nullable().optional(),
|
|
4236
|
+
/** Replace the live token-stream window, or `null` to clear it (block vanishes). */
|
|
4237
|
+
tokenStream: external_exports.string().max(8e3).nullable().optional(),
|
|
4238
|
+
/** Switch kind: 'surface' = full-width artifact, 'bubble' or null = chat bubble. */
|
|
4239
|
+
kind: KindSchema.nullable().optional(),
|
|
4240
|
+
/** Update the surface heading, or `null` to clear it. */
|
|
4241
|
+
title: external_exports.string().max(200).nullable().optional(),
|
|
4242
|
+
/** @deprecated use `kind`. */
|
|
4243
|
+
display: DisplaySchema.nullable().optional()
|
|
4191
4244
|
}).refine((d) => Object.values(d).some((v) => v !== void 0), {
|
|
4192
4245
|
message: "At least one field required"
|
|
4193
4246
|
});
|
|
@@ -4205,16 +4258,20 @@ var ChecklistResponseSchema = external_exports.object({
|
|
|
4205
4258
|
var TextInputResponseSchema = external_exports.object({
|
|
4206
4259
|
text: external_exports.string()
|
|
4207
4260
|
});
|
|
4208
|
-
var
|
|
4209
|
-
|
|
4210
|
-
|
|
4261
|
+
var QuestionAnswerSchema = external_exports.object({
|
|
4262
|
+
questionId: external_exports.string().min(1),
|
|
4263
|
+
selected: external_exports.array(external_exports.string()),
|
|
4264
|
+
other: external_exports.string().optional()
|
|
4265
|
+
});
|
|
4266
|
+
var QuestionsResponseSchema = external_exports.object({
|
|
4267
|
+
answers: external_exports.array(QuestionAnswerSchema)
|
|
4211
4268
|
});
|
|
4212
4269
|
var InteractionResponseSchema = external_exports.discriminatedUnion("interactionType", [
|
|
4213
4270
|
external_exports.object({ interactionType: external_exports.literal("approval"), response: ApprovalResponseSchema }),
|
|
4214
4271
|
external_exports.object({ interactionType: external_exports.literal("choice"), response: ChoiceResponseSchema }),
|
|
4215
4272
|
external_exports.object({ interactionType: external_exports.literal("checklist"), response: ChecklistResponseSchema }),
|
|
4216
4273
|
external_exports.object({ interactionType: external_exports.literal("text_input"), response: TextInputResponseSchema }),
|
|
4217
|
-
external_exports.object({ interactionType: external_exports.literal("
|
|
4274
|
+
external_exports.object({ interactionType: external_exports.literal("questions"), response: QuestionsResponseSchema })
|
|
4218
4275
|
]);
|
|
4219
4276
|
var WebhookVesselSchema = external_exports.object({
|
|
4220
4277
|
id: external_exports.string(),
|
|
@@ -4252,6 +4309,11 @@ var WebhookInteractionResponsePayloadSchema = external_exports.object({
|
|
|
4252
4309
|
vessel: WebhookVesselSchema
|
|
4253
4310
|
})
|
|
4254
4311
|
});
|
|
4312
|
+
var SupersededInteractionSchema = external_exports.object({
|
|
4313
|
+
message_id: external_exports.string(),
|
|
4314
|
+
interaction_type: InteractionTypeSchema,
|
|
4315
|
+
prompt: external_exports.string().nullable()
|
|
4316
|
+
});
|
|
4255
4317
|
var WebhookUserMessagePayloadSchema = external_exports.object({
|
|
4256
4318
|
event: external_exports.literal("message.user"),
|
|
4257
4319
|
vessel_id: external_exports.string(),
|
|
@@ -4261,7 +4323,8 @@ var WebhookUserMessagePayloadSchema = external_exports.object({
|
|
|
4261
4323
|
message_id: external_exports.string(),
|
|
4262
4324
|
content: external_exports.string(),
|
|
4263
4325
|
vessel: WebhookVesselSchema,
|
|
4264
|
-
context: external_exports.array(WebhookContextMessageSchema)
|
|
4326
|
+
context: external_exports.array(WebhookContextMessageSchema),
|
|
4327
|
+
superseded_interaction: SupersededInteractionSchema.optional()
|
|
4265
4328
|
})
|
|
4266
4329
|
});
|
|
4267
4330
|
var WebhookVesselCreatedPayloadSchema = external_exports.object({
|
|
@@ -4609,10 +4672,10 @@ async function cmdPush(args) {
|
|
|
4609
4672
|
console.log(`Message sent. vessel_id=${data.vessel_id} message_id=${data.message_id}`);
|
|
4610
4673
|
}
|
|
4611
4674
|
function readStdin() {
|
|
4612
|
-
return new Promise((
|
|
4675
|
+
return new Promise((resolve2) => {
|
|
4613
4676
|
const chunks = [];
|
|
4614
4677
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
4615
|
-
process.stdin.on("end", () =>
|
|
4678
|
+
process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
4616
4679
|
});
|
|
4617
4680
|
}
|
|
4618
4681
|
function zodIssueLines(err) {
|
|
@@ -4761,7 +4824,42 @@ async function cmdTypesToggle(enabled) {
|
|
|
4761
4824
|
});
|
|
4762
4825
|
console.log(`User-initiated vessels ${((_a = data.workspace) == null ? void 0 : _a.userVesselsEnabled) ? "enabled" : "disabled"}.`);
|
|
4763
4826
|
}
|
|
4827
|
+
function templateDir() {
|
|
4828
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
4829
|
+
return join(here, "..", "template", "agent");
|
|
4830
|
+
}
|
|
4831
|
+
async function cmdInitAgentTemplate(args) {
|
|
4832
|
+
const target = resolve(args.find((a) => !a.startsWith("--")) ?? "vessels-agent");
|
|
4833
|
+
const src = templateDir();
|
|
4834
|
+
if (!existsSync(src)) {
|
|
4835
|
+
console.error("Template not found in this CLI build. Reinstall: npm i -g vessels@latest");
|
|
4836
|
+
process.exit(1);
|
|
4837
|
+
}
|
|
4838
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
4839
|
+
console.error(`Refusing to scaffold into a non-empty directory: ${target}`);
|
|
4840
|
+
process.exit(1);
|
|
4841
|
+
}
|
|
4842
|
+
cpSync(src, target, { recursive: true });
|
|
4843
|
+
for (const [from, to] of [["_gitignore", ".gitignore"], ["_env.example", ".env.example"]]) {
|
|
4844
|
+
const p = join(target, from);
|
|
4845
|
+
if (existsSync(p)) renameSync(p, join(target, to));
|
|
4846
|
+
}
|
|
4847
|
+
console.log(`
|
|
4848
|
+
Scaffolded a Vessels agent into ${target}
|
|
4849
|
+
`);
|
|
4850
|
+
console.log("Next:\n");
|
|
4851
|
+
console.log(` cd ${target}`);
|
|
4852
|
+
console.log(" npm install");
|
|
4853
|
+
console.log(" cp .env.example .env # then fill in your keys");
|
|
4854
|
+
console.log(" npm run dev # webhook server on :3000\n");
|
|
4855
|
+
console.log("Need keys? Run `vessels init --email you@example.com` (then --otp <code>),");
|
|
4856
|
+
console.log("then point a webhook at your running server:");
|
|
4857
|
+
console.log(" vessels webhooks create --url https://<your-public-url>/\n");
|
|
4858
|
+
console.log("Edit src/role.ts (who your agent is) and src/tools.ts (its backend tools).");
|
|
4859
|
+
console.log("Full reference: https://vessels.app/llms-full.txt");
|
|
4860
|
+
}
|
|
4764
4861
|
async function cmdInit(args) {
|
|
4862
|
+
if (args.includes("--agent-template")) return cmdInitAgentTemplate(args);
|
|
4765
4863
|
const flags = parseFlags(args);
|
|
4766
4864
|
const email = flags.email || await prompt("Email: ");
|
|
4767
4865
|
if (!flags.otp) {
|
|
@@ -4842,11 +4940,20 @@ Quick setup (Claude Code / AI agents \u2014 fully non-interactive):
|
|
|
4842
4940
|
# With a webhook (optional, run once your server is deployed):
|
|
4843
4941
|
vessels init --email me@example.com --otp 847293 --webhook-url https://myapp.com/hooks/vessels
|
|
4844
4942
|
|
|
4943
|
+
Want a running agent to start from?
|
|
4944
|
+
|
|
4945
|
+
vessels init --agent-template ./my-agent # scaffolds a Vessels-native tool-loop agent
|
|
4946
|
+
|
|
4845
4947
|
Commands:
|
|
4846
4948
|
vessels init --email <email> [--otp <code>] [--name <key-name>] [--webhook-url <url>]
|
|
4847
4949
|
First-time setup: account + API key (+ webhook). Two-step: run once to send OTP,
|
|
4848
4950
|
re-run with --otp to complete. Prints copy-ready .env entries.
|
|
4849
4951
|
|
|
4952
|
+
vessels init --agent-template [dir]
|
|
4953
|
+
Scaffold a runnable, Vessels-native starter agent into <dir> (default ./vessels-agent):
|
|
4954
|
+
a Claude tool-loop wired to Vessels, with stub tools to swap for your real backend.
|
|
4955
|
+
Edit src/role.ts and src/tools.ts, then npm install and npm run dev.
|
|
4956
|
+
|
|
4850
4957
|
vessels login [--email <email>] [--otp <code>]
|
|
4851
4958
|
vessels logout
|
|
4852
4959
|
vessels whoami
|
package/package.json
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
"name": "vessels",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Vessels CLI — manage your agent communication layer from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vessels": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"vessels",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsup": "^8.5.1",
|
|
26
|
+
"typescript": "^5",
|
|
27
|
+
"@types/node": "^25.5.2",
|
|
28
|
+
"@vessels/types": "workspace:*"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {}
|
|
25
31
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Your Vessels agent
|
|
2
|
+
|
|
3
|
+
A working, Vessels-native agent. It runs on **your** box, keeps its own state where **you**
|
|
4
|
+
tell it to, and talks to Vessels over HTTP purely to show your human operator what's happening
|
|
5
|
+
and collect their decisions. Swap the two stub tools for your real backend and you have a real
|
|
6
|
+
agent — a booking manager, a support triager, a contracts analyst, a stock-desk assistant.
|
|
7
|
+
|
|
8
|
+
> **Vessels is the view/interaction layer — your agent owns its data.** Vessels never holds your
|
|
9
|
+
> agent's memory, state, or business data. It carries the human-facing messages you send and the
|
|
10
|
+
> answers you get back. Your conversation history and your locks live in *your* store (see
|
|
11
|
+
> `src/store.ts`).
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
cp .env.example .env # then fill in the three required keys
|
|
18
|
+
npm run dev # starts the webhook server on :3000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Don't have keys yet? Install the CLI and run setup:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx vessels init --email you@example.com # emails a code
|
|
25
|
+
npx vessels init --email you@example.com --otp 123456
|
|
26
|
+
# → prints VESSELS_API_KEY. Then point a webhook at your running server:
|
|
27
|
+
npx vessels webhooks create --url https://<your-public-url>/ # → prints VESSELS_WEBHOOK_SECRET
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For local development, expose `:3000` with a tunnel (ngrok, cloudflared, `vessels`-friendly
|
|
31
|
+
host) and use that public URL as the webhook URL. Then open a vessel in the Vessels app / mobile
|
|
32
|
+
and message it — your agent answers.
|
|
33
|
+
|
|
34
|
+
## The two files you edit
|
|
35
|
+
|
|
36
|
+
| File | What it is |
|
|
37
|
+
|------|-----------|
|
|
38
|
+
| **`src/role.ts`** | WHO your agent is and WHAT it does — one short paragraph. The only place the engine learns your domain. |
|
|
39
|
+
| **`src/tools.ts`** | Your backend tools. Each is `{ definition, handler, narrate? }`. Replace the two stubs. |
|
|
40
|
+
|
|
41
|
+
Everything else is the **engine** and you rarely touch it:
|
|
42
|
+
|
|
43
|
+
| File | What it is |
|
|
44
|
+
|------|-----------|
|
|
45
|
+
| `src/protocol.ts` | The domain-free system prompt — how to talk to Vessels (bubbles vs surfaces, lead with a reply, plan before working, ask the human as a structured tool call). |
|
|
46
|
+
| `src/vessels-tools.ts` | The fixed Vessels control tools + payload sanitisers + interaction mapping. |
|
|
47
|
+
| `src/agent.ts` | One turn: the Claude tool loop, the live working card, the forced-ending safety net. |
|
|
48
|
+
| `src/store.ts` | `AgentStore` — your conversation state + per-vessel lock. |
|
|
49
|
+
| `src/index.ts` | The webhook server (verify → ACK → run the turn). |
|
|
50
|
+
|
|
51
|
+
## How a turn works
|
|
52
|
+
|
|
53
|
+
1. The human acts in Vessels → Vessels POSTs a webhook to `src/index.ts`.
|
|
54
|
+
2. The server verifies the signature, **ACKs 200 immediately**, and runs the turn in the background.
|
|
55
|
+
3. The engine leads with a one-line reply, opens a live working card, plans, calls **your** tools,
|
|
56
|
+
and ends with exactly one finishing tool — a message (`finish`) or a human decision
|
|
57
|
+
(`request_approval` / `choice` / `checklist` / `text`).
|
|
58
|
+
4. When the human answers, Vessels sends another webhook and the loop continues — the engine loads
|
|
59
|
+
the prior conversation from your store, so the agent picks up exactly where it left off.
|
|
60
|
+
|
|
61
|
+
**Contacting the human is a tool call.** That's the headline pattern: the model doesn't print to a
|
|
62
|
+
human, it raises `request_approval`/`choice`/… and the turn pauses until they answer. Human-in-the-loop
|
|
63
|
+
is native, not bolted on.
|
|
64
|
+
|
|
65
|
+
## State & durability
|
|
66
|
+
|
|
67
|
+
By default the agent uses **`MemoryStore`** — zero infrastructure, correct for a single long-lived
|
|
68
|
+
process, state resets on restart. To make it durable, set `DATABASE_URL` and it switches to
|
|
69
|
+
**`PostgresStore`**, which:
|
|
70
|
+
|
|
71
|
+
- persists each vessel's conversation history, and
|
|
72
|
+
- holds a **cross-process per-vessel lock** (so two webhooks for the same vessel can't run
|
|
73
|
+
interleaved turns) — the thing you need once you run more than one instance.
|
|
74
|
+
|
|
75
|
+
`PostgresStore` self-provisions its tables on boot (`CREATE TABLE IF NOT EXISTS`) — there's no
|
|
76
|
+
migration to run. Want Redis, Dynamo, or your own DB? Implement the `AgentStore` interface in
|
|
77
|
+
`src/store.ts`; nothing else changes.
|
|
78
|
+
|
|
79
|
+
## Deploying
|
|
80
|
+
|
|
81
|
+
This template is a long-lived Node process, so background work after the 200 ACK simply finishes.
|
|
82
|
+
If you deploy to **serverless** (Lambda / Vercel / Workers), the process can freeze right after the
|
|
83
|
+
response — there you must `await` the turn before responding, or use the platform's background
|
|
84
|
+
primitive (e.g. `waitUntil`) so the turn isn't killed mid-flight. The `parseWebhookEvent → ACK →
|
|
85
|
+
runTurn` shape stays the same; only the server wrapper changes.
|
|
86
|
+
|
|
87
|
+
## The full Vessels surface
|
|
88
|
+
|
|
89
|
+
The engine already exposes the breadth of Vessels to the model, so your agent can use all of it:
|
|
90
|
+
chat **bubbles** and full-width **surfaces**; all four **interactions** (`approval` / `choice` /
|
|
91
|
+
`checklist` / `text`, with labels, `allowCustom`, `minSelections`, `reasonRequired`, and
|
|
92
|
+
**metadata** that rides back to you for routing); the live **working card** with a ticking **plan**,
|
|
93
|
+
auto-narrated **steps**, and **token streaming**; **pinned cards**, **labels**, **attachments**
|
|
94
|
+
(images/files you host), **preview links**, **suggested replies**, vessel **naming/renaming**, and
|
|
95
|
+
user-initiated vessel **types**. Idempotency keys, the per-vessel lock, and the resolve-before-ask
|
|
96
|
+
discipline are handled for you. (It deliberately keeps the notification rule too: the working card
|
|
97
|
+
stays silent, only your reply and outcome buzz the human's phone.)
|
|
98
|
+
|
|
99
|
+
A few Vessels features live on **your backend**, not inside a turn — call them on the `vessels`
|
|
100
|
+
SDK directly:
|
|
101
|
+
|
|
102
|
+
- `vessels.pushMany({ vessels: [...], message, interaction })` — broadcast the same message/decision
|
|
103
|
+
to many vessels at once (e.g. "course closed Saturday" to every affected booking).
|
|
104
|
+
- `vessels.getMessages({ vessel })` — re-read a vessel's human-facing history to reconcile a
|
|
105
|
+
stateless/restarted worker (not your memory — that's your store).
|
|
106
|
+
- `vessels.validatePush(payload)` — check a payload against the server schema without sending.
|
|
107
|
+
|
|
108
|
+
## Learn more
|
|
109
|
+
|
|
110
|
+
- Full Vessels docs (API, SDK, webhooks, interaction types): <https://vessels.app/llms-full.txt>
|
|
111
|
+
- The SDK: `vessels-sdk` on npm.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ── Required ────────────────────────────────────────────────────────────────
|
|
2
|
+
# A vsl_ API key for your workspace (push back to Vessels). From `vessels init` or Settings.
|
|
3
|
+
VESSELS_API_KEY=vsl_...
|
|
4
|
+
# The secret printed when you created your webhook (`vessels webhooks create` or Settings).
|
|
5
|
+
VESSELS_WEBHOOK_SECRET=whsec_...
|
|
6
|
+
# Your Anthropic API key (drives the agent — this is YOUR LLM key, on YOUR bill).
|
|
7
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
8
|
+
|
|
9
|
+
# ── Optional ────────────────────────────────────────────────────────────────
|
|
10
|
+
# Claude model (default: claude-sonnet-4-6).
|
|
11
|
+
# ANTHROPIC_MODEL=claude-sonnet-4-6
|
|
12
|
+
# Port the webhook server listens on (default: 3000).
|
|
13
|
+
# PORT=3000
|
|
14
|
+
# Override the Vessels base URL (default: https://vessels.app).
|
|
15
|
+
# VESSELS_BASE_URL=https://vessels.app
|
|
16
|
+
# Set DEBUG=1 to log the turn flow (model calls, tools, pushes).
|
|
17
|
+
# DEBUG=1
|
|
18
|
+
|
|
19
|
+
# ── Durability upgrade (optional) ─────────────────────────────────────────────
|
|
20
|
+
# Set DATABASE_URL and the agent uses PostgresStore: durable conversation state +
|
|
21
|
+
# a cross-process lock. It self-provisions its tables on boot (no migration to run).
|
|
22
|
+
# Leave it unset to use the zero-infra in-memory default.
|
|
23
|
+
# DATABASE_URL=postgres://user:pass@host:5432/dbname
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vessels-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "A Vessels-native agent — talks to its human operator through Vessels.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch src/index.ts",
|
|
9
|
+
"start": "tsx src/index.ts",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@anthropic-ai/sdk": "^0.85.0",
|
|
14
|
+
"dotenv": "^16.4.5",
|
|
15
|
+
"pg": "^8.13.0",
|
|
16
|
+
"vessels-sdk": "^0.14.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.10.0",
|
|
20
|
+
"@types/pg": "^8.11.10",
|
|
21
|
+
"tsx": "^4.19.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
}
|
|
24
|
+
}
|