@flow-engineer/flow 0.2.0 → 0.2.2
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/LICENSE +202 -21
- package/README.md +190 -17
- package/coding-agents/auth-skill.md +137 -0
- package/coding-agents/context-skill.md +94 -0
- package/coding-agents/skill.md +245 -11
- package/coding-agents/test-skill.md +13 -11
- package/coding-agents/tools-skill.md +78 -1
- package/dist/cli/src/auth.d.ts +11 -0
- package/dist/cli/src/auth.js +147 -0
- package/dist/cli/src/commands.d.ts +119 -0
- package/dist/cli/src/commands.js +212 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +5 -0
- package/dist/cli/src/run.d.ts +28 -0
- package/dist/cli/src/run.js +523 -0
- package/dist/cli/src/terminal.d.ts +3 -0
- package/dist/cli/src/terminal.js +135 -0
- package/dist/flow-messaging/src/client.d.ts +7 -1
- package/dist/flow-messaging/src/client.js +13 -1
- package/dist/flow-messaging/src/index.d.ts +38 -3
- package/dist/flow-messaging/src/index.js +242 -40
- package/dist/mcp/src/client.d.ts +54 -0
- package/dist/mcp/src/client.js +175 -0
- package/dist/mcp/src/describe.d.ts +41 -0
- package/dist/mcp/src/describe.js +110 -0
- package/dist/mcp/src/index.d.ts +4 -0
- package/dist/mcp/src/index.js +7 -0
- package/dist/mcp/src/server.d.ts +70 -0
- package/dist/mcp/src/server.js +252 -0
- package/dist/mcp/src/terminal.d.ts +24 -0
- package/dist/mcp/src/terminal.js +54 -0
- package/dist/simulators/src/index.d.ts +1 -1
- package/dist/simulators/src/page.html +267 -49
- package/dist/simulators/src/server.d.ts +27 -0
- package/dist/simulators/src/server.js +264 -40
- package/dist/src/agent.d.ts +69 -3
- package/dist/src/agent.js +162 -8
- package/dist/src/channels.d.ts +2 -0
- package/dist/src/conversation.d.ts +16 -1
- package/dist/src/conversation.js +140 -12
- package/dist/src/events.d.ts +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/model.d.ts +27 -0
- package/dist/src/model.js +85 -13
- package/dist/src/names.d.ts +5 -0
- package/dist/src/names.js +5 -0
- package/dist/src/skills.d.ts +4 -0
- package/dist/src/skills.js +23 -0
- package/dist/src/store.d.ts +2 -0
- package/dist/src/store.js +18 -0
- package/dist/src/tools.d.ts +69 -0
- package/dist/src/tools.js +17 -1
- package/dist/src/traces.js +32 -4
- package/dist/src/types.d.ts +2 -0
- package/dist/testing/src/account.d.ts +4 -0
- package/dist/testing/src/account.js +891 -0
- package/dist/testing/src/chat.d.ts +3 -0
- package/dist/testing/src/chat.js +125 -0
- package/dist/testing/src/check.d.ts +4 -0
- package/dist/testing/src/check.js +90 -0
- package/dist/testing/src/chinwag.js +1 -1
- package/dist/testing/src/cli.js +105 -18
- package/dist/testing/src/client.d.ts +2 -0
- package/dist/testing/src/client.js +4 -3
- package/dist/testing/src/flow-login.d.ts +176 -0
- package/dist/testing/src/flow-login.js +274 -0
- package/dist/testing/src/new.d.ts +15 -2
- package/dist/testing/src/new.js +80 -21
- package/dist/testing/src/plugin.d.ts +4 -0
- package/dist/testing/src/plugin.js +116 -0
- package/docs/agents.md +160 -0
- package/examples/cafe/src/agent.ts +6 -2
- package/examples/cafe/src/channels.ts +20 -0
- package/examples/cafe/src/cli.ts +24 -0
- package/examples/cafe/src/main.ts +17 -3
- package/examples/cafe/src/mcp.ts +22 -0
- package/examples/cafe/src/tools.ts +13 -0
- package/flow-messaging/README.md +66 -27
- package/package.json +30 -2
- package/spec/agent-endpoint.openapi.yaml +2 -2
- package/spec/nudges.openapi.yaml +2 -2
package/dist/src/index.d.ts
CHANGED
|
@@ -5,10 +5,10 @@ export { nudgeHandler, peopleHandler, type NudgeHandlerOptions, type PeopleHandl
|
|
|
5
5
|
export { sqlite, type Database } from "./db.ts";
|
|
6
6
|
export { postgres, type PostgresPool } from "./postgres.ts";
|
|
7
7
|
export { jsonlSink, type EventSink, type FlowEvent, type ChinwagEvent } from "./events.ts";
|
|
8
|
-
export { openAICompatible, type OpenAICompatibleOptions } from "./model.ts";
|
|
8
|
+
export { flowModel, flowModelURL, openAICompatible, type OpenAICompatibleOptions } from "./model.ts";
|
|
9
9
|
export { Store, type StoreApi } from "./store.ts";
|
|
10
10
|
export type { AgentFiles } from "./skills.ts";
|
|
11
11
|
export type { TracesOptions } from "./traces.ts";
|
|
12
|
-
export { tool, toSpec, type Tool, type ToolContext } from "./tools.ts";
|
|
12
|
+
export { tool, toSpec, type ChatAuth, type ChatAuthContext, type CredentialProvider, type Credentials, type SignIn, type Tool, type ToolContext, type ToolHints } from "./tools.ts";
|
|
13
13
|
export type { Attachment, ChannelInfo, ChatMessage, Deliver, Incoming, ListRow, Media, Model, ModelRequest, ModelResponse, Outbound, PersonMessage, Template, TemplateButton, TemplateDefinition, ToolCall, ToolSpec, Transcribe } from "./types.ts";
|
|
14
14
|
export { z } from "zod";
|
package/dist/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { nudgeHandler, peopleHandler } from "./http.js";
|
|
|
5
5
|
export { sqlite } from "./db.js";
|
|
6
6
|
export { postgres } from "./postgres.js";
|
|
7
7
|
export { jsonlSink } from "./events.js";
|
|
8
|
-
export { openAICompatible } from "./model.js";
|
|
8
|
+
export { flowModel, flowModelURL, openAICompatible } from "./model.js";
|
|
9
9
|
export { Store } from "./store.js";
|
|
10
10
|
export { tool, toSpec } from "./tools.js";
|
|
11
11
|
export { z } from "zod";
|
package/dist/src/model.d.ts
CHANGED
|
@@ -12,6 +12,27 @@ export interface OpenAICompatibleOptions {
|
|
|
12
12
|
/** Its context window in tokens, so the history sent is kept within it. */
|
|
13
13
|
contextTokens?: number;
|
|
14
14
|
}
|
|
15
|
+
/** Flow's OpenAI-compatible model API, reached with FLOW_KEY. */
|
|
16
|
+
export declare const FLOW_MODEL_URL = "https://flow.engineer/llm/framework/v1";
|
|
17
|
+
/**
|
|
18
|
+
* Where FLOW_KEY is sent for the model: FLOW_MODEL_URL when set, else the origin of the Flow server the CLI
|
|
19
|
+
* signs in to (FLOW_FRAMEWORK_LOGIN_URL) with /llm/framework/v1, else flow.engineer's.
|
|
20
|
+
*/
|
|
21
|
+
export declare function flowModelURL(env?: Record<string, string | undefined>): string;
|
|
22
|
+
/** The .env names a model key may be on, in the order they are read. */
|
|
23
|
+
export declare const MODEL_KEY_NAMES: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* What to tell the owner when no model key is set, said the same way by `chat`, `npm start`,
|
|
26
|
+
* `init`, `setup` and `link`: one sentence, and `next` as a command (`nextLine` is how it is printed).
|
|
27
|
+
*/
|
|
28
|
+
export declare const NO_MODEL_KEY: {
|
|
29
|
+
readonly code: "no_model_key";
|
|
30
|
+
readonly message: `This agent has no model key: npx @flow-engineer/flow setup writes FLOW_KEY, the agent's Flow key (with the account's free trial model credit), into .env in the agent's folder, or put your own on one of the lines ${string} or ANTHROPIC_API_KEY= (an OpenRouter key is made at https://openrouter.ai/keys).` | `This agent has no model key: npx @flow-engineer/flow setup writes FLOW_KEY, the agent's Flow key (with the account's free trial model credit), into .env in the agent's folder, or put your own on one of the lines ${string} or FLOW_KEY= (an OpenRouter key is made at https://openrouter.ai/keys).` | `This agent has no model key: npx @flow-engineer/flow setup writes FLOW_KEY, the agent's Flow key (with the account's free trial model credit), into .env in the agent's folder, or put your own on one of the lines ${string} or GEMINI_API_KEY= (an OpenRouter key is made at https://openrouter.ai/keys).` | `This agent has no model key: npx @flow-engineer/flow setup writes FLOW_KEY, the agent's Flow key (with the account's free trial model credit), into .env in the agent's folder, or put your own on one of the lines ${string} or OPENAI_API_KEY= (an OpenRouter key is made at https://openrouter.ai/keys).` | `This agent has no model key: npx @flow-engineer/flow setup writes FLOW_KEY, the agent's Flow key (with the account's free trial model credit), into .env in the agent's folder, or put your own on one of the lines ${string} or OPENROUTER_API_KEY= (an OpenRouter key is made at https://openrouter.ai/keys).`;
|
|
31
|
+
readonly next: "npx @flow-engineer/flow setup";
|
|
32
|
+
readonly nextLine: "Next: run npx @flow-engineer/flow setup to get Flow's free trial credit, or add your own key.";
|
|
33
|
+
};
|
|
34
|
+
/** True when the environment names a model key with a value. */
|
|
35
|
+
export declare const hasModelKey: (env: Record<string, string | undefined>) => boolean;
|
|
15
36
|
/** Which key, address and model an omitted `apiKey` / `baseURL` resolve to. */
|
|
16
37
|
export declare function modelFromKeys(env: Record<string, string | undefined>): {
|
|
17
38
|
apiKey?: string;
|
|
@@ -19,3 +40,9 @@ export declare function modelFromKeys(env: Record<string, string | undefined>):
|
|
|
19
40
|
model: string;
|
|
20
41
|
};
|
|
21
42
|
export declare function openAICompatible(options?: OpenAICompatibleOptions): Model;
|
|
43
|
+
/**
|
|
44
|
+
* The model through Flow: FLOW_KEY (written by `flow setup`) at {@link flowModelURL}, the
|
|
45
|
+
* account's Flow trial credit paying. `model` is one Flow offers (default anthropic/claude-haiku-5.5;
|
|
46
|
+
* GET /models lists them). With no key it fails when made, with a next step.
|
|
47
|
+
*/
|
|
48
|
+
export declare function flowModel(options?: Omit<OpenAICompatibleOptions, "baseURL">): Model;
|
package/dist/src/model.js
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
// Models over the OpenAI-compatible chat API: OpenRouter by default; OpenAI, Gemini, Anthropic or a
|
|
2
|
-
// local server by changing the base URL
|
|
2
|
+
// local server by changing the base URL; or Flow, with the agent's FLOW_KEY that `flow setup` writes,
|
|
3
|
+
// which needs no provider key and spends the account's Flow trial credit.
|
|
3
4
|
import OpenAI from "openai";
|
|
4
|
-
|
|
5
|
+
import { setting } from "./names.js";
|
|
6
|
+
/** Flow's OpenAI-compatible model API, reached with FLOW_KEY. */
|
|
7
|
+
export const FLOW_MODEL_URL = "https://flow.engineer/llm/framework/v1";
|
|
8
|
+
/**
|
|
9
|
+
* Where FLOW_KEY is sent for the model: FLOW_MODEL_URL when set, else the origin of the Flow server the CLI
|
|
10
|
+
* signs in to (FLOW_FRAMEWORK_LOGIN_URL) with /llm/framework/v1, else flow.engineer's.
|
|
11
|
+
*/
|
|
12
|
+
export function flowModelURL(env = process.env) {
|
|
13
|
+
const named = env.FLOW_MODEL_URL?.trim();
|
|
14
|
+
if (named)
|
|
15
|
+
return named;
|
|
16
|
+
const login = URL.parse(setting("LOGIN_URL", env)?.trim() ?? "");
|
|
17
|
+
return login ? `${login.origin}/llm/framework/v1` : FLOW_MODEL_URL;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The first of these with a value is the agent's model. `MODEL` overrides that provider's default id.
|
|
21
|
+
* Flow's key is last, so a provider key the owner adds wins over the FLOW_KEY setup wrote.
|
|
22
|
+
*/
|
|
5
23
|
const KEYS = [
|
|
6
24
|
{ env: "OPENROUTER_API_KEY", baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-haiku-5.5" },
|
|
7
25
|
{ env: "OPENAI_API_KEY", baseURL: "https://api.openai.com/v1", model: "gpt-6-luna" },
|
|
8
26
|
{ env: "ANTHROPIC_API_KEY", baseURL: "https://api.anthropic.com/v1/", model: "claude-haiku-5-5" },
|
|
9
27
|
{ env: "GEMINI_API_KEY", baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/", model: "gemini-3.8-flash" },
|
|
28
|
+
{ env: "FLOW_KEY", baseURL: FLOW_MODEL_URL, model: "anthropic/claude-haiku-5.5" },
|
|
10
29
|
];
|
|
30
|
+
/** The .env names a model key may be on, in the order they are read. */
|
|
31
|
+
export const MODEL_KEY_NAMES = KEYS.map((k) => k.env);
|
|
32
|
+
/**
|
|
33
|
+
* What to tell the owner when no model key is set, said the same way by `chat`, `npm start`,
|
|
34
|
+
* `init`, `setup` and `link`: one sentence, and `next` as a command (`nextLine` is how it is printed).
|
|
35
|
+
*/
|
|
36
|
+
export const NO_MODEL_KEY = {
|
|
37
|
+
code: "no_model_key",
|
|
38
|
+
message: `This agent has no model key: npx @flow-engineer/flow setup writes FLOW_KEY, the agent's Flow key (with the account's free trial model credit), into .env in the agent's folder, or put your own on one of the lines ${KEYS.slice(0, -2).map((k) => `${k.env}=`).join(", ")} or ${KEYS.at(-2).env}= (an OpenRouter key is made at https://openrouter.ai/keys).`,
|
|
39
|
+
next: "npx @flow-engineer/flow setup",
|
|
40
|
+
nextLine: "Next: run npx @flow-engineer/flow setup to get Flow's free trial credit, or add your own key.",
|
|
41
|
+
};
|
|
42
|
+
/** True when the environment names a model key with a value. */
|
|
43
|
+
export const hasModelKey = (env) => KEYS.some((k) => Boolean(env[k.env]?.trim()));
|
|
11
44
|
/** Which key, address and model an omitted `apiKey` / `baseURL` resolve to. */
|
|
12
45
|
export function modelFromKeys(env) {
|
|
13
46
|
const named = env.MODEL?.trim();
|
|
14
47
|
for (const provider of KEYS) {
|
|
15
48
|
const apiKey = env[provider.env]?.trim();
|
|
16
49
|
if (apiKey)
|
|
17
|
-
return { apiKey, baseURL: provider.baseURL, model: named || provider.model };
|
|
50
|
+
return { apiKey, baseURL: provider.env === "FLOW_KEY" ? flowModelURL(env) : provider.baseURL, model: named || provider.model };
|
|
18
51
|
}
|
|
19
52
|
return { baseURL: KEYS[0].baseURL, model: named || KEYS[0].model };
|
|
20
53
|
}
|
|
@@ -24,8 +57,9 @@ export function openAICompatible(options = {}) {
|
|
|
24
57
|
const baseURL = options.baseURL ?? fromEnv?.baseURL ?? KEYS[0].baseURL;
|
|
25
58
|
const apiKey = options.apiKey ?? fromEnv?.apiKey ?? (process.env.OPENROUTER_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim());
|
|
26
59
|
// A server at a base URL you name (a local one) may need no key; the client still sends one.
|
|
60
|
+
// Code that builds its model itself can also pass apiKey, or baseURL for a server that needs none.
|
|
27
61
|
if (!apiKey && options.baseURL === undefined) {
|
|
28
|
-
throw new Error(
|
|
62
|
+
throw Object.assign(new Error(`${NO_MODEL_KEY.message}\n${NO_MODEL_KEY.nextLine}`), { code: NO_MODEL_KEY.code, next: NO_MODEL_KEY.next });
|
|
29
63
|
}
|
|
30
64
|
const client = new OpenAI({
|
|
31
65
|
apiKey: apiKey || "none",
|
|
@@ -33,18 +67,26 @@ export function openAICompatible(options = {}) {
|
|
|
33
67
|
maxRetries: 1,
|
|
34
68
|
});
|
|
35
69
|
const model = options.model?.trim() || fromEnv?.model || KEYS[0].model;
|
|
70
|
+
// Flow reads PDFs for every model it offers, as OpenRouter (behind it) does.
|
|
71
|
+
const readsPdfs = baseURL.startsWith("https://openrouter.ai/") || baseURL === flowModelURL(process.env);
|
|
36
72
|
return {
|
|
37
|
-
reads: { images: options.images ?? true, pdfs: options.pdfs ??
|
|
73
|
+
reads: { images: options.images ?? true, pdfs: options.pdfs ?? readsPdfs },
|
|
38
74
|
...(options.contextTokens ? { contextTokens: options.contextTokens } : {}),
|
|
39
75
|
async complete(request) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
let completion;
|
|
77
|
+
try {
|
|
78
|
+
completion = await client.chat.completions.create({
|
|
79
|
+
model,
|
|
80
|
+
temperature: options.temperature,
|
|
81
|
+
messages: [{ role: "system", content: request.system }, ...request.messages.map(toWire)],
|
|
82
|
+
...(request.tools.length
|
|
83
|
+
? { tools: request.tools.map((t) => ({ type: "function", function: t })) }
|
|
84
|
+
: {}),
|
|
85
|
+
}, { signal: request.signal });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw flowError(error) ?? error;
|
|
89
|
+
}
|
|
48
90
|
const message = completion.choices[0]?.message;
|
|
49
91
|
return {
|
|
50
92
|
text: message?.content?.trim() || null,
|
|
@@ -57,6 +99,36 @@ export function openAICompatible(options = {}) {
|
|
|
57
99
|
},
|
|
58
100
|
};
|
|
59
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The model through Flow: FLOW_KEY (written by `flow setup`) at {@link flowModelURL}, the
|
|
104
|
+
* account's Flow trial credit paying. `model` is one Flow offers (default anthropic/claude-haiku-5.5;
|
|
105
|
+
* GET /models lists them). With no key it fails when made, with a next step.
|
|
106
|
+
*/
|
|
107
|
+
export function flowModel(options = {}) {
|
|
108
|
+
const apiKey = options.apiKey?.trim() || process.env.FLOW_KEY?.trim();
|
|
109
|
+
if (!apiKey) {
|
|
110
|
+
throw Object.assign(new Error(`FLOW_KEY is not set: npx @flow-engineer/flow setup writes it into .env in the agent's folder.\nNext: run ${NO_MODEL_KEY.next}.`), { code: NO_MODEL_KEY.code, next: NO_MODEL_KEY.next });
|
|
111
|
+
}
|
|
112
|
+
return openAICompatible({ ...options, apiKey, baseURL: flowModelURL(process.env), model: options.model?.trim() || process.env.MODEL?.trim() || KEYS.at(-1).model });
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A typed error from Flow's model API ({"error": {message, type, code, next}}: trial_credit_used,
|
|
116
|
+
* invalid_api_key, rate_limit_exceeded, ...) as an Error with its message and next step; undefined
|
|
117
|
+
* for any other error, which is thrown as it came.
|
|
118
|
+
*/
|
|
119
|
+
function flowError(error) {
|
|
120
|
+
if (!(error instanceof OpenAI.APIError))
|
|
121
|
+
return undefined;
|
|
122
|
+
const body = error.error;
|
|
123
|
+
if (typeof body?.next !== "string" || !body.next || typeof body.message !== "string")
|
|
124
|
+
return undefined;
|
|
125
|
+
const sentence = body.message.trim();
|
|
126
|
+
return Object.assign(new Error(`${sentence}\nNext: ${body.next}`, { cause: error }), {
|
|
127
|
+
code: typeof body.code === "string" ? body.code : `http_${error.status}`,
|
|
128
|
+
next: body.next,
|
|
129
|
+
status: error.status,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
60
132
|
function toWire(message) {
|
|
61
133
|
if (message.role === "tool")
|
|
62
134
|
return { role: "tool", tool_call_id: message.toolCallId, content: message.content };
|
package/dist/src/names.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/** A setting from the environment: FLOW_FRAMEWORK_<name>, else CHINWAG_<name> from before the rename. */
|
|
2
2
|
export declare function setting(name: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* The command to print in a next step. `npx flow` would fetch another npm package wherever this one is
|
|
5
|
+
* not installed, so a hint always names the package.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RUN = "npx @flow-engineer/flow";
|
|
3
8
|
/** An agent's results folder, in the agent's folder: written here, and git-ignored by `flow new`. */
|
|
4
9
|
export declare const RESULTS_DIR = ".flow";
|
|
5
10
|
/** The results folder from before the rename, still read until a run writes {@link RESULTS_DIR}. */
|
package/dist/src/names.js
CHANGED
|
@@ -8,6 +8,11 @@ import { join } from "node:path";
|
|
|
8
8
|
export function setting(name, env = process.env) {
|
|
9
9
|
return env[`FLOW_FRAMEWORK_${name}`] ?? env[`CHINWAG_${name}`];
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The command to print in a next step. `npx flow` would fetch another npm package wherever this one is
|
|
13
|
+
* not installed, so a hint always names the package.
|
|
14
|
+
*/
|
|
15
|
+
export const RUN = "npx @flow-engineer/flow";
|
|
11
16
|
/** An agent's results folder, in the agent's folder: written here, and git-ignored by `flow new`. */
|
|
12
17
|
export const RESULTS_DIR = ".flow";
|
|
13
18
|
/** The results folder from before the rename, still read until a run writes {@link RESULTS_DIR}. */
|
package/dist/src/skills.d.ts
CHANGED
|
@@ -9,4 +9,8 @@ export type AgentFiles = Record<string, string>;
|
|
|
9
9
|
export declare function readAgentFolder(dir: string): AgentFiles;
|
|
10
10
|
export declare function loadSkills(files: AgentFiles): Skill[];
|
|
11
11
|
export declare function loadInstructions(files: AgentFiles, skills: Skill[], inline?: boolean): string;
|
|
12
|
+
/** What an MCP server tells a host about its skills when it connects (chinwag/mcp). */
|
|
13
|
+
export declare function mcpInstructions(skills: Skill[]): string | undefined;
|
|
14
|
+
/** The MCP server's way to read a skill: a tool, since every host calls tools. */
|
|
15
|
+
export declare function readSkillTool(skills: Skill[]): Tool;
|
|
12
16
|
export declare function skillTool(skills: Skill[], onLoad: (conversationId: string, name: string) => void): Tool;
|
package/dist/src/skills.js
CHANGED
|
@@ -51,10 +51,33 @@ export function loadInstructions(files, skills, inline = false) {
|
|
|
51
51
|
const list = skills.map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
|
52
52
|
return `${base}\n\n## Skills\nLoad a skill with load_skill before doing what it covers; call it together with the first other tool you need, not on its own.\n${list}`;
|
|
53
53
|
}
|
|
54
|
+
/** What an MCP server tells a host about its skills when it connects (chinwag/mcp). */
|
|
55
|
+
export function mcpInstructions(skills) {
|
|
56
|
+
if (!skills.length)
|
|
57
|
+
return undefined;
|
|
58
|
+
const list = skills.map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
|
59
|
+
return `This server's skills are rules for one kind of request each. Read a skill with read_skill before doing what it covers.\n${list}`;
|
|
60
|
+
}
|
|
61
|
+
/** The MCP server's way to read a skill: a tool, since every host calls tools. */
|
|
62
|
+
export function readSkillTool(skills) {
|
|
63
|
+
const names = skills.map((s) => s.name);
|
|
64
|
+
return tool({
|
|
65
|
+
name: "read_skill",
|
|
66
|
+
title: "Read a skill",
|
|
67
|
+
description: "Read one of this server's skills in full. Read it before doing what its description covers.",
|
|
68
|
+
input: z.object({ name: z.enum(names) }),
|
|
69
|
+
signIn: "none",
|
|
70
|
+
hints: { readOnly: true, openWorld: false },
|
|
71
|
+
run({ name }) {
|
|
72
|
+
return skills.find((s) => s.name === name)?.body ?? `No skill named ${name}.`;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
54
76
|
export function skillTool(skills, onLoad) {
|
|
55
77
|
const names = skills.map((s) => s.name);
|
|
56
78
|
return tool({
|
|
57
79
|
name: "load_skill",
|
|
80
|
+
signIn: "none",
|
|
58
81
|
description: "Read a skill's full instructions. Use it before doing what the skill covers.",
|
|
59
82
|
input: z.object({ name: z.enum(names) }),
|
|
60
83
|
deadlineMs: 60_000,
|
package/dist/src/store.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ export declare class Store {
|
|
|
95
95
|
allConversations(): Promise<ConversationRow[]>;
|
|
96
96
|
createConversation(id: string, personKey: string): Promise<void>;
|
|
97
97
|
addIdentity(conversationId: string, channel: string, address: string, verifiedBy: string): Promise<void>;
|
|
98
|
+
/** Bind verified login proof; an identity already owned by another account cannot move. */
|
|
99
|
+
linkIdentity(conversationId: string, channel: string, address: string): Promise<boolean>;
|
|
98
100
|
touchIdentity(channel: string, address: string, at: number): Promise<void>;
|
|
99
101
|
setIdentityVia(channel: string, address: string, via: Via): Promise<void>;
|
|
100
102
|
/**
|
package/dist/src/store.js
CHANGED
|
@@ -153,6 +153,24 @@ export class Store {
|
|
|
153
153
|
async addIdentity(conversationId, channel, address, verifiedBy) {
|
|
154
154
|
await this.db.run("INSERT INTO identities (agent_id, channel, address, conversation_id, verified_by) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING", [this.agentId, channel, address, conversationId, verifiedBy]);
|
|
155
155
|
}
|
|
156
|
+
/** Bind verified login proof; an identity already owned by another account cannot move. */
|
|
157
|
+
async linkIdentity(conversationId, channel, address) {
|
|
158
|
+
return this.db.transaction(async (tx) => {
|
|
159
|
+
const lock = this.db.dialect === "postgres" ? " FOR UPDATE" : "";
|
|
160
|
+
const [previous] = await tx.query(`SELECT conversation_id FROM identities WHERE agent_id = ? AND channel = ? AND address = ?${lock}`, [this.agentId, channel, address]);
|
|
161
|
+
const rows = await tx.query(`INSERT INTO identities (agent_id, channel, address, conversation_id, verified_by)
|
|
162
|
+
VALUES (?, ?, ?, ?, 'resolver') ON CONFLICT (agent_id, channel, address) DO UPDATE SET
|
|
163
|
+
conversation_id = excluded.conversation_id, verified_by = excluded.verified_by
|
|
164
|
+
WHERE identities.conversation_id = excluded.conversation_id OR EXISTS (
|
|
165
|
+
SELECT 1 FROM conversations c WHERE c.agent_id = identities.agent_id
|
|
166
|
+
AND c.id = identities.conversation_id AND c.person_key NOT LIKE 'key:%') RETURNING conversation_id`, [this.agentId, channel, address, conversationId]);
|
|
167
|
+
if (rows.length && previous && previous.conversation_id !== conversationId) {
|
|
168
|
+
// Signing in must not undo an anonymous sender's STOP or other no-contact preferences.
|
|
169
|
+
await tx.run("UPDATE no_contact SET conversation_id = ? WHERE agent_id = ? AND conversation_id = ?", [conversationId, this.agentId, previous.conversation_id]);
|
|
170
|
+
}
|
|
171
|
+
return rows.length > 0;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
156
174
|
async touchIdentity(channel, address, at) {
|
|
157
175
|
await this.db.run("UPDATE identities SET last_message_at = ? WHERE agent_id = ? AND channel = ? AND address = ?", [at, this.agentId, channel, address]);
|
|
158
176
|
}
|
package/dist/src/tools.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface ToolContext {
|
|
|
13
13
|
key: string | null;
|
|
14
14
|
/** The channel of their latest message. */
|
|
15
15
|
channel: string;
|
|
16
|
+
/** Provider/server-issued address on a chat channel; absent for standalone MCP calls. */
|
|
17
|
+
address?: string;
|
|
16
18
|
/** The model's id for this call, as the history keeps it. */
|
|
17
19
|
callId: string;
|
|
18
20
|
/**
|
|
@@ -23,7 +25,40 @@ export interface ToolContext {
|
|
|
23
25
|
idempotencyKey: string;
|
|
24
26
|
/** Aborted when the conversation closes (the person was forgotten): stop and drop the work. */
|
|
25
27
|
signal: AbortSignal;
|
|
28
|
+
/** Server-side backend access for this verified person. Never supplied by the model or serialized into history or traces. */
|
|
29
|
+
credentials?: Credentials;
|
|
26
30
|
}
|
|
31
|
+
/** A business supplies named backend credentials or clients, resolved afresh for the verified person. */
|
|
32
|
+
export type CredentialProvider = (name: string, context: Readonly<Omit<ToolContext, "credentials">>) => unknown | Promise<unknown>;
|
|
33
|
+
export interface Credentials {
|
|
34
|
+
/** The business decides how to obtain, refresh and revoke access. The name is written by the tool author, not an account id from the model. */
|
|
35
|
+
get<T = unknown>(name: string): Promise<T>;
|
|
36
|
+
}
|
|
37
|
+
/** Server-side identity at a chat tool boundary. No credentials or model-supplied account ids. */
|
|
38
|
+
export interface ChatAuthContext {
|
|
39
|
+
conversationId: string;
|
|
40
|
+
person: string;
|
|
41
|
+
key: string | null;
|
|
42
|
+
channel: string;
|
|
43
|
+
address: string;
|
|
44
|
+
tool: string;
|
|
45
|
+
signal: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
/** Brand-owned login: OAuth, an existing login page, verified phone, or a custom challenge. */
|
|
48
|
+
export interface ChatAuth {
|
|
49
|
+
/** Recheck a resolved account's current access. Without this, a non-null resolved key is enough. */
|
|
50
|
+
verify?: (context: Readonly<ChatAuthContext>) => boolean | Promise<boolean>;
|
|
51
|
+
/** Called only when a protected operation needs authentication. A custom in-chat prompt needs no URL. */
|
|
52
|
+
signIn: (context: Readonly<ChatAuthContext>) => {
|
|
53
|
+
message: string;
|
|
54
|
+
url?: string;
|
|
55
|
+
} | Promise<{
|
|
56
|
+
message: string;
|
|
57
|
+
url?: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
/** Bind backend access without putting secrets on an enumerable context or in any event. */
|
|
61
|
+
export declare function toolContext(context: Omit<ToolContext, "credentials">, provider?: CredentialProvider): ToolContext;
|
|
27
62
|
export interface Tool<Input = any> {
|
|
28
63
|
name: string;
|
|
29
64
|
description: string;
|
|
@@ -31,6 +66,35 @@ export interface Tool<Input = any> {
|
|
|
31
66
|
run(input: Input, context: ToolContext): Promise<unknown> | unknown;
|
|
32
67
|
/** The longest the reply waits for this tool before it moves to the background (default: the agent's, 60 s). */
|
|
33
68
|
deadlineMs?: number;
|
|
69
|
+
/** A name for people ("Place an order"); derived from `name` when absent. */
|
|
70
|
+
title?: string;
|
|
71
|
+
/** The result's shape. Over MCP the result is checked against it and sent as structured content too. */
|
|
72
|
+
output?: z.ZodType;
|
|
73
|
+
/** Short lines a host shows while the tool runs and once it is done (at most 64 characters each). */
|
|
74
|
+
status?: {
|
|
75
|
+
running?: string;
|
|
76
|
+
done?: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Whether the person must be signed in. "required" (the default) refuses a call without sign-in,
|
|
80
|
+
* "optional" runs either way, "none" never needs it. Enforced where sign-in is configured:
|
|
81
|
+
* createAgent({ auth }) for chat, or mcpServer({ auth }) for MCP. Existing agents without auth
|
|
82
|
+
* keep their current behaviour.
|
|
83
|
+
*/
|
|
84
|
+
signIn?: SignIn;
|
|
85
|
+
/** How the tool behaves, for hosts to decide what to ask the person first. Nothing enforces them. */
|
|
86
|
+
hints?: ToolHints;
|
|
87
|
+
}
|
|
88
|
+
export type SignIn = "required" | "optional" | "none";
|
|
89
|
+
export interface ToolHints {
|
|
90
|
+
/** It changes nothing (default false: a tool counts as a write). */
|
|
91
|
+
readOnly?: boolean;
|
|
92
|
+
/** A write that may delete or overwrite (default true for writes; always false for reads). */
|
|
93
|
+
destructive?: boolean;
|
|
94
|
+
/** Calling it again with the same input changes nothing more (default false). */
|
|
95
|
+
idempotent?: boolean;
|
|
96
|
+
/** It reaches things outside the business's own systems, such as the web (default true). */
|
|
97
|
+
openWorld?: boolean;
|
|
34
98
|
}
|
|
35
99
|
/**
|
|
36
100
|
* Marks a tool the test cloud plays in place of a built-in (get_user_profile): it is offered only
|
|
@@ -38,6 +102,11 @@ export interface Tool<Input = any> {
|
|
|
38
102
|
*/
|
|
39
103
|
export declare const STAND_IN: unique symbol;
|
|
40
104
|
export declare function tool<Input>(definition: Tool<Input>): Tool<Input>;
|
|
105
|
+
/**
|
|
106
|
+
* Marks a tool whose input schema is given as JSON Schema (a tool another MCP server defines): the
|
|
107
|
+
* model is shown that schema exactly, not one rebuilt from the zod that validates it.
|
|
108
|
+
*/
|
|
109
|
+
export declare const INPUT_SCHEMA: unique symbol;
|
|
41
110
|
export declare function toSpec(t: Tool): ToolSpec;
|
|
42
111
|
/** A tool's output as the model reads it. */
|
|
43
112
|
export declare function render(output: unknown): string;
|
package/dist/src/tools.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// Tools are plain functions with a zod input. The runtime decides at call time whether one runs
|
|
2
2
|
// in the reply or carries on in the background; tool authors mark nothing.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
/** Bind backend access without putting secrets on an enumerable context or in any event. */
|
|
5
|
+
export function toolContext(context, provider) {
|
|
6
|
+
const identity = Object.freeze({ ...context });
|
|
7
|
+
if (provider)
|
|
8
|
+
Object.defineProperty(context, "credentials", {
|
|
9
|
+
value: Object.freeze({ get: (name) => Promise.resolve().then(() => provider(name, identity)) }),
|
|
10
|
+
enumerable: false,
|
|
11
|
+
});
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
4
14
|
/**
|
|
5
15
|
* Marks a tool the test cloud plays in place of a built-in (get_user_profile): it is offered only
|
|
6
16
|
* to an agent that has that built-in, and then instead of it, so a test never runs the real one.
|
|
@@ -9,8 +19,14 @@ export const STAND_IN = Symbol("chinwag.standIn");
|
|
|
9
19
|
export function tool(definition) {
|
|
10
20
|
return definition;
|
|
11
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Marks a tool whose input schema is given as JSON Schema (a tool another MCP server defines): the
|
|
24
|
+
* model is shown that schema exactly, not one rebuilt from the zod that validates it.
|
|
25
|
+
*/
|
|
26
|
+
export const INPUT_SCHEMA = Symbol("chinwag.inputSchema");
|
|
12
27
|
export function toSpec(t) {
|
|
13
|
-
const
|
|
28
|
+
const given = t[INPUT_SCHEMA];
|
|
29
|
+
const { $schema: _ignored, ...parameters } = given ?? z.toJSONSchema(t.input);
|
|
14
30
|
return { name: t.name, description: t.description, parameters };
|
|
15
31
|
}
|
|
16
32
|
/** A tool's output as the model reads it. */
|
package/dist/src/traces.js
CHANGED
|
@@ -41,10 +41,14 @@ export function tracesFromEnv(env = process.env) {
|
|
|
41
41
|
return { key, url: setting("TRACES_URL", env)?.trim() || OBSERVE_URL, content: setting("TRACES_CONTENT", env) === "1" };
|
|
42
42
|
}
|
|
43
43
|
/** The endpoint's channel names: Flow Framework's custom UIs are web or app chats. */
|
|
44
|
-
const CHANNEL_KIND = { sms: "sms", whatsapp: "whatsapp", custom: "web" };
|
|
44
|
+
const CHANNEL_KIND = { sms: "sms", mms: "mms", whatsapp: "whatsapp", telegram: "telegram", imessage: "imessage", web: "web", voice: "voice", other: "other", custom: "web", cli: "cli" };
|
|
45
45
|
let warnedMetadataOnly = false;
|
|
46
46
|
export function traces(options, agent) {
|
|
47
|
-
const
|
|
47
|
+
const url = options.url ?? "";
|
|
48
|
+
let end = url.length;
|
|
49
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47)
|
|
50
|
+
end--;
|
|
51
|
+
const base = url.slice(0, end);
|
|
48
52
|
if (!options.key)
|
|
49
53
|
throw new Error("flow: traces need a key");
|
|
50
54
|
if (!base)
|
|
@@ -58,6 +62,7 @@ export function traces(options, agent) {
|
|
|
58
62
|
console.warn("flow: traces are metadata only. Set FLOW_FRAMEWORK_TRACES_CONTENT=1 to send what was written.");
|
|
59
63
|
}
|
|
60
64
|
const open = new Map();
|
|
65
|
+
const calls = new Map();
|
|
61
66
|
/** Messages and background results that came while no turn was open: they join the next one. */
|
|
62
67
|
const waiting = new Map();
|
|
63
68
|
const queue = [];
|
|
@@ -168,6 +173,29 @@ export function traces(options, agent) {
|
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
function handle(e) {
|
|
176
|
+
// An MCP/CLI call has no conversation or model turn. Reuse the same bounded exporter.
|
|
177
|
+
if (["mcp", "mcp-plugin", "cli"].includes(String(e.surface))) {
|
|
178
|
+
const call = String(e.callId);
|
|
179
|
+
if (e.type === "tool.start") {
|
|
180
|
+
calls.set(call, { at: e.at, ...(content ? { input: e.input } : {}) });
|
|
181
|
+
if (calls.size > MAX_QUEUED)
|
|
182
|
+
calls.delete(calls.keys().next().value);
|
|
183
|
+
}
|
|
184
|
+
else if (e.type === "tool.end") {
|
|
185
|
+
const started = calls.get(call);
|
|
186
|
+
calls.delete(call);
|
|
187
|
+
const attributes = toolAttributes(e, started?.input);
|
|
188
|
+
attributes.push(attr("chinwag.surface", String(e.surface)), attr("openinference.span.kind", "TOOL"));
|
|
189
|
+
for (const [field, key] of Object.entries({ client: "chinwag.mcp.client", clientVersion: "chinwag.mcp.client_version", session: "chinwag.mcp.session", person: "chinwag.person", command: "chinwag.command" })) {
|
|
190
|
+
if (typeof e[field] === "string")
|
|
191
|
+
attributes.push(attr(key, e[field]));
|
|
192
|
+
}
|
|
193
|
+
if (e.ok === false)
|
|
194
|
+
attributes.push(attr("error", typeof e.error === "string" ? clip(e.error) : "Tool failed"));
|
|
195
|
+
enqueue([span(hex(16), `mcp.call ${e.tool}`, started?.at ?? e.at - Number(e.ms ?? 0), e.at, attributes, undefined, e.ok === false)]);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
171
199
|
const id = e.conversationId;
|
|
172
200
|
const turn = open.get(id);
|
|
173
201
|
switch (e.type) {
|
|
@@ -241,8 +269,8 @@ export function traces(options, agent) {
|
|
|
241
269
|
const attributes = [attr("gen_ai.operation.name", "execute_tool"), attr("gen_ai.tool.name", String(e.tool)), attr("gen_ai.tool.call.id", String(e.callId))];
|
|
242
270
|
if (content && input !== undefined)
|
|
243
271
|
attributes.push(attr("gen_ai.tool.call.arguments", clip(JSON.stringify(input))));
|
|
244
|
-
if (content &&
|
|
245
|
-
attributes.push(attr("gen_ai.tool.call.result", clip(e.output)));
|
|
272
|
+
if (content && e.output !== undefined)
|
|
273
|
+
attributes.push(attr("gen_ai.tool.call.result", clip(typeof e.output === "string" ? e.output : JSON.stringify(e.output))));
|
|
246
274
|
return attributes;
|
|
247
275
|
}
|
|
248
276
|
return {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ export interface ListRow {
|
|
|
85
85
|
export type Outbound = {
|
|
86
86
|
type: "message";
|
|
87
87
|
text: string;
|
|
88
|
+
/** A framework-generated login challenge; its link opens real authentication in simulators. */
|
|
89
|
+
authentication?: boolean;
|
|
88
90
|
/** The person's message this one quotes (channels with replies). */
|
|
89
91
|
replyTo?: string;
|
|
90
92
|
/** Reply buttons; a tap comes back as the person's message. */
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const ACCOUNT_COMMANDS: Set<string>;
|
|
2
|
+
export declare const ACCOUNT_LINES = " flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]\n Sign in to flow.engineer: opens the browser, prints the link and code,\n and waits up to --timeout (default 120) for approval; --wait goes on\n waiting for the same code, --no-wait returns at once\n flow logout [--json] End this machine's sign-in and delete its token\n flow whoami [--json] Show the account you are signed in as\n flow setup [--replace] [--rotate <keys> [--now]] [--ci] [--json]\n Write this agent's keys (its Flow key FLOW_KEY, which also pays for the\n model on the account's trial credit when there is no provider key, test\n cloud, traces, Flow Messaging) and each open channel's sender into\n ./.env (in an agent's folder: package.json); --rotate key,cloud,traces,\n messaging or all makes those keys again (the old FLOW_KEY works 24 h\n more; --now ends it at once)\n flow link [--channel telegram|whatsapp|imessage] [--timeout <s>] [--no-browser] [--json]\n Print the link that opens a chat with this agent from a phone; signs in\n and runs setup first when this folder is not set up yet\n flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their\n values) with the trial credit left, the\n sign-in, each channel's sender in .env, and, signed in, which channels\n are open now and their free-trial counts";
|
|
3
|
+
/** Runs one account command; resolves to the exit status. */
|
|
4
|
+
export declare function accountCli(command: string, args: string[]): Promise<number>;
|