@owlmeans/agent-common 0.1.18-rc.7
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 +55 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/agent-common/SKILL.md +66 -0
- package/build/consts.d.ts +67 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +70 -0
- package/build/consts.js.map +1 -0
- package/build/errors.d.ts +11 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +18 -0
- package/build/errors.js.map +1 -0
- package/build/flows.d.ts +22 -0
- package/build/flows.d.ts.map +1 -0
- package/build/flows.js +110 -0
- package/build/flows.js.map +1 -0
- package/build/helpers/conversation.d.ts +26 -0
- package/build/helpers/conversation.d.ts.map +1 -0
- package/build/helpers/conversation.js +52 -0
- package/build/helpers/conversation.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +97 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +40 -0
- package/src/consts.ts +73 -0
- package/src/errors.ts +22 -0
- package/src/flows.ts +118 -0
- package/src/helpers/conversation.ts +62 -0
- package/src/index.ts +5 -0
- package/src/types.ts +104 -0
- package/tests/flow.spec.ts +121 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @owlmeans/agent-common
|
|
2
|
+
|
|
3
|
+
Serializable contracts for OwlMeans agents and LLM pipelines: conversation identity, the run
|
|
4
|
+
lifecycle flow, and the record shapes an application persists.
|
|
5
|
+
|
|
6
|
+
Runtime-free by design — no LangChain, no LangGraph, no storage driver. A backend or a browser
|
|
7
|
+
bundle can import these shapes to read what an agent wrote without pulling the runtime. The
|
|
8
|
+
runtime is [`@owlmeans/agent`](../agent).
|
|
9
|
+
|
|
10
|
+
## What is here
|
|
11
|
+
|
|
12
|
+
| Export | Purpose |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `conversationFor(purpose, override?)` | Derives a `ConversationRef` from an `LlmPurpose` dedication — the thread identity an agent run belongs to |
|
|
15
|
+
| `truncateAt(text, max)` | Boundary-aware truncation; every character cap in the family lands here |
|
|
16
|
+
| `agentRunFlow` / `AgentRunStep` / `AgentRunTransition` | The `@owlmeans/flow` lifecycle a run is driven through |
|
|
17
|
+
| `ConversationEvent` | One finished run, compacted: `summary` (what happened) + `advice` (what to do next) |
|
|
18
|
+
| `AgentRunState` | The data plane of a run — serialized flow plus the execution snapshot |
|
|
19
|
+
| `MemoryNode` / `MemoryEvent` | Agent-authored memory: a subsystem graph node, and an entry in a bounded sequence |
|
|
20
|
+
| `AgentRunMessage` | What a transport carries; execution state travels by reference |
|
|
21
|
+
|
|
22
|
+
## Conventions worth knowing
|
|
23
|
+
|
|
24
|
+
**Timestamps are ISO strings, never `Date`.** These contracts cross process boundaries and storage
|
|
25
|
+
backends and must survive `JSON.stringify` unchanged. A store whose backend prefers dates converts
|
|
26
|
+
at its own adapter boundary.
|
|
27
|
+
|
|
28
|
+
**Flow steps are lifecycle stages, not conversational turns.** `FlowPayload` holds flat scalars
|
|
29
|
+
only and a ReAct loop's turn count is unbounded, so the loop lives inside the `Working` step and
|
|
30
|
+
only its counter travels in the payload. What the steps buy is the ability to say where an
|
|
31
|
+
interrupted run resumes.
|
|
32
|
+
|
|
33
|
+
**Each working step has exactly one non-explicit outgoing transition**, so `FlowModel.next()` always
|
|
34
|
+
has an unambiguous answer and a driver can advance a run without knowing the vocabulary. `Fail` is
|
|
35
|
+
marked explicit precisely so it never becomes that automatic answer.
|
|
36
|
+
|
|
37
|
+
**Port names are exported, ports are not bound here.** `AGENT_CONVERSATION_STORE` and friends are
|
|
38
|
+
the keys a consumer registers its own storage under. An unbound port is not an error — the plugin
|
|
39
|
+
that needs it degrades to a no-op.
|
|
40
|
+
|
|
41
|
+
<!-- owlmeans:agent-guidance:start -->
|
|
42
|
+
## Agent guidance
|
|
43
|
+
|
|
44
|
+
This package ships embedded agent skills under `agent-meta/`. After installing your
|
|
45
|
+
`@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
|
|
46
|
+
your project's skill store (`.agents/skills/`):
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx @owlmeans/agent-skills
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The embedded files are version-matched to this package release. Do not edit them
|
|
53
|
+
directly — they are regenerated on each publish. To contribute guidance edits,
|
|
54
|
+
open a PR against the source monorepo.
|
|
55
|
+
<!-- owlmeans:agent-guidance:end -->
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"package": "@owlmeans/agent-common",
|
|
4
|
+
"version": "0.1.18-rc.7",
|
|
5
|
+
"generatedAt": "2026-08-27T21:09:58.915Z",
|
|
6
|
+
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "skill",
|
|
10
|
+
"name": "agent-common",
|
|
11
|
+
"category": "package-specific",
|
|
12
|
+
"file": "skills/agent-common/SKILL.md",
|
|
13
|
+
"canonicalPath": ".agents/skills/agent-common/SKILL.md"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-common
|
|
3
|
+
description: How to use @owlmeans/agent-common — runtime-free contracts for OwlMeans agents: conversation identity, the run-lifecycle flow, and the record shapes an application persists (conversation events, run state, memory nodes and events). Auto-invoked when importing agent record types, conversationFor, truncateAt, or the agent run flow.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# @owlmeans/agent-common
|
|
9
|
+
|
|
10
|
+
**Layer:** Cross-cutting domain
|
|
11
|
+
**Install:** `"@owlmeans/agent-common": "^0.1.18-rc.7"` in `dependencies`
|
|
12
|
+
|
|
13
|
+
Serializable contracts for the agent family. No LangChain, no LangGraph, no storage driver — a
|
|
14
|
+
backend or a browser bundle imports these to read what an agent wrote without pulling the runtime.
|
|
15
|
+
The runtime is `@owlmeans/agent`.
|
|
16
|
+
|
|
17
|
+
## Key exports
|
|
18
|
+
|
|
19
|
+
| Export | Description |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `conversationFor(purpose, override?)` | Derives a `ConversationRef` from an `LlmPurpose` dedication. |
|
|
22
|
+
| `truncateAt(text, max)` | Boundary-aware truncation. Every character cap in the family lands here. |
|
|
23
|
+
| `agentRunFlow`, `AgentRunStep`, `AgentRunTransition`, `AGENT_RUN_FLOW` | The `@owlmeans/flow` lifecycle a run is driven through. |
|
|
24
|
+
| `agentFlows` | Every flow this package declares, for a provider to serve. |
|
|
25
|
+
| `ConversationEvent`, `ConversationEventInput` | One finished run, compacted: `summary` + `advice`. |
|
|
26
|
+
| `AgentRunState` | Serialized flow plus the execution snapshot — the data plane of a run. |
|
|
27
|
+
| `MemoryNode`, `MemoryEvent` (+ `MemoryEventInput`) | Agent-authored memory records. |
|
|
28
|
+
| `AgentRunMessage` | What a transport carries; execution state travels by reference. |
|
|
29
|
+
| `AGENTS_SERVICE` | The service alias — **`agents`**, plural. |
|
|
30
|
+
| `AGENT_*_STORE` | Port names a consumer binds its storage under. |
|
|
31
|
+
| `AgentRunStatus` | `ok` / `failed`, written on a conversation event. |
|
|
32
|
+
| `AgentCommonError`, `AgentRunStateError` | The error family. |
|
|
33
|
+
|
|
34
|
+
## Rules
|
|
35
|
+
|
|
36
|
+
**Timestamps are ISO strings, never `Date`.** These contracts cross process boundaries and storage
|
|
37
|
+
backends and must survive `JSON.stringify` unchanged. A store whose backend prefers dates converts
|
|
38
|
+
at its own adapter boundary — that is the adapter's job, not the contract's.
|
|
39
|
+
|
|
40
|
+
**The service alias is `agents`, and the accessor is `ctx.agents()`.** Plural and deliberately not
|
|
41
|
+
`agent`: a consuming application very often already has a service of its own called that, and a
|
|
42
|
+
context accessor collision is silent — the second registration simply wins.
|
|
43
|
+
|
|
44
|
+
**Flow steps are lifecycle stages, not conversational turns.** `FlowPayload` holds flat scalars only
|
|
45
|
+
and a ReAct loop's turn count is unbounded, so the loop lives inside the `Working` step and only its
|
|
46
|
+
counter travels in the payload. What the steps buy is knowing where an interrupted run resumes:
|
|
47
|
+
`Working` from the last checkpoint, or `Finalizing` when the loop finished but the compaction never
|
|
48
|
+
committed.
|
|
49
|
+
|
|
50
|
+
**Each working step keeps exactly one non-explicit outgoing transition.** That is what lets
|
|
51
|
+
`FlowModel.next()` give a driver an unambiguous answer without knowing the vocabulary. `Fail` is
|
|
52
|
+
marked `explicit` precisely so it can never become that automatic answer. Adding a second automatic
|
|
53
|
+
edge to any step breaks `next()` for that step.
|
|
54
|
+
|
|
55
|
+
**A conversation id degrades to a named default, never to an empty string.** An empty key would
|
|
56
|
+
silently collapse every run of every subject into one thread.
|
|
57
|
+
|
|
58
|
+
**Port names are exported; ports are not bound here.** An unbound port is not an error — the plugin
|
|
59
|
+
that needs it degrades to a no-op, the way `ExecutionService.checkpoint` does with no plugin
|
|
60
|
+
registered.
|
|
61
|
+
|
|
62
|
+
## Testing
|
|
63
|
+
|
|
64
|
+
Category A (unit, no env, no network). This package's own `tests/flow.spec.ts` covers the flow
|
|
65
|
+
round-trip — which is also the standing check that `@owlmeans/flow` still works server-side, since
|
|
66
|
+
every other consumer of that package is client-side.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent service alias.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately plural, and deliberately not `agent`: a consuming application very often already
|
|
5
|
+
* has a service of its own called that (the viable platform's Kubernetes-facing `AgentService` is
|
|
6
|
+
* registered under `agent`), and a context accessor collision is silent — the second registration
|
|
7
|
+
* simply wins and every later lookup resolves the wrong service.
|
|
8
|
+
*/
|
|
9
|
+
export declare const AGENTS_SERVICE = "agents";
|
|
10
|
+
/**
|
|
11
|
+
* Port names.
|
|
12
|
+
*
|
|
13
|
+
* These are the keys a consumer binds its own storage under; the package never names a storage
|
|
14
|
+
* technology. A port left unbound is not an error — the plugin that needs it degrades to a no-op,
|
|
15
|
+
* the same way `ExecutionService.checkpoint` does with no plugin registered.
|
|
16
|
+
*/
|
|
17
|
+
export declare const AGENT_CONVERSATION_STORE = "agent-conversation-store";
|
|
18
|
+
export declare const AGENT_RUN_STATE_STORE = "agent-run-state-store";
|
|
19
|
+
export declare const AGENT_MEMORY_GRAPH_STORE = "agent-memory-graph-store";
|
|
20
|
+
export declare const AGENT_MEMORY_EVENTS_STORE = "agent-memory-events-store";
|
|
21
|
+
/** The lifecycle flow every agent run is driven through. */
|
|
22
|
+
export declare const AGENT_RUN_FLOW = "agent-run";
|
|
23
|
+
/**
|
|
24
|
+
* The steps of {@link AGENT_RUN_FLOW}.
|
|
25
|
+
*
|
|
26
|
+
* These are recoverable LIFECYCLE stages, not conversational turns. A ReAct loop's turn count is
|
|
27
|
+
* unbounded and its messages are not scalars, while `FlowPayload` holds flat scalars only — so the
|
|
28
|
+
* loop lives inside `Working` and only its counter travels in the payload. What the steps buy is
|
|
29
|
+
* the ability to say where a crashed run has to resume: at `Working` from the last checkpoint, or
|
|
30
|
+
* at `Finalizing` when the loop finished but the compaction never committed.
|
|
31
|
+
*/
|
|
32
|
+
export declare enum AgentRunStep {
|
|
33
|
+
Received = "received",
|
|
34
|
+
Prepared = "prepared",
|
|
35
|
+
Working = "working",
|
|
36
|
+
Finalizing = "finalizing",
|
|
37
|
+
Finished = "finished",
|
|
38
|
+
Failed = "failed"
|
|
39
|
+
}
|
|
40
|
+
/** The transitions of {@link AGENT_RUN_FLOW}. */
|
|
41
|
+
export declare enum AgentRunTransition {
|
|
42
|
+
Prepare = "prepare",
|
|
43
|
+
Work = "work",
|
|
44
|
+
Finalize = "finalize",
|
|
45
|
+
Finish = "finish",
|
|
46
|
+
Fail = "fail",
|
|
47
|
+
Resume = "resume"
|
|
48
|
+
}
|
|
49
|
+
/** How a run ended. Written on the conversation event so a reader can weigh the advice. */
|
|
50
|
+
export declare enum AgentRunStatus {
|
|
51
|
+
Ok = "ok",
|
|
52
|
+
Failed = "failed"
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Default caps.
|
|
56
|
+
*
|
|
57
|
+
* Every one of them is enforced by truncation after the model answers, never by asking the model
|
|
58
|
+
* to obey a limit. A cap in a prompt is a request; a cap in code is a cap.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DEFAULT_SUMMARY_CHARS = 1200;
|
|
61
|
+
export declare const DEFAULT_ADVICE_CHARS = 400;
|
|
62
|
+
export declare const DEFAULT_EVENT_WINDOW = 3;
|
|
63
|
+
export declare const DEFAULT_MEMORY_NODE_CHARS = 2000;
|
|
64
|
+
export declare const DEFAULT_MEMORY_EVENTS_LIMIT = 50;
|
|
65
|
+
/** Separator between a dedication's kind and its target — `project:<id>`. */
|
|
66
|
+
export declare const SCOPE_SEP = ":";
|
|
67
|
+
//# sourceMappingURL=consts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,WAAW,CAAA;AAEtC;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,6BAA6B,CAAA;AAClE,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,wBAAwB,6BAA6B,CAAA;AAClE,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AAEpE,4DAA4D;AAC5D,eAAO,MAAM,cAAc,cAAc,CAAA;AAEzC;;;;;;;;GAQG;AACH,oBAAY,YAAY;IACtB,QAAQ,aAAa;IACrB,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,UAAU,eAAe;IACzB,QAAQ,aAAa;IACrB,MAAM,WAAW;CAClB;AAED,iDAAiD;AACjD,oBAAY,kBAAkB;IAC5B,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,QAAQ,aAAa;IACrB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,2FAA2F;AAC3F,oBAAY,cAAc;IACxB,EAAE,OAAO;IACT,MAAM,WAAW;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAA;AACzC,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,eAAO,MAAM,oBAAoB,IAAI,CAAA;AACrC,eAAO,MAAM,yBAAyB,OAAO,CAAA;AAC7C,eAAO,MAAM,2BAA2B,KAAK,CAAA;AAE7C,6EAA6E;AAC7E,eAAO,MAAM,SAAS,MAAM,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent service alias.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately plural, and deliberately not `agent`: a consuming application very often already
|
|
5
|
+
* has a service of its own called that (the viable platform's Kubernetes-facing `AgentService` is
|
|
6
|
+
* registered under `agent`), and a context accessor collision is silent — the second registration
|
|
7
|
+
* simply wins and every later lookup resolves the wrong service.
|
|
8
|
+
*/
|
|
9
|
+
export const AGENTS_SERVICE = 'agents';
|
|
10
|
+
/**
|
|
11
|
+
* Port names.
|
|
12
|
+
*
|
|
13
|
+
* These are the keys a consumer binds its own storage under; the package never names a storage
|
|
14
|
+
* technology. A port left unbound is not an error — the plugin that needs it degrades to a no-op,
|
|
15
|
+
* the same way `ExecutionService.checkpoint` does with no plugin registered.
|
|
16
|
+
*/
|
|
17
|
+
export const AGENT_CONVERSATION_STORE = 'agent-conversation-store';
|
|
18
|
+
export const AGENT_RUN_STATE_STORE = 'agent-run-state-store';
|
|
19
|
+
export const AGENT_MEMORY_GRAPH_STORE = 'agent-memory-graph-store';
|
|
20
|
+
export const AGENT_MEMORY_EVENTS_STORE = 'agent-memory-events-store';
|
|
21
|
+
/** The lifecycle flow every agent run is driven through. */
|
|
22
|
+
export const AGENT_RUN_FLOW = 'agent-run';
|
|
23
|
+
/**
|
|
24
|
+
* The steps of {@link AGENT_RUN_FLOW}.
|
|
25
|
+
*
|
|
26
|
+
* These are recoverable LIFECYCLE stages, not conversational turns. A ReAct loop's turn count is
|
|
27
|
+
* unbounded and its messages are not scalars, while `FlowPayload` holds flat scalars only — so the
|
|
28
|
+
* loop lives inside `Working` and only its counter travels in the payload. What the steps buy is
|
|
29
|
+
* the ability to say where a crashed run has to resume: at `Working` from the last checkpoint, or
|
|
30
|
+
* at `Finalizing` when the loop finished but the compaction never committed.
|
|
31
|
+
*/
|
|
32
|
+
export var AgentRunStep;
|
|
33
|
+
(function (AgentRunStep) {
|
|
34
|
+
AgentRunStep["Received"] = "received";
|
|
35
|
+
AgentRunStep["Prepared"] = "prepared";
|
|
36
|
+
AgentRunStep["Working"] = "working";
|
|
37
|
+
AgentRunStep["Finalizing"] = "finalizing";
|
|
38
|
+
AgentRunStep["Finished"] = "finished";
|
|
39
|
+
AgentRunStep["Failed"] = "failed";
|
|
40
|
+
})(AgentRunStep || (AgentRunStep = {}));
|
|
41
|
+
/** The transitions of {@link AGENT_RUN_FLOW}. */
|
|
42
|
+
export var AgentRunTransition;
|
|
43
|
+
(function (AgentRunTransition) {
|
|
44
|
+
AgentRunTransition["Prepare"] = "prepare";
|
|
45
|
+
AgentRunTransition["Work"] = "work";
|
|
46
|
+
AgentRunTransition["Finalize"] = "finalize";
|
|
47
|
+
AgentRunTransition["Finish"] = "finish";
|
|
48
|
+
AgentRunTransition["Fail"] = "fail";
|
|
49
|
+
AgentRunTransition["Resume"] = "resume";
|
|
50
|
+
})(AgentRunTransition || (AgentRunTransition = {}));
|
|
51
|
+
/** How a run ended. Written on the conversation event so a reader can weigh the advice. */
|
|
52
|
+
export var AgentRunStatus;
|
|
53
|
+
(function (AgentRunStatus) {
|
|
54
|
+
AgentRunStatus["Ok"] = "ok";
|
|
55
|
+
AgentRunStatus["Failed"] = "failed";
|
|
56
|
+
})(AgentRunStatus || (AgentRunStatus = {}));
|
|
57
|
+
/**
|
|
58
|
+
* Default caps.
|
|
59
|
+
*
|
|
60
|
+
* Every one of them is enforced by truncation after the model answers, never by asking the model
|
|
61
|
+
* to obey a limit. A cap in a prompt is a request; a cap in code is a cap.
|
|
62
|
+
*/
|
|
63
|
+
export const DEFAULT_SUMMARY_CHARS = 1200;
|
|
64
|
+
export const DEFAULT_ADVICE_CHARS = 400;
|
|
65
|
+
export const DEFAULT_EVENT_WINDOW = 3;
|
|
66
|
+
export const DEFAULT_MEMORY_NODE_CHARS = 2000;
|
|
67
|
+
export const DEFAULT_MEMORY_EVENTS_LIMIT = 50;
|
|
68
|
+
/** Separator between a dedication's kind and its target — `project:<id>`. */
|
|
69
|
+
export const SCOPE_SEP = ':';
|
|
70
|
+
//# sourceMappingURL=consts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA;AAEtC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAA;AAClE,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAA;AAClE,MAAM,CAAC,MAAM,yBAAyB,GAAG,2BAA2B,CAAA;AAEpE,4DAA4D;AAC5D,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAA;AAEzC;;;;;;;;GAQG;AACH,MAAM,CAAN,IAAY,YAOX;AAPD,WAAY,YAAY;IACtB,qCAAqB,CAAA;IACrB,qCAAqB,CAAA;IACrB,mCAAmB,CAAA;IACnB,yCAAyB,CAAA;IACzB,qCAAqB,CAAA;IACrB,iCAAiB,CAAA;AACnB,CAAC,EAPW,YAAY,KAAZ,YAAY,QAOvB;AAED,iDAAiD;AACjD,MAAM,CAAN,IAAY,kBAOX;AAPD,WAAY,kBAAkB;IAC5B,yCAAmB,CAAA;IACnB,mCAAa,CAAA;IACb,2CAAqB,CAAA;IACrB,uCAAiB,CAAA;IACjB,mCAAa,CAAA;IACb,uCAAiB,CAAA;AACnB,CAAC,EAPW,kBAAkB,KAAlB,kBAAkB,QAO7B;AAED,2FAA2F;AAC3F,MAAM,CAAN,IAAY,cAGX;AAHD,WAAY,cAAc;IACxB,2BAAS,CAAA;IACT,mCAAiB,CAAA;AACnB,CAAC,EAHW,cAAc,KAAd,cAAc,QAGzB;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAA;AACzC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAA;AACvC,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAA;AACrC,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAA;AAC7C,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAA;AAE7C,6EAA6E;AAC7E,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
export declare class AgentCommonError extends ResilientError {
|
|
3
|
+
static typeName: string;
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
6
|
+
/** A run was asked to advance along a transition its current step does not offer. */
|
|
7
|
+
export declare class AgentRunStateError extends AgentCommonError {
|
|
8
|
+
static typeName: string;
|
|
9
|
+
constructor(message?: string);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,gBAAiB,SAAQ,cAAc;IAClD,OAAuB,QAAQ,EAAE,MAAM,CAAoC;IAE3E,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF;AAED,qFAAqF;AACrF,qBAAa,kBAAmB,SAAQ,gBAAgB;IACtD,OAAuB,QAAQ,EAAE,MAAM,CAAyC;IAEhF,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF"}
|
package/build/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
export class AgentCommonError extends ResilientError {
|
|
3
|
+
static typeName = `Agent${ResilientError.typeName}`;
|
|
4
|
+
constructor(message = 'error') {
|
|
5
|
+
super(AgentCommonError.typeName, `agent:${message}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** A run was asked to advance along a transition its current step does not offer. */
|
|
9
|
+
export class AgentRunStateError extends AgentCommonError {
|
|
10
|
+
static typeName = `RunState${AgentCommonError.typeName}`;
|
|
11
|
+
constructor(message = 'error') {
|
|
12
|
+
super(`run-state:${message}`);
|
|
13
|
+
this.type = AgentRunStateError.typeName;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
ResilientError.registerErrorClass(AgentCommonError);
|
|
17
|
+
ResilientError.registerErrorClass(AgentRunStateError);
|
|
18
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,gBAAiB,SAAQ,cAAc;IAC3C,MAAM,CAAU,QAAQ,GAAW,QAAQ,cAAc,CAAC,QAAQ,EAAE,CAAA;IAE3E,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC,CAAA;IACtD,CAAC;CACF;AAED,qFAAqF;AACrF,MAAM,OAAO,kBAAmB,SAAQ,gBAAgB;IAC/C,MAAM,CAAU,QAAQ,GAAW,WAAW,gBAAgB,CAAC,QAAQ,EAAE,CAAA;IAEhF,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,aAAa,OAAO,EAAE,CAAC,CAAA;QAC7B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAA;IACzC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAA;AACnD,cAAc,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAA"}
|
package/build/flows.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ShallowFlow } from '@owlmeans/flow';
|
|
2
|
+
/**
|
|
3
|
+
* The lifecycle of one agent run.
|
|
4
|
+
*
|
|
5
|
+
* Read it as "how far did this run get", not "what did it say". Everything conversational happens
|
|
6
|
+
* inside `Working`; the steps exist so that a run interrupted anywhere can be told where to pick
|
|
7
|
+
* up. `Fail` is reachable from every working step, and `Resume` re-enters `Working` from a
|
|
8
|
+
* checkpoint.
|
|
9
|
+
*
|
|
10
|
+
* `service` is left as the flow name on every step. A flow step normally binds to a service or an
|
|
11
|
+
* entrypoint, but an agent run is driven by whoever holds the model — there is no second party to
|
|
12
|
+
* hand control to, and inventing one would put a name in the serialized state that nothing
|
|
13
|
+
* resolves.
|
|
14
|
+
*
|
|
15
|
+
* Each working step keeps exactly ONE non-explicit outgoing transition, so `FlowModel.next()`
|
|
16
|
+
* always has an unambiguous answer: that is what lets a driver advance the run without knowing the
|
|
17
|
+
* vocabulary. `Fail` is marked explicit precisely so it never becomes that automatic answer.
|
|
18
|
+
*/
|
|
19
|
+
export declare const agentRunFlow: ShallowFlow;
|
|
20
|
+
/** Every flow this package declares, for a provider to serve. */
|
|
21
|
+
export declare const agentFlows: ShallowFlow[];
|
|
22
|
+
//# sourceMappingURL=flows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../src/flows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAGjD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,EAAE,WA8F1B,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,UAAU,EAAE,WAAW,EAAmB,CAAA"}
|
package/build/flows.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AGENT_RUN_FLOW, AgentRunStep, AgentRunTransition } from './consts.js';
|
|
2
|
+
/**
|
|
3
|
+
* The lifecycle of one agent run.
|
|
4
|
+
*
|
|
5
|
+
* Read it as "how far did this run get", not "what did it say". Everything conversational happens
|
|
6
|
+
* inside `Working`; the steps exist so that a run interrupted anywhere can be told where to pick
|
|
7
|
+
* up. `Fail` is reachable from every working step, and `Resume` re-enters `Working` from a
|
|
8
|
+
* checkpoint.
|
|
9
|
+
*
|
|
10
|
+
* `service` is left as the flow name on every step. A flow step normally binds to a service or an
|
|
11
|
+
* entrypoint, but an agent run is driven by whoever holds the model — there is no second party to
|
|
12
|
+
* hand control to, and inventing one would put a name in the serialized state that nothing
|
|
13
|
+
* resolves.
|
|
14
|
+
*
|
|
15
|
+
* Each working step keeps exactly ONE non-explicit outgoing transition, so `FlowModel.next()`
|
|
16
|
+
* always has an unambiguous answer: that is what lets a driver advance the run without knowing the
|
|
17
|
+
* vocabulary. `Fail` is marked explicit precisely so it never becomes that automatic answer.
|
|
18
|
+
*/
|
|
19
|
+
export const agentRunFlow = {
|
|
20
|
+
flow: AGENT_RUN_FLOW,
|
|
21
|
+
initialStep: AgentRunStep.Received,
|
|
22
|
+
steps: {
|
|
23
|
+
[AgentRunStep.Received]: {
|
|
24
|
+
index: 0,
|
|
25
|
+
step: AgentRunStep.Received,
|
|
26
|
+
service: AGENT_RUN_FLOW,
|
|
27
|
+
initial: true,
|
|
28
|
+
transitions: {
|
|
29
|
+
[AgentRunTransition.Prepare]: {
|
|
30
|
+
transition: AgentRunTransition.Prepare,
|
|
31
|
+
step: AgentRunStep.Prepared,
|
|
32
|
+
},
|
|
33
|
+
[AgentRunTransition.Fail]: {
|
|
34
|
+
transition: AgentRunTransition.Fail,
|
|
35
|
+
step: AgentRunStep.Failed,
|
|
36
|
+
explicit: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
[AgentRunStep.Prepared]: {
|
|
41
|
+
index: 1,
|
|
42
|
+
step: AgentRunStep.Prepared,
|
|
43
|
+
service: AGENT_RUN_FLOW,
|
|
44
|
+
transitions: {
|
|
45
|
+
[AgentRunTransition.Work]: {
|
|
46
|
+
transition: AgentRunTransition.Work,
|
|
47
|
+
step: AgentRunStep.Working,
|
|
48
|
+
},
|
|
49
|
+
[AgentRunTransition.Fail]: {
|
|
50
|
+
transition: AgentRunTransition.Fail,
|
|
51
|
+
step: AgentRunStep.Failed,
|
|
52
|
+
explicit: true,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
[AgentRunStep.Working]: {
|
|
57
|
+
index: 2,
|
|
58
|
+
step: AgentRunStep.Working,
|
|
59
|
+
service: AGENT_RUN_FLOW,
|
|
60
|
+
transitions: {
|
|
61
|
+
[AgentRunTransition.Finalize]: {
|
|
62
|
+
transition: AgentRunTransition.Finalize,
|
|
63
|
+
step: AgentRunStep.Finalizing,
|
|
64
|
+
},
|
|
65
|
+
[AgentRunTransition.Fail]: {
|
|
66
|
+
transition: AgentRunTransition.Fail,
|
|
67
|
+
step: AgentRunStep.Failed,
|
|
68
|
+
explicit: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
[AgentRunStep.Finalizing]: {
|
|
73
|
+
index: 3,
|
|
74
|
+
step: AgentRunStep.Finalizing,
|
|
75
|
+
service: AGENT_RUN_FLOW,
|
|
76
|
+
transitions: {
|
|
77
|
+
[AgentRunTransition.Finish]: {
|
|
78
|
+
transition: AgentRunTransition.Finish,
|
|
79
|
+
step: AgentRunStep.Finished,
|
|
80
|
+
},
|
|
81
|
+
[AgentRunTransition.Fail]: {
|
|
82
|
+
transition: AgentRunTransition.Fail,
|
|
83
|
+
step: AgentRunStep.Failed,
|
|
84
|
+
explicit: true,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
[AgentRunStep.Finished]: {
|
|
89
|
+
index: 4,
|
|
90
|
+
step: AgentRunStep.Finished,
|
|
91
|
+
service: AGENT_RUN_FLOW,
|
|
92
|
+
transitions: {},
|
|
93
|
+
},
|
|
94
|
+
[AgentRunStep.Failed]: {
|
|
95
|
+
index: 5,
|
|
96
|
+
step: AgentRunStep.Failed,
|
|
97
|
+
service: AGENT_RUN_FLOW,
|
|
98
|
+
transitions: {
|
|
99
|
+
[AgentRunTransition.Resume]: {
|
|
100
|
+
transition: AgentRunTransition.Resume,
|
|
101
|
+
step: AgentRunStep.Working,
|
|
102
|
+
explicit: true,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
/** Every flow this package declares, for a provider to serve. */
|
|
109
|
+
export const agentFlows = [agentRunFlow];
|
|
110
|
+
//# sourceMappingURL=flows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flows.js","sourceRoot":"","sources":["../src/flows.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAE9E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAgB;IACvC,IAAI,EAAE,cAAc;IACpB,WAAW,EAAE,YAAY,CAAC,QAAQ;IAElC,KAAK,EAAE;QACL,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE;YACvB,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,YAAY,CAAC,QAAQ;YAC3B,OAAO,EAAE,cAAc;YACvB,OAAO,EAAE,IAAI;YACb,WAAW,EAAE;gBACX,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE;oBAC5B,UAAU,EAAE,kBAAkB,CAAC,OAAO;oBACtC,IAAI,EAAE,YAAY,CAAC,QAAQ;iBAC5B;gBACD,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE;oBACzB,UAAU,EAAE,kBAAkB,CAAC,IAAI;oBACnC,IAAI,EAAE,YAAY,CAAC,MAAM;oBACzB,QAAQ,EAAE,IAAI;iBACf;aACF;SACF;QAED,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE;YACvB,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,YAAY,CAAC,QAAQ;YAC3B,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE;gBACX,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE;oBACzB,UAAU,EAAE,kBAAkB,CAAC,IAAI;oBACnC,IAAI,EAAE,YAAY,CAAC,OAAO;iBAC3B;gBACD,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE;oBACzB,UAAU,EAAE,kBAAkB,CAAC,IAAI;oBACnC,IAAI,EAAE,YAAY,CAAC,MAAM;oBACzB,QAAQ,EAAE,IAAI;iBACf;aACF;SACF;QAED,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE;YACtB,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,YAAY,CAAC,OAAO;YAC1B,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE;gBACX,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE;oBAC7B,UAAU,EAAE,kBAAkB,CAAC,QAAQ;oBACvC,IAAI,EAAE,YAAY,CAAC,UAAU;iBAC9B;gBACD,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE;oBACzB,UAAU,EAAE,kBAAkB,CAAC,IAAI;oBACnC,IAAI,EAAE,YAAY,CAAC,MAAM;oBACzB,QAAQ,EAAE,IAAI;iBACf;aACF;SACF;QAED,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE;YACzB,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,YAAY,CAAC,UAAU;YAC7B,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE;gBACX,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE;oBAC3B,UAAU,EAAE,kBAAkB,CAAC,MAAM;oBACrC,IAAI,EAAE,YAAY,CAAC,QAAQ;iBAC5B;gBACD,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE;oBACzB,UAAU,EAAE,kBAAkB,CAAC,IAAI;oBACnC,IAAI,EAAE,YAAY,CAAC,MAAM;oBACzB,QAAQ,EAAE,IAAI;iBACf;aACF;SACF;QAED,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE;YACvB,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,YAAY,CAAC,QAAQ;YAC3B,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE,EAAE;SAChB;QAED,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE;YACrB,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,YAAY,CAAC,MAAM;YACzB,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE;gBACX,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE;oBAC3B,UAAU,EAAE,kBAAkB,CAAC,MAAM;oBACrC,IAAI,EAAE,YAAY,CAAC,OAAO;oBAC1B,QAAQ,EAAE,IAAI;iBACf;aACF;SACF;KACF;CACF,CAAA;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,UAAU,GAAkB,CAAC,YAAY,CAAC,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LlmPurpose } from '@owlmeans/llm-common';
|
|
2
|
+
import type { ConversationRef } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The conversation a run belongs to.
|
|
5
|
+
*
|
|
6
|
+
* An LLM purpose already carries the only correlation key most applications have — `dedication`,
|
|
7
|
+
* conventionally `<kind>:<id>`. Deriving the conversation from it means an application gets
|
|
8
|
+
* continuity without inventing and threading a second identifier, and two runs dedicated to the
|
|
9
|
+
* same subject land in the same conversation by construction.
|
|
10
|
+
*
|
|
11
|
+
* The scope is the dedication's TARGET rather than the whole string, so conversations addressed at
|
|
12
|
+
* different granularities (a project, one of its stories) still share the subject their memory is
|
|
13
|
+
* filed under. Both halves can be overridden for an application whose threads are not one per
|
|
14
|
+
* dedication.
|
|
15
|
+
*/
|
|
16
|
+
export declare const conversationFor: (purpose: LlmPurpose | undefined, override?: Partial<ConversationRef>) => ConversationRef;
|
|
17
|
+
/**
|
|
18
|
+
* Cut `text` to `max` characters on a boundary a reader will not trip over.
|
|
19
|
+
*
|
|
20
|
+
* Every cap in this package lands here, because a model asked for "at most N characters" answers
|
|
21
|
+
* with N plus whatever it felt was needed. Truncating mid-word reads as corruption and truncating
|
|
22
|
+
* mid-sentence reads as a bug report, so the cut prefers the last paragraph, then line, then
|
|
23
|
+
* sentence, then word break inside the last quarter of the budget, and always marks itself.
|
|
24
|
+
*/
|
|
25
|
+
export declare const truncateAt: (text: string, max: number) => string;
|
|
26
|
+
//# sourceMappingURL=conversation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/helpers/conversation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,YACjB,UAAU,GAAG,SAAS,aACpB,OAAO,CAAC,eAAe,CAAC,KAClC,eASF,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,SAAU,MAAM,OAAO,MAAM,KAAG,MAsBtD,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SCOPE_SEP } from '../consts.js';
|
|
2
|
+
/**
|
|
3
|
+
* The conversation a run belongs to.
|
|
4
|
+
*
|
|
5
|
+
* An LLM purpose already carries the only correlation key most applications have — `dedication`,
|
|
6
|
+
* conventionally `<kind>:<id>`. Deriving the conversation from it means an application gets
|
|
7
|
+
* continuity without inventing and threading a second identifier, and two runs dedicated to the
|
|
8
|
+
* same subject land in the same conversation by construction.
|
|
9
|
+
*
|
|
10
|
+
* The scope is the dedication's TARGET rather than the whole string, so conversations addressed at
|
|
11
|
+
* different granularities (a project, one of its stories) still share the subject their memory is
|
|
12
|
+
* filed under. Both halves can be overridden for an application whose threads are not one per
|
|
13
|
+
* dedication.
|
|
14
|
+
*/
|
|
15
|
+
export const conversationFor = (purpose, override) => {
|
|
16
|
+
const dedication = purpose?.dedication ?? '';
|
|
17
|
+
const separator = dedication.indexOf(SCOPE_SEP);
|
|
18
|
+
const target = separator < 0 ? dedication : dedication.slice(separator + 1);
|
|
19
|
+
return {
|
|
20
|
+
conversationId: override?.conversationId ?? (dedication !== '' ? dedication : 'anonymous'),
|
|
21
|
+
scope: override?.scope ?? (target !== '' ? target : 'anonymous'),
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Cut `text` to `max` characters on a boundary a reader will not trip over.
|
|
26
|
+
*
|
|
27
|
+
* Every cap in this package lands here, because a model asked for "at most N characters" answers
|
|
28
|
+
* with N plus whatever it felt was needed. Truncating mid-word reads as corruption and truncating
|
|
29
|
+
* mid-sentence reads as a bug report, so the cut prefers the last paragraph, then line, then
|
|
30
|
+
* sentence, then word break inside the last quarter of the budget, and always marks itself.
|
|
31
|
+
*/
|
|
32
|
+
export const truncateAt = (text, max) => {
|
|
33
|
+
const trimmed = text.trim();
|
|
34
|
+
if (trimmed.length <= max) {
|
|
35
|
+
return trimmed;
|
|
36
|
+
}
|
|
37
|
+
if (max <= 1) {
|
|
38
|
+
return trimmed.slice(0, Math.max(0, max));
|
|
39
|
+
}
|
|
40
|
+
const ellipsis = '…';
|
|
41
|
+
const budget = max - ellipsis.length;
|
|
42
|
+
const head = trimmed.slice(0, budget);
|
|
43
|
+
const floor = Math.floor(budget * 0.75);
|
|
44
|
+
for (const boundary of ['\n\n', '\n', '. ', ' ']) {
|
|
45
|
+
const at = head.lastIndexOf(boundary);
|
|
46
|
+
if (at >= floor) {
|
|
47
|
+
return head.slice(0, boundary === '. ' ? at + 1 : at).trimEnd() + ellipsis;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return head.trimEnd() + ellipsis;
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=conversation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation.js","sourceRoot":"","sources":["../../src/helpers/conversation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,OAA+B,EAC/B,QAAmC,EAClB,EAAE;IACnB,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,EAAE,CAAA;IAC5C,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAA;IAE3E,OAAO;QACL,cAAc,EAAE,QAAQ,EAAE,cAAc,IAAI,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC;QAC1F,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;KACjE,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,GAAW,EAAU,EAAE;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IAC3B,IAAI,OAAO,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QAC1B,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACb,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;IAC3C,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,CAAA;IACpB,MAAM,MAAM,GAAG,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAA;IACpC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAEvC,KAAK,MAAM,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAA;QACrC,IAAI,EAAE,IAAI,KAAK,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAA;QAC5E,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAA;AAClC,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,mBAAmB,YAAY,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,2BAA2B,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAE3B,cAAc,YAAY,CAAA;AAC1B,cAAc,2BAA2B,CAAA"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ResourceRecord } from '@owlmeans/resource';
|
|
2
|
+
import type { ExecutionState } from '@owlmeans/llm-common';
|
|
3
|
+
import type { AgentRunStatus } from './consts.js';
|
|
4
|
+
/**
|
|
5
|
+
* What a conversation is, as an address.
|
|
6
|
+
*
|
|
7
|
+
* `conversationId` is the thread a run belongs to; `scope` is the wider subject the thread is
|
|
8
|
+
* about — for a project-dedicated agent the two coincide, but memory is scoped per project while
|
|
9
|
+
* conversations may be finer-grained, so they are separate fields rather than one.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConversationRef {
|
|
12
|
+
conversationId: string;
|
|
13
|
+
scope: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One finished run, compacted.
|
|
17
|
+
*
|
|
18
|
+
* Two parts on purpose: `summary` says what happened, `advice` says what to do next. The second is
|
|
19
|
+
* what an agent actually needs on the way in — a summary alone leaves the next run to re-derive
|
|
20
|
+
* the plan from the outcome, which is where it invents a different one.
|
|
21
|
+
*
|
|
22
|
+
* Timestamps are ISO strings, never `Date`: these contracts cross process boundaries and storage
|
|
23
|
+
* backends, and must survive `JSON.stringify` unchanged. A consumer whose store prefers dates maps
|
|
24
|
+
* them at its own adapter boundary.
|
|
25
|
+
*/
|
|
26
|
+
export interface ConversationEvent extends ResourceRecord {
|
|
27
|
+
conversationId: string;
|
|
28
|
+
scope: string;
|
|
29
|
+
/** Monotonic within a conversation, allocated by the store. */
|
|
30
|
+
seq: number;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
/** The ask that opened the run, truncated. Present so a reader can see what was attempted. */
|
|
33
|
+
prompt?: string;
|
|
34
|
+
summary: string;
|
|
35
|
+
advice?: string;
|
|
36
|
+
status: AgentRunStatus;
|
|
37
|
+
}
|
|
38
|
+
/** What a store is asked to append; `seq` and `id` are the store's to allocate. */
|
|
39
|
+
export interface ConversationEventInput extends Omit<ConversationEvent, 'id' | 'seq' | 'createdAt'> {
|
|
40
|
+
createdAt?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The data plane of a run.
|
|
44
|
+
*
|
|
45
|
+
* `flow` is the serialized {@link import('@owlmeans/flow').FlowModel} state — the control plane
|
|
46
|
+
* collapsed to a string — and `state` is the execution snapshot the LLM layer produces. Keeping
|
|
47
|
+
* them in one record is what makes a resume a single read.
|
|
48
|
+
*/
|
|
49
|
+
export interface AgentRunState extends ResourceRecord {
|
|
50
|
+
id: string;
|
|
51
|
+
conversationId: string;
|
|
52
|
+
flow: string;
|
|
53
|
+
state: ExecutionState;
|
|
54
|
+
updatedAt: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A node of the subsystem memory graph.
|
|
58
|
+
*
|
|
59
|
+
* `subsystem` is the lookup key within a scope, and `links` are the other subsystems this one
|
|
60
|
+
* refers to. The graph is deliberately shallow: agents read a node and follow a link or two, they
|
|
61
|
+
* do not traverse.
|
|
62
|
+
*/
|
|
63
|
+
export interface MemoryNode extends ResourceRecord {
|
|
64
|
+
scope: string;
|
|
65
|
+
subsystem: string;
|
|
66
|
+
content: string;
|
|
67
|
+
links: string[];
|
|
68
|
+
updatedAt: string;
|
|
69
|
+
}
|
|
70
|
+
/** An entry in the bounded event-sequence memory. */
|
|
71
|
+
export interface MemoryEvent extends ResourceRecord {
|
|
72
|
+
scope: string;
|
|
73
|
+
/** Monotonic within a scope, allocated by the store. */
|
|
74
|
+
seq: number;
|
|
75
|
+
kind: string;
|
|
76
|
+
content: string;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
}
|
|
79
|
+
/** What a store is asked to append; `seq` and `id` are the store's to allocate. */
|
|
80
|
+
export interface MemoryEventInput extends Omit<MemoryEvent, 'id' | 'seq' | 'createdAt'> {
|
|
81
|
+
createdAt?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* What a transport carries.
|
|
85
|
+
*
|
|
86
|
+
* The execution state travels by REFERENCE (`stateRef`), not by value: a project execution's state
|
|
87
|
+
* holds the whole project specification, and a queue whose messages carry that is a queue that
|
|
88
|
+
* falls over on the first large project. The flow string is small enough to inline, and it is what
|
|
89
|
+
* a consumer needs to route the message before it reads anything.
|
|
90
|
+
*/
|
|
91
|
+
export interface AgentRunMessage {
|
|
92
|
+
id: string;
|
|
93
|
+
conversationId: string;
|
|
94
|
+
flow: string;
|
|
95
|
+
stateRef?: string;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,cAAc,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,cAAc,CAAA;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,IAAI,GAAG,KAAK,GAAG,WAAW,CAAC;IACjG,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,cAAc,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAW,SAAQ,cAAc;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,qDAAqD;AACrD,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,KAAK,GAAG,WAAW,CAAC;IACrF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/agent-common",
|
|
3
|
+
"version": "0.1.18-rc.7",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -b",
|
|
8
|
+
"dev": "sleep 174 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
10
|
+
"test": "bun test ./tests"
|
|
11
|
+
},
|
|
12
|
+
"main": "build/index.js",
|
|
13
|
+
"module": "build/index.js",
|
|
14
|
+
"types": "build/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./build/index.js",
|
|
18
|
+
"require": "./build/index.js",
|
|
19
|
+
"default": "./build/index.js",
|
|
20
|
+
"module": "./build/index.js",
|
|
21
|
+
"types": "./build/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
26
|
+
"@types/bun": "^1.3.14",
|
|
27
|
+
"@types/node": "^26.1.0",
|
|
28
|
+
"nodemon": "^3.1.14",
|
|
29
|
+
"typescript": "^7.0.2"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@owlmeans/error": "^0.1.18-rc.6",
|
|
33
|
+
"@owlmeans/flow": "^0.1.18-rc.6",
|
|
34
|
+
"@owlmeans/llm-common": "^0.1.18-rc.6",
|
|
35
|
+
"@owlmeans/resource": "^0.1.18-rc.6"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent service alias.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately plural, and deliberately not `agent`: a consuming application very often already
|
|
5
|
+
* has a service of its own called that (the viable platform's Kubernetes-facing `AgentService` is
|
|
6
|
+
* registered under `agent`), and a context accessor collision is silent — the second registration
|
|
7
|
+
* simply wins and every later lookup resolves the wrong service.
|
|
8
|
+
*/
|
|
9
|
+
export const AGENTS_SERVICE = 'agents'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Port names.
|
|
13
|
+
*
|
|
14
|
+
* These are the keys a consumer binds its own storage under; the package never names a storage
|
|
15
|
+
* technology. A port left unbound is not an error — the plugin that needs it degrades to a no-op,
|
|
16
|
+
* the same way `ExecutionService.checkpoint` does with no plugin registered.
|
|
17
|
+
*/
|
|
18
|
+
export const AGENT_CONVERSATION_STORE = 'agent-conversation-store'
|
|
19
|
+
export const AGENT_RUN_STATE_STORE = 'agent-run-state-store'
|
|
20
|
+
export const AGENT_MEMORY_GRAPH_STORE = 'agent-memory-graph-store'
|
|
21
|
+
export const AGENT_MEMORY_EVENTS_STORE = 'agent-memory-events-store'
|
|
22
|
+
|
|
23
|
+
/** The lifecycle flow every agent run is driven through. */
|
|
24
|
+
export const AGENT_RUN_FLOW = 'agent-run'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The steps of {@link AGENT_RUN_FLOW}.
|
|
28
|
+
*
|
|
29
|
+
* These are recoverable LIFECYCLE stages, not conversational turns. A ReAct loop's turn count is
|
|
30
|
+
* unbounded and its messages are not scalars, while `FlowPayload` holds flat scalars only — so the
|
|
31
|
+
* loop lives inside `Working` and only its counter travels in the payload. What the steps buy is
|
|
32
|
+
* the ability to say where a crashed run has to resume: at `Working` from the last checkpoint, or
|
|
33
|
+
* at `Finalizing` when the loop finished but the compaction never committed.
|
|
34
|
+
*/
|
|
35
|
+
export enum AgentRunStep {
|
|
36
|
+
Received = 'received',
|
|
37
|
+
Prepared = 'prepared',
|
|
38
|
+
Working = 'working',
|
|
39
|
+
Finalizing = 'finalizing',
|
|
40
|
+
Finished = 'finished',
|
|
41
|
+
Failed = 'failed',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The transitions of {@link AGENT_RUN_FLOW}. */
|
|
45
|
+
export enum AgentRunTransition {
|
|
46
|
+
Prepare = 'prepare',
|
|
47
|
+
Work = 'work',
|
|
48
|
+
Finalize = 'finalize',
|
|
49
|
+
Finish = 'finish',
|
|
50
|
+
Fail = 'fail',
|
|
51
|
+
Resume = 'resume',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** How a run ended. Written on the conversation event so a reader can weigh the advice. */
|
|
55
|
+
export enum AgentRunStatus {
|
|
56
|
+
Ok = 'ok',
|
|
57
|
+
Failed = 'failed',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default caps.
|
|
62
|
+
*
|
|
63
|
+
* Every one of them is enforced by truncation after the model answers, never by asking the model
|
|
64
|
+
* to obey a limit. A cap in a prompt is a request; a cap in code is a cap.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_SUMMARY_CHARS = 1200
|
|
67
|
+
export const DEFAULT_ADVICE_CHARS = 400
|
|
68
|
+
export const DEFAULT_EVENT_WINDOW = 3
|
|
69
|
+
export const DEFAULT_MEMORY_NODE_CHARS = 2000
|
|
70
|
+
export const DEFAULT_MEMORY_EVENTS_LIMIT = 50
|
|
71
|
+
|
|
72
|
+
/** Separator between a dedication's kind and its target — `project:<id>`. */
|
|
73
|
+
export const SCOPE_SEP = ':'
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error'
|
|
2
|
+
|
|
3
|
+
export class AgentCommonError extends ResilientError {
|
|
4
|
+
public static override typeName: string = `Agent${ResilientError.typeName}`
|
|
5
|
+
|
|
6
|
+
constructor(message: string = 'error') {
|
|
7
|
+
super(AgentCommonError.typeName, `agent:${message}`)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A run was asked to advance along a transition its current step does not offer. */
|
|
12
|
+
export class AgentRunStateError extends AgentCommonError {
|
|
13
|
+
public static override typeName: string = `RunState${AgentCommonError.typeName}`
|
|
14
|
+
|
|
15
|
+
constructor(message: string = 'error') {
|
|
16
|
+
super(`run-state:${message}`)
|
|
17
|
+
this.type = AgentRunStateError.typeName
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ResilientError.registerErrorClass(AgentCommonError)
|
|
22
|
+
ResilientError.registerErrorClass(AgentRunStateError)
|
package/src/flows.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ShallowFlow } from '@owlmeans/flow'
|
|
2
|
+
import { AGENT_RUN_FLOW, AgentRunStep, AgentRunTransition } from './consts.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The lifecycle of one agent run.
|
|
6
|
+
*
|
|
7
|
+
* Read it as "how far did this run get", not "what did it say". Everything conversational happens
|
|
8
|
+
* inside `Working`; the steps exist so that a run interrupted anywhere can be told where to pick
|
|
9
|
+
* up. `Fail` is reachable from every working step, and `Resume` re-enters `Working` from a
|
|
10
|
+
* checkpoint.
|
|
11
|
+
*
|
|
12
|
+
* `service` is left as the flow name on every step. A flow step normally binds to a service or an
|
|
13
|
+
* entrypoint, but an agent run is driven by whoever holds the model — there is no second party to
|
|
14
|
+
* hand control to, and inventing one would put a name in the serialized state that nothing
|
|
15
|
+
* resolves.
|
|
16
|
+
*
|
|
17
|
+
* Each working step keeps exactly ONE non-explicit outgoing transition, so `FlowModel.next()`
|
|
18
|
+
* always has an unambiguous answer: that is what lets a driver advance the run without knowing the
|
|
19
|
+
* vocabulary. `Fail` is marked explicit precisely so it never becomes that automatic answer.
|
|
20
|
+
*/
|
|
21
|
+
export const agentRunFlow: ShallowFlow = {
|
|
22
|
+
flow: AGENT_RUN_FLOW,
|
|
23
|
+
initialStep: AgentRunStep.Received,
|
|
24
|
+
|
|
25
|
+
steps: {
|
|
26
|
+
[AgentRunStep.Received]: {
|
|
27
|
+
index: 0,
|
|
28
|
+
step: AgentRunStep.Received,
|
|
29
|
+
service: AGENT_RUN_FLOW,
|
|
30
|
+
initial: true,
|
|
31
|
+
transitions: {
|
|
32
|
+
[AgentRunTransition.Prepare]: {
|
|
33
|
+
transition: AgentRunTransition.Prepare,
|
|
34
|
+
step: AgentRunStep.Prepared,
|
|
35
|
+
},
|
|
36
|
+
[AgentRunTransition.Fail]: {
|
|
37
|
+
transition: AgentRunTransition.Fail,
|
|
38
|
+
step: AgentRunStep.Failed,
|
|
39
|
+
explicit: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
[AgentRunStep.Prepared]: {
|
|
45
|
+
index: 1,
|
|
46
|
+
step: AgentRunStep.Prepared,
|
|
47
|
+
service: AGENT_RUN_FLOW,
|
|
48
|
+
transitions: {
|
|
49
|
+
[AgentRunTransition.Work]: {
|
|
50
|
+
transition: AgentRunTransition.Work,
|
|
51
|
+
step: AgentRunStep.Working,
|
|
52
|
+
},
|
|
53
|
+
[AgentRunTransition.Fail]: {
|
|
54
|
+
transition: AgentRunTransition.Fail,
|
|
55
|
+
step: AgentRunStep.Failed,
|
|
56
|
+
explicit: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
[AgentRunStep.Working]: {
|
|
62
|
+
index: 2,
|
|
63
|
+
step: AgentRunStep.Working,
|
|
64
|
+
service: AGENT_RUN_FLOW,
|
|
65
|
+
transitions: {
|
|
66
|
+
[AgentRunTransition.Finalize]: {
|
|
67
|
+
transition: AgentRunTransition.Finalize,
|
|
68
|
+
step: AgentRunStep.Finalizing,
|
|
69
|
+
},
|
|
70
|
+
[AgentRunTransition.Fail]: {
|
|
71
|
+
transition: AgentRunTransition.Fail,
|
|
72
|
+
step: AgentRunStep.Failed,
|
|
73
|
+
explicit: true,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
[AgentRunStep.Finalizing]: {
|
|
79
|
+
index: 3,
|
|
80
|
+
step: AgentRunStep.Finalizing,
|
|
81
|
+
service: AGENT_RUN_FLOW,
|
|
82
|
+
transitions: {
|
|
83
|
+
[AgentRunTransition.Finish]: {
|
|
84
|
+
transition: AgentRunTransition.Finish,
|
|
85
|
+
step: AgentRunStep.Finished,
|
|
86
|
+
},
|
|
87
|
+
[AgentRunTransition.Fail]: {
|
|
88
|
+
transition: AgentRunTransition.Fail,
|
|
89
|
+
step: AgentRunStep.Failed,
|
|
90
|
+
explicit: true,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
[AgentRunStep.Finished]: {
|
|
96
|
+
index: 4,
|
|
97
|
+
step: AgentRunStep.Finished,
|
|
98
|
+
service: AGENT_RUN_FLOW,
|
|
99
|
+
transitions: {},
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
[AgentRunStep.Failed]: {
|
|
103
|
+
index: 5,
|
|
104
|
+
step: AgentRunStep.Failed,
|
|
105
|
+
service: AGENT_RUN_FLOW,
|
|
106
|
+
transitions: {
|
|
107
|
+
[AgentRunTransition.Resume]: {
|
|
108
|
+
transition: AgentRunTransition.Resume,
|
|
109
|
+
step: AgentRunStep.Working,
|
|
110
|
+
explicit: true,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Every flow this package declares, for a provider to serve. */
|
|
118
|
+
export const agentFlows: ShallowFlow[] = [agentRunFlow]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LlmPurpose } from '@owlmeans/llm-common'
|
|
2
|
+
import { SCOPE_SEP } from '../consts.js'
|
|
3
|
+
import type { ConversationRef } from '../types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The conversation a run belongs to.
|
|
7
|
+
*
|
|
8
|
+
* An LLM purpose already carries the only correlation key most applications have — `dedication`,
|
|
9
|
+
* conventionally `<kind>:<id>`. Deriving the conversation from it means an application gets
|
|
10
|
+
* continuity without inventing and threading a second identifier, and two runs dedicated to the
|
|
11
|
+
* same subject land in the same conversation by construction.
|
|
12
|
+
*
|
|
13
|
+
* The scope is the dedication's TARGET rather than the whole string, so conversations addressed at
|
|
14
|
+
* different granularities (a project, one of its stories) still share the subject their memory is
|
|
15
|
+
* filed under. Both halves can be overridden for an application whose threads are not one per
|
|
16
|
+
* dedication.
|
|
17
|
+
*/
|
|
18
|
+
export const conversationFor = (
|
|
19
|
+
purpose: LlmPurpose | undefined,
|
|
20
|
+
override?: Partial<ConversationRef>,
|
|
21
|
+
): ConversationRef => {
|
|
22
|
+
const dedication = purpose?.dedication ?? ''
|
|
23
|
+
const separator = dedication.indexOf(SCOPE_SEP)
|
|
24
|
+
const target = separator < 0 ? dedication : dedication.slice(separator + 1)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
conversationId: override?.conversationId ?? (dedication !== '' ? dedication : 'anonymous'),
|
|
28
|
+
scope: override?.scope ?? (target !== '' ? target : 'anonymous'),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cut `text` to `max` characters on a boundary a reader will not trip over.
|
|
34
|
+
*
|
|
35
|
+
* Every cap in this package lands here, because a model asked for "at most N characters" answers
|
|
36
|
+
* with N plus whatever it felt was needed. Truncating mid-word reads as corruption and truncating
|
|
37
|
+
* mid-sentence reads as a bug report, so the cut prefers the last paragraph, then line, then
|
|
38
|
+
* sentence, then word break inside the last quarter of the budget, and always marks itself.
|
|
39
|
+
*/
|
|
40
|
+
export const truncateAt = (text: string, max: number): string => {
|
|
41
|
+
const trimmed = text.trim()
|
|
42
|
+
if (trimmed.length <= max) {
|
|
43
|
+
return trimmed
|
|
44
|
+
}
|
|
45
|
+
if (max <= 1) {
|
|
46
|
+
return trimmed.slice(0, Math.max(0, max))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ellipsis = '…'
|
|
50
|
+
const budget = max - ellipsis.length
|
|
51
|
+
const head = trimmed.slice(0, budget)
|
|
52
|
+
const floor = Math.floor(budget * 0.75)
|
|
53
|
+
|
|
54
|
+
for (const boundary of ['\n\n', '\n', '. ', ' ']) {
|
|
55
|
+
const at = head.lastIndexOf(boundary)
|
|
56
|
+
if (at >= floor) {
|
|
57
|
+
return head.slice(0, boundary === '. ' ? at + 1 : at).trimEnd() + ellipsis
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return head.trimEnd() + ellipsis
|
|
62
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ResourceRecord } from '@owlmeans/resource'
|
|
2
|
+
import type { ExecutionState } from '@owlmeans/llm-common'
|
|
3
|
+
import type { AgentRunStatus } from './consts.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a conversation is, as an address.
|
|
7
|
+
*
|
|
8
|
+
* `conversationId` is the thread a run belongs to; `scope` is the wider subject the thread is
|
|
9
|
+
* about — for a project-dedicated agent the two coincide, but memory is scoped per project while
|
|
10
|
+
* conversations may be finer-grained, so they are separate fields rather than one.
|
|
11
|
+
*/
|
|
12
|
+
export interface ConversationRef {
|
|
13
|
+
conversationId: string
|
|
14
|
+
scope: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One finished run, compacted.
|
|
19
|
+
*
|
|
20
|
+
* Two parts on purpose: `summary` says what happened, `advice` says what to do next. The second is
|
|
21
|
+
* what an agent actually needs on the way in — a summary alone leaves the next run to re-derive
|
|
22
|
+
* the plan from the outcome, which is where it invents a different one.
|
|
23
|
+
*
|
|
24
|
+
* Timestamps are ISO strings, never `Date`: these contracts cross process boundaries and storage
|
|
25
|
+
* backends, and must survive `JSON.stringify` unchanged. A consumer whose store prefers dates maps
|
|
26
|
+
* them at its own adapter boundary.
|
|
27
|
+
*/
|
|
28
|
+
export interface ConversationEvent extends ResourceRecord {
|
|
29
|
+
conversationId: string
|
|
30
|
+
scope: string
|
|
31
|
+
/** Monotonic within a conversation, allocated by the store. */
|
|
32
|
+
seq: number
|
|
33
|
+
createdAt: string
|
|
34
|
+
/** The ask that opened the run, truncated. Present so a reader can see what was attempted. */
|
|
35
|
+
prompt?: string
|
|
36
|
+
summary: string
|
|
37
|
+
advice?: string
|
|
38
|
+
status: AgentRunStatus
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What a store is asked to append; `seq` and `id` are the store's to allocate. */
|
|
42
|
+
export interface ConversationEventInput extends Omit<ConversationEvent, 'id' | 'seq' | 'createdAt'> {
|
|
43
|
+
createdAt?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The data plane of a run.
|
|
48
|
+
*
|
|
49
|
+
* `flow` is the serialized {@link import('@owlmeans/flow').FlowModel} state — the control plane
|
|
50
|
+
* collapsed to a string — and `state` is the execution snapshot the LLM layer produces. Keeping
|
|
51
|
+
* them in one record is what makes a resume a single read.
|
|
52
|
+
*/
|
|
53
|
+
export interface AgentRunState extends ResourceRecord {
|
|
54
|
+
id: string
|
|
55
|
+
conversationId: string
|
|
56
|
+
flow: string
|
|
57
|
+
state: ExecutionState
|
|
58
|
+
updatedAt: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A node of the subsystem memory graph.
|
|
63
|
+
*
|
|
64
|
+
* `subsystem` is the lookup key within a scope, and `links` are the other subsystems this one
|
|
65
|
+
* refers to. The graph is deliberately shallow: agents read a node and follow a link or two, they
|
|
66
|
+
* do not traverse.
|
|
67
|
+
*/
|
|
68
|
+
export interface MemoryNode extends ResourceRecord {
|
|
69
|
+
scope: string
|
|
70
|
+
subsystem: string
|
|
71
|
+
content: string
|
|
72
|
+
links: string[]
|
|
73
|
+
updatedAt: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** An entry in the bounded event-sequence memory. */
|
|
77
|
+
export interface MemoryEvent extends ResourceRecord {
|
|
78
|
+
scope: string
|
|
79
|
+
/** Monotonic within a scope, allocated by the store. */
|
|
80
|
+
seq: number
|
|
81
|
+
kind: string
|
|
82
|
+
content: string
|
|
83
|
+
createdAt: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What a store is asked to append; `seq` and `id` are the store's to allocate. */
|
|
87
|
+
export interface MemoryEventInput extends Omit<MemoryEvent, 'id' | 'seq' | 'createdAt'> {
|
|
88
|
+
createdAt?: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* What a transport carries.
|
|
93
|
+
*
|
|
94
|
+
* The execution state travels by REFERENCE (`stateRef`), not by value: a project execution's state
|
|
95
|
+
* holds the whole project specification, and a queue whose messages carry that is a queue that
|
|
96
|
+
* falls over on the first large project. The flow string is small enough to inline, and it is what
|
|
97
|
+
* a consumer needs to route the message before it reads anything.
|
|
98
|
+
*/
|
|
99
|
+
export interface AgentRunMessage {
|
|
100
|
+
id: string
|
|
101
|
+
conversationId: string
|
|
102
|
+
flow: string
|
|
103
|
+
stateRef?: string
|
|
104
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { makeFlowModel } from '@owlmeans/flow'
|
|
3
|
+
import type { Flow, FlowProvider } from '@owlmeans/flow'
|
|
4
|
+
import {
|
|
5
|
+
AGENT_RUN_FLOW, AgentRunStep, AgentRunTransition, agentRunFlow, conversationFor, truncateAt,
|
|
6
|
+
} from '../src/index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A run's flow has to survive leaving the process, or the steps buy nothing: the whole reason the
|
|
10
|
+
* lifecycle is a flow rather than a field is that a crashed run can be told where to resume. These
|
|
11
|
+
* pin that the serialized form round-trips, which is also the gate on `@owlmeans/flow` being
|
|
12
|
+
* usable server-side at all — it has no server consumer anywhere else in the monorepo.
|
|
13
|
+
*/
|
|
14
|
+
const provider: FlowProvider = async flow => {
|
|
15
|
+
if (flow !== AGENT_RUN_FLOW) {
|
|
16
|
+
throw new Error(`unknown flow ${flow}`)
|
|
17
|
+
}
|
|
18
|
+
return { ...agentRunFlow, config: {}, prefabs: {} } as Flow
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('agent-common — the run lifecycle flow', () => {
|
|
22
|
+
test('starts at Received and advances through the working steps', async () => {
|
|
23
|
+
const model = await makeFlowModel(agentRunFlow)
|
|
24
|
+
expect(model.step().step).toBe(AgentRunStep.Received)
|
|
25
|
+
|
|
26
|
+
model.transit(AgentRunTransition.Prepare, true)
|
|
27
|
+
expect(model.step().step).toBe(AgentRunStep.Prepared)
|
|
28
|
+
|
|
29
|
+
model.transit(AgentRunTransition.Work, true)
|
|
30
|
+
expect(model.step().step).toBe(AgentRunStep.Working)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('offers exactly one automatic transition per working step', async () => {
|
|
34
|
+
const model = await makeFlowModel(agentRunFlow)
|
|
35
|
+
|
|
36
|
+
for (const expected of [AgentRunTransition.Prepare, AgentRunTransition.Work, AgentRunTransition.Finalize]) {
|
|
37
|
+
// `next()` is what lets a driver advance without knowing the vocabulary; it only works while
|
|
38
|
+
// exactly one outgoing transition is non-explicit. `Fail` is explicit for that reason.
|
|
39
|
+
expect(model.next().transition).toBe(expected)
|
|
40
|
+
model.transit(expected, true)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(model.step().step).toBe(AgentRunStep.Finalizing)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('round-trips a mid-run state through its serialized form', async () => {
|
|
47
|
+
const model = await makeFlowModel(agentRunFlow)
|
|
48
|
+
model.transit(AgentRunTransition.Prepare, true)
|
|
49
|
+
const token = model.transit(AgentRunTransition.Work, true)
|
|
50
|
+
|
|
51
|
+
const restored = await makeFlowModel(token, provider)
|
|
52
|
+
|
|
53
|
+
expect(restored.step().step).toBe(AgentRunStep.Working)
|
|
54
|
+
expect(restored.state().flow).toBe(AGENT_RUN_FLOW)
|
|
55
|
+
expect(restored.state().ok).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('carries a failure and its message across serialization', async () => {
|
|
59
|
+
const model = await makeFlowModel(agentRunFlow)
|
|
60
|
+
model.transit(AgentRunTransition.Prepare, true)
|
|
61
|
+
const token = model.transit(AgentRunTransition.Fail, false, 'the model refused')
|
|
62
|
+
|
|
63
|
+
const restored = await makeFlowModel(token, provider)
|
|
64
|
+
|
|
65
|
+
expect(restored.step().step).toBe(AgentRunStep.Failed)
|
|
66
|
+
expect(restored.state().ok).toBe(false)
|
|
67
|
+
expect(restored.state().message).toBe('the model refused')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('agent-common — conversation identity', () => {
|
|
72
|
+
test('derives the thread from a dedication and the scope from its target', () => {
|
|
73
|
+
expect(conversationFor({ dedication: 'project:abc123' }))
|
|
74
|
+
.toEqual({ conversationId: 'project:abc123', scope: 'abc123' })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('degrades to a named default rather than an empty key', () => {
|
|
78
|
+
// An empty conversation id would silently collapse every run of every subject into one thread.
|
|
79
|
+
expect(conversationFor(undefined)).toEqual({ conversationId: 'anonymous', scope: 'anonymous' })
|
|
80
|
+
expect(conversationFor({})).toEqual({ conversationId: 'anonymous', scope: 'anonymous' })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('accepts an override for an application whose threads are not one per dedication', () => {
|
|
84
|
+
expect(conversationFor({ dedication: 'story:s1' }, { scope: 'p1' }))
|
|
85
|
+
.toEqual({ conversationId: 'story:s1', scope: 'p1' })
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('agent-common — truncation', () => {
|
|
90
|
+
test('leaves text inside the budget untouched', () => {
|
|
91
|
+
expect(truncateAt(' short ', 100)).toBe('short')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('never exceeds the budget', () => {
|
|
95
|
+
const long = 'word '.repeat(500)
|
|
96
|
+
expect(truncateAt(long, 40).length).toBeLessThanOrEqual(40)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('cuts on a word boundary instead of mid-word', () => {
|
|
100
|
+
const source = 'alpha beta gamma delta epsilon zeta'
|
|
101
|
+
const result = truncateAt(source, 20)
|
|
102
|
+
|
|
103
|
+
expect(result).toBe('alpha beta gamma…')
|
|
104
|
+
// What was kept is a whole number of the original's words: the character the cut landed on is
|
|
105
|
+
// the space that followed the last one it took.
|
|
106
|
+
expect(source.startsWith(result.slice(0, -1))).toBe(true)
|
|
107
|
+
expect(source.charAt(result.length - 1)).toBe(' ')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('prefers a sentence end when one falls inside the budget tail', () => {
|
|
111
|
+
expect(truncateAt('The first sentence ends here. Then a second one runs on.', 37))
|
|
112
|
+
.toBe('The first sentence ends here.…')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('falls back to a word break when the sentence end is too far back to keep', () => {
|
|
116
|
+
// The boundary search only accepts a cut inside the last quarter of the budget — an earlier
|
|
117
|
+
// sentence end would throw away most of what fits, which is worse than a clean word break.
|
|
118
|
+
expect(truncateAt('One sentence here. And a second one that overflows.', 30))
|
|
119
|
+
.toBe('One sentence here. And a…')
|
|
120
|
+
})
|
|
121
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.server.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
|
+
},
|
|
10
|
+
"include": [
|
|
11
|
+
"src/**/*"
|
|
12
|
+
],
|
|
13
|
+
"exclude": [
|
|
14
|
+
"./dist/**/*",
|
|
15
|
+
"./build/**/*",
|
|
16
|
+
"./tests/**/*",
|
|
17
|
+
"./*.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|