@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 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"}
@@ -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"}
@@ -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"}
@@ -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"}
@@ -0,0 +1,6 @@
1
+ export * from './consts.js';
2
+ export * from './errors.js';
3
+ export type * from './types.js';
4
+ export * from './flows.js';
5
+ export * from './helpers/conversation.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -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,5 @@
1
+ export * from './consts.js';
2
+ export * from './errors.js';
3
+ export * from './flows.js';
4
+ export * from './helpers/conversation.js';
5
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -0,0 +1,5 @@
1
+ export * from './consts.js'
2
+ export * from './errors.js'
3
+ export type * from './types.js'
4
+ export * from './flows.js'
5
+ export * from './helpers/conversation.js'
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
+ }