@hotmeshio/long-tail 0.1.9 → 0.1.11
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.
|
@@ -160,13 +160,18 @@ async function resolveClient(serverId) {
|
|
|
160
160
|
if (clients.has(serverId))
|
|
161
161
|
return clients.get(serverId);
|
|
162
162
|
// 2. Check built-in server factories -- exact match first, then fuzzy
|
|
163
|
+
// Normalize strips non-alphanumeric chars so hyphens and underscores
|
|
164
|
+
// match (e.g., "long_tail_vision" matches "long-tail-vision").
|
|
165
|
+
const norm = (s) => s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
163
166
|
let matchedName = null;
|
|
164
167
|
if (builtinFactories.has(serverId)) {
|
|
165
168
|
matchedName = serverId;
|
|
166
169
|
}
|
|
167
170
|
else {
|
|
171
|
+
const normId = norm(serverId);
|
|
168
172
|
for (const [name] of builtinFactories) {
|
|
169
|
-
|
|
173
|
+
const normName = norm(name);
|
|
174
|
+
if (normName.includes(normId) || normId.includes(normName)) {
|
|
170
175
|
matchedName = name;
|
|
171
176
|
break;
|
|
172
177
|
}
|
|
@@ -44,6 +44,27 @@ const yamlDb = __importStar(require("../db"));
|
|
|
44
44
|
const scope_1 = require("./scope");
|
|
45
45
|
const callbacks_1 = require("./callbacks");
|
|
46
46
|
const events_1 = require("./events");
|
|
47
|
+
/**
|
|
48
|
+
* HotMesh YAML maps serialize arrays as objects with numeric keys
|
|
49
|
+
* (e.g., {0: "a", 1: "b"}). Recursively coerce these back to arrays
|
|
50
|
+
* before passing args to MCP tools that expect real arrays.
|
|
51
|
+
*/
|
|
52
|
+
function coerceNumericObjects(obj) {
|
|
53
|
+
if (Array.isArray(obj))
|
|
54
|
+
return obj.map(coerceNumericObjects);
|
|
55
|
+
if (obj === null || typeof obj !== 'object')
|
|
56
|
+
return obj;
|
|
57
|
+
const record = obj;
|
|
58
|
+
const keys = Object.keys(record);
|
|
59
|
+
if (keys.length > 0 && keys.every((k, i) => k === String(i))) {
|
|
60
|
+
return keys.map((k) => coerceNumericObjects(record[k]));
|
|
61
|
+
}
|
|
62
|
+
const result = {};
|
|
63
|
+
for (const [k, v] of Object.entries(record)) {
|
|
64
|
+
result[k] = coerceNumericObjects(v);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
47
68
|
/** Track which topics already have registered workers */
|
|
48
69
|
const registeredTopics = new Set();
|
|
49
70
|
/**
|
|
@@ -194,7 +215,8 @@ async function registerWorkersForWorkflow(workflow) {
|
|
|
194
215
|
};
|
|
195
216
|
}
|
|
196
217
|
const exchangedArgs = await (0, ephemeral_1.exchangeTokensInArgs)(mergedArgs);
|
|
197
|
-
const
|
|
218
|
+
const coercedArgs = coerceNumericObjects(exchangedArgs);
|
|
219
|
+
const result = await mcpClient.callServerTool(serverId, toolName, coercedArgs);
|
|
198
220
|
if (result && typeof result === 'object' && 'error' in result) {
|
|
199
221
|
logger_1.loggerRegistry.error(`[yaml-workflow:worker] ${toolName} error: ${JSON.stringify(result).slice(0, 200)}`);
|
|
200
222
|
}
|
package/build/start/workers.js
CHANGED
|
@@ -114,9 +114,16 @@ async function startWorkers(startConfig, workers, builtinMcpServerFactories) {
|
|
|
114
114
|
});
|
|
115
115
|
// Start each worker
|
|
116
116
|
for (const w of workers) {
|
|
117
|
+
if (w.connection?.readonly) {
|
|
118
|
+
// Readonly workers register for discovery only — they must not
|
|
119
|
+
// consume messages from the stream (that is the real worker's job).
|
|
120
|
+
(0, registry_1.registerWorker)(w.workflow.name, w.taskQueue);
|
|
121
|
+
logger_1.loggerRegistry.info(`[long-tail] readonly worker registered: ${w.taskQueue}::${w.workflow.name}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
117
124
|
const label = `${w.taskQueue}::${w.workflow.name}`;
|
|
118
125
|
const worker = await hotmesh_1.Durable.Worker.create({
|
|
119
|
-
connection
|
|
126
|
+
connection,
|
|
120
127
|
taskQueue: w.taskQueue,
|
|
121
128
|
workflow: w.workflow,
|
|
122
129
|
guid: `${label}-${hotmesh_1.Durable.guid()}`,
|
|
@@ -205,14 +205,15 @@ IMPORTANT: A bare array like \`['.png']\` as a row after sub-pipes will CRASH
|
|
|
205
205
|
2. **Worker per tool**: Each MCP tool call is a worker activity
|
|
206
206
|
3. **Collision-proof activity IDs**: Multiple workflows share the same app namespace. Activity IDs MUST be globally unique within the app. Use a descriptive name with a shared 4-char random suffix appended to every activity in the flow: \`trigger_x8kf\`, \`capture_x8kf\`, \`analyze_x8kf\`, \`store_x8kf\`. The suffix is the same for all activities in one workflow but unique across workflows. NEVER use bare names like \`trigger\`, \`capture\`, \`analyze\` — they WILL collide with other workflows in the same app.
|
|
207
207
|
4. **workflowName**: Every worker MUST have \`workflowName: '<tool_name>'\` in its input.maps — this routes to the correct MCP tool handler
|
|
208
|
-
5. **
|
|
209
|
-
6. **
|
|
210
|
-
7. **
|
|
211
|
-
8. **
|
|
212
|
-
9. **
|
|
213
|
-
10. **
|
|
214
|
-
11. **
|
|
215
|
-
12. **
|
|
208
|
+
5. **mcp_server_id**: In the activity_manifest, use the exact hyphenated server name from the inventory (e.g., "long-tail-vision"), NOT the underscored tool prefix (e.g., "long_tail_vision")
|
|
209
|
+
6. **_scope threading**: Every worker MUST have \`_scope: '{trigger_x8kf.output.data._scope}'\` (using YOUR trigger's ID) for IAM context
|
|
210
|
+
7. **Wire outputs forward**: Use \`{prevActivity.output.data.fieldName}\` to pass data between steps
|
|
211
|
+
8. **Use @pipe for transforms**: When a value needs runtime computation (date stamp, string concat, slugify), use @pipe — never hardcode computed values
|
|
212
|
+
9. **Simple fields stay simple**: If a field just passes a trigger value through (domain, key, url), use a plain reference like \`'{trigger_x8kf.output.data.domain}'\` — NEVER wrap it in @pipe. Only use @pipe when actual transformation is needed.
|
|
213
|
+
10. **File extensions**: Screenshot paths MUST include .png extension. Use @pipe concat if deriving from a slug
|
|
214
|
+
11. **job.maps on last activity**: The final activity should have job.maps to promote output fields to the workflow result
|
|
215
|
+
12. **Linear transitions**: Chain activities with transitions unless branching or iteration is needed
|
|
216
|
+
13. **Conditional transitions**: For branching, use multi-target transitions with conditions:
|
|
216
217
|
\`\`\`yaml
|
|
217
218
|
transitions:
|
|
218
219
|
check_x8kf:
|
|
@@ -222,7 +223,7 @@ transitions:
|
|
|
222
223
|
- to: proceed_x8kf
|
|
223
224
|
\`\`\`
|
|
224
225
|
Conditions can match on \`code\` (HTTP status) or \`match\` (field comparisons). The first matching condition wins; the last entry (no conditions) is the default.
|
|
225
|
-
|
|
226
|
+
14. **Trigger stats for idempotency**: Use \`stats.id\` and \`stats.key\` on the trigger when the workflow needs custom job IDs or indexed lookups:
|
|
226
227
|
\`\`\`yaml
|
|
227
228
|
trigger_x8kf:
|
|
228
229
|
type: trigger
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/long-tail",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Long Tail Workflows — Durable AI workflows with human-in-the-loop escalation. Powered by PostgreSQL.",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@anthropic-ai/sdk": "^0.82.0",
|
|
63
63
|
"@aws-sdk/client-s3": "^3.1017.0",
|
|
64
|
-
"@hotmeshio/hotmesh": "^0.14.
|
|
64
|
+
"@hotmeshio/hotmesh": "^0.14.4",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
66
66
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.215.0",
|
|
67
67
|
"@opentelemetry/resources": "^2.5.1",
|