agent-state-machine 2.0.9 → 2.0.10
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 +5 -5
- package/lib/index.js +3 -3
- package/lib/runtime/index.js +1 -1
- package/lib/runtime/prompt.js +3 -3
- package/lib/setup.js +4 -4
- package/package.json +5 -2
- package/vercel-server/ui/index.html +30 -40
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A workflow runner for building **linear, stateful agent workflows** in plain Jav
|
|
|
4
4
|
|
|
5
5
|
You write normal `async/await` code. The runtime handles:
|
|
6
6
|
- **Auto-persisted** `memory` (saved to disk on mutation)
|
|
7
|
-
- **Human-in-the-loop** blocking via `
|
|
7
|
+
- **Human-in-the-loop** blocking via `askHuman()` or agent-driven interactions
|
|
8
8
|
- Local **JS agents** + **Markdown agents** (LLM-powered)
|
|
9
9
|
|
|
10
10
|
---
|
|
@@ -80,7 +80,7 @@ workflows/<name>/
|
|
|
80
80
|
* - Interactive prompts pause and wait for user input
|
|
81
81
|
*/
|
|
82
82
|
|
|
83
|
-
import { agent, memory,
|
|
83
|
+
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
84
84
|
import { notify } from './scripts/mac-notification.js';
|
|
85
85
|
|
|
86
86
|
// Model configuration (also supports models in a separate config export)
|
|
@@ -101,7 +101,7 @@ export default async function() {
|
|
|
101
101
|
console.log('Starting project-builder workflow...');
|
|
102
102
|
|
|
103
103
|
// Example: Get user input (saved to memory)
|
|
104
|
-
const userLocation = await
|
|
104
|
+
const userLocation = await askHuman('Where do you live?');
|
|
105
105
|
console.log('Example prompt answer:', userLocation);
|
|
106
106
|
|
|
107
107
|
const userInfo = await agent('yoda-name-collector');
|
|
@@ -168,7 +168,7 @@ A persisted object for your workflow.
|
|
|
168
168
|
memory.count = (memory.count || 0) + 1;
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
### `
|
|
171
|
+
### `askHuman(question, options?)`
|
|
172
172
|
|
|
173
173
|
Gets user input.
|
|
174
174
|
|
|
@@ -176,7 +176,7 @@ Gets user input.
|
|
|
176
176
|
- Otherwise it creates `interactions/<slug>.md` and blocks until you confirm in the terminal (or respond in the browser).
|
|
177
177
|
|
|
178
178
|
```js
|
|
179
|
-
const repo = await
|
|
179
|
+
const repo = await askHuman('What repo should I work on?', { slug: 'repo' });
|
|
180
180
|
memory.repo = repo;
|
|
181
181
|
```
|
|
182
182
|
|
package/lib/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
WorkflowRuntime,
|
|
12
12
|
agent,
|
|
13
13
|
executeAgent,
|
|
14
|
-
|
|
14
|
+
askHuman,
|
|
15
15
|
parallel,
|
|
16
16
|
parallelLimit,
|
|
17
17
|
getMemory,
|
|
@@ -82,7 +82,7 @@ export {
|
|
|
82
82
|
WorkflowRuntime,
|
|
83
83
|
agent,
|
|
84
84
|
executeAgent,
|
|
85
|
-
|
|
85
|
+
askHuman,
|
|
86
86
|
parallel,
|
|
87
87
|
parallelLimit,
|
|
88
88
|
getCurrentRuntime,
|
|
@@ -99,7 +99,7 @@ const api = {
|
|
|
99
99
|
WorkflowRuntime,
|
|
100
100
|
agent,
|
|
101
101
|
executeAgent,
|
|
102
|
-
|
|
102
|
+
askHuman,
|
|
103
103
|
parallel,
|
|
104
104
|
parallelLimit,
|
|
105
105
|
getCurrentRuntime,
|
package/lib/runtime/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export {
|
|
|
14
14
|
} from './runtime.js';
|
|
15
15
|
|
|
16
16
|
export { agent, executeAgent } from './agent.js';
|
|
17
|
-
export {
|
|
17
|
+
export { askHuman } from './prompt.js';
|
|
18
18
|
export { parallel, parallelLimit } from './parallel.js';
|
|
19
19
|
export { createMemoryProxy } from './memory.js';
|
|
20
20
|
|
package/lib/runtime/prompt.js
CHANGED
|
@@ -27,10 +27,10 @@ const C = {
|
|
|
27
27
|
* @param {string} options.slug - Unique identifier for this prompt (for file)
|
|
28
28
|
* @returns {Promise<string>} User's response
|
|
29
29
|
*/
|
|
30
|
-
export async function
|
|
30
|
+
export async function askHuman(question, options = {}) {
|
|
31
31
|
const runtime = getCurrentRuntime();
|
|
32
32
|
if (!runtime) {
|
|
33
|
-
throw new Error('
|
|
33
|
+
throw new Error('askHuman() must be called within a workflow context');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const slug = options.slug || generateSlug(question);
|
|
@@ -121,7 +121,7 @@ function askQuestionWithRemote(runtime, question, slug, memoryKey) {
|
|
|
121
121
|
console.log(`\n${C.green}✓ Answered via remote${C.reset}`);
|
|
122
122
|
resolve(response);
|
|
123
123
|
},
|
|
124
|
-
reject: () => {} // Prompts don't reject
|
|
124
|
+
reject: () => { } // Prompts don't reject
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
|
package/lib/setup.js
CHANGED
|
@@ -59,7 +59,7 @@ async function setup(workflowName) {
|
|
|
59
59
|
* - Interactive prompts pause and wait for user input
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
|
-
import { agent, memory,
|
|
62
|
+
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
63
63
|
import { notify } from './scripts/mac-notification.js';
|
|
64
64
|
|
|
65
65
|
// Model configuration (also supports models in a separate config export)
|
|
@@ -80,7 +80,7 @@ export default async function() {
|
|
|
80
80
|
console.log('Starting ${workflowName} workflow...');
|
|
81
81
|
|
|
82
82
|
// Example: Get user input (saved to memory)
|
|
83
|
-
const userLocation = await
|
|
83
|
+
const userLocation = await askHuman('Where do you live?');
|
|
84
84
|
console.log('Example prompt answer:', userLocation);
|
|
85
85
|
|
|
86
86
|
const userInfo = await agent('yoda-name-collector');
|
|
@@ -339,13 +339,13 @@ state-machine reset-hard ${workflowName}
|
|
|
339
339
|
Edit \`workflow.js\` - write normal async JavaScript:
|
|
340
340
|
|
|
341
341
|
\\\`\\\`\\\`js
|
|
342
|
-
import { agent, memory,
|
|
342
|
+
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
343
343
|
|
|
344
344
|
export default async function() {
|
|
345
345
|
console.log('Starting project-builder workflow...');
|
|
346
346
|
|
|
347
347
|
// Example: Get user input (saved to memory)
|
|
348
|
-
const userLocation = await
|
|
348
|
+
const userLocation = await askHuman('Where do you live?');
|
|
349
349
|
console.log('Example prompt answer:', userLocation);
|
|
350
350
|
|
|
351
351
|
const userInfo = await agent('yoda-name-collector');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-state-machine",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A workflow orchestrator for running agents and scripts in sequence with state management",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,5 +32,8 @@
|
|
|
32
32
|
"vercel-server/public",
|
|
33
33
|
"vercel-server/ui",
|
|
34
34
|
"vercel-server/api"
|
|
35
|
-
]
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"nodemailer": "^7.0.11"
|
|
38
|
+
}
|
|
36
39
|
}
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
</script>
|
|
220
220
|
|
|
221
221
|
<script type="text/babel">
|
|
222
|
-
const { useEffect, useMemo, useState } = React;
|
|
222
|
+
const { useEffect, useMemo, useRef, useState } = React;
|
|
223
223
|
|
|
224
224
|
const Icon = ({ name }) => {
|
|
225
225
|
const common = "w-4 h-4";
|
|
@@ -253,14 +253,6 @@
|
|
|
253
253
|
</svg>
|
|
254
254
|
);
|
|
255
255
|
}
|
|
256
|
-
if (name === "sort") {
|
|
257
|
-
return (
|
|
258
|
-
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
259
|
-
<path strokeLinecap="round" d="M7 4h14M7 8h10M7 12h6" />
|
|
260
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M3 20V4m0 16 2-2m-2 2-2-2" />
|
|
261
|
-
</svg>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
256
|
return null;
|
|
265
257
|
};
|
|
266
258
|
|
|
@@ -396,6 +388,18 @@
|
|
|
396
388
|
function InteractionForm({ interaction, onSubmit, disabled }) {
|
|
397
389
|
const [response, setResponse] = useState("");
|
|
398
390
|
const [submitting, setSubmitting] = useState(false);
|
|
391
|
+
const textareaRef = useRef(null);
|
|
392
|
+
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
setResponse(interaction.question || "");
|
|
395
|
+
}, [interaction.slug, interaction.question]);
|
|
396
|
+
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
const el = textareaRef.current;
|
|
399
|
+
if (!el) return;
|
|
400
|
+
el.style.height = "auto";
|
|
401
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
402
|
+
}, [response]);
|
|
399
403
|
|
|
400
404
|
const handleSubmit = async (e) => {
|
|
401
405
|
e.preventDefault();
|
|
@@ -416,8 +420,8 @@
|
|
|
416
420
|
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
417
421
|
INPUT REQUIRED
|
|
418
422
|
</div>
|
|
419
|
-
<div className="mt-2 text-[
|
|
420
|
-
|
|
423
|
+
<div className="mt-2 text-[12px] tracking-[0.16em] uppercase" style={{ color: "var(--muted)" }}>
|
|
424
|
+
Safe to delete all text and type your answer.
|
|
421
425
|
</div>
|
|
422
426
|
</div>
|
|
423
427
|
<div className="flex items-center gap-2">
|
|
@@ -427,17 +431,17 @@
|
|
|
427
431
|
|
|
428
432
|
<form onSubmit={handleSubmit} className="px-6 py-5">
|
|
429
433
|
<textarea
|
|
434
|
+
ref={textareaRef}
|
|
430
435
|
value={response}
|
|
431
436
|
onChange={(e) => setResponse(e.target.value)}
|
|
432
437
|
disabled={disabled || submitting}
|
|
433
|
-
placeholder="Type response…"
|
|
434
438
|
className="w-full rounded-2xl hairline p-4 text-[13px] leading-relaxed min-h-[120px]"
|
|
435
439
|
style={{
|
|
436
440
|
background: "transparent",
|
|
437
441
|
color: "var(--fg)",
|
|
438
442
|
borderColor: "var(--hairline)",
|
|
439
443
|
outline: "none",
|
|
440
|
-
resize: "
|
|
444
|
+
resize: "none",
|
|
441
445
|
direction: "ltr",
|
|
442
446
|
textAlign: "left",
|
|
443
447
|
}}
|
|
@@ -471,7 +475,6 @@
|
|
|
471
475
|
if (saved === "light" || saved === "dark") return saved;
|
|
472
476
|
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
473
477
|
});
|
|
474
|
-
const [sortNewest, setSortNewest] = useState(true);
|
|
475
478
|
const [pendingInteraction, setPendingInteraction] = useState(null);
|
|
476
479
|
|
|
477
480
|
const token =
|
|
@@ -630,12 +633,11 @@
|
|
|
630
633
|
};
|
|
631
634
|
|
|
632
635
|
const toggleTheme = () => setTheme((p) => (p === "dark" ? "light" : "dark"));
|
|
633
|
-
const toggleSort = () => setSortNewest((p) => !p);
|
|
634
636
|
|
|
635
637
|
const formatTime = (ts) =>
|
|
636
638
|
new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
637
639
|
|
|
638
|
-
const visibleEvents =
|
|
640
|
+
const visibleEvents = [...history].reverse();
|
|
639
641
|
|
|
640
642
|
const wrapIO = (side, children, key) => (
|
|
641
643
|
<div key={key} className={`io-row ${side === "right" ? "io-right" : side === "center" ? "io-center" : "io-left"}`}>
|
|
@@ -667,14 +669,12 @@
|
|
|
667
669
|
return wrapIO(
|
|
668
670
|
"center",
|
|
669
671
|
<section className="text-center">
|
|
670
|
-
<div className="divider" />
|
|
671
672
|
<div className="py-4">
|
|
672
673
|
<div className="text-[11px] tracking-[0.24em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
673
674
|
{item.event.replace("WORKFLOW_", "")} • {time}
|
|
674
675
|
</div>
|
|
675
676
|
{item.error && <div className="mt-3 text-[13px] leading-relaxed markdown-body">{item.error}</div>}
|
|
676
677
|
</div>
|
|
677
|
-
<div className="divider" />
|
|
678
678
|
</section>,
|
|
679
679
|
idx
|
|
680
680
|
);
|
|
@@ -916,15 +916,6 @@
|
|
|
916
916
|
</div>
|
|
917
917
|
|
|
918
918
|
<div className="flex items-center gap-2">
|
|
919
|
-
<Toggle
|
|
920
|
-
onClick={() => setSortNewest((p) => !p)}
|
|
921
|
-
label=""
|
|
922
|
-
title={sortNewest ? "Newest first" : "Oldest first"}
|
|
923
|
-
>
|
|
924
|
-
<span className={`transition-transform duration-100 ${!sortNewest ? "rotate-180" : ""}`}>
|
|
925
|
-
<Icon name="sort" />
|
|
926
|
-
</span>
|
|
927
|
-
</Toggle>
|
|
928
919
|
<Toggle onClick={() => setTheme((p) => (p === "dark" ? "light" : "dark"))} label="" title="Toggle theme">
|
|
929
920
|
{theme === "dark" ? <Icon name="sun" /> : <Icon name="moon" />}
|
|
930
921
|
</Toggle>
|
|
@@ -935,19 +926,6 @@
|
|
|
935
926
|
</div>
|
|
936
927
|
</header>
|
|
937
928
|
|
|
938
|
-
{/* Pending interaction always left */}
|
|
939
|
-
{pendingInteraction && (
|
|
940
|
-
<div className="io-row io-left mb-8">
|
|
941
|
-
<div className="io-card">
|
|
942
|
-
<InteractionForm
|
|
943
|
-
interaction={pendingInteraction}
|
|
944
|
-
onSubmit={handleSubmit}
|
|
945
|
-
disabled={status !== "connected"}
|
|
946
|
-
/>
|
|
947
|
-
</div>
|
|
948
|
-
</div>
|
|
949
|
-
)}
|
|
950
|
-
|
|
951
929
|
{/* Error stays right */}
|
|
952
930
|
{error && (
|
|
953
931
|
<div className="io-row io-right mb-8">
|
|
@@ -974,6 +952,18 @@
|
|
|
974
952
|
{visibleEvents.map(renderEvent)}
|
|
975
953
|
</main>
|
|
976
954
|
|
|
955
|
+
{pendingInteraction && (
|
|
956
|
+
<div className="io-row io-left mt-10">
|
|
957
|
+
<div className="io-card">
|
|
958
|
+
<InteractionForm
|
|
959
|
+
interaction={pendingInteraction}
|
|
960
|
+
onSubmit={handleSubmit}
|
|
961
|
+
disabled={status !== "connected"}
|
|
962
|
+
/>
|
|
963
|
+
</div>
|
|
964
|
+
</div>
|
|
965
|
+
)}
|
|
966
|
+
|
|
977
967
|
<footer className="mt-16 pt-8 divider">
|
|
978
968
|
<div className="center-wrap text-center">
|
|
979
969
|
<div className="py-6 text-[11px] tracking-[0.28em] font-semibold" style={{ color: "var(--muted)" }}>
|