interlacing 0.11.0__tar.gz

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.
Files changed (88) hide show
  1. interlacing-0.11.0/.gitignore +7 -0
  2. interlacing-0.11.0/CHANGELOG.md +19 -0
  3. interlacing-0.11.0/LICENSE +21 -0
  4. interlacing-0.11.0/PKG-INFO +203 -0
  5. interlacing-0.11.0/README.md +179 -0
  6. interlacing-0.11.0/README.zh-CN.md +179 -0
  7. interlacing-0.11.0/docs/CASES.md +161 -0
  8. interlacing-0.11.0/docs/GRAPH.md +432 -0
  9. interlacing-0.11.0/examples/data/prompts/scout.md +1 -0
  10. interlacing-0.11.0/examples/data/release.json +14 -0
  11. interlacing-0.11.0/examples/data/scouts.json +10 -0
  12. interlacing-0.11.0/examples/data/steps.py +27 -0
  13. interlacing-0.11.0/examples/graphs/blocks_joined.py +42 -0
  14. interlacing-0.11.0/examples/graphs/camera_to_arm.py +67 -0
  15. interlacing-0.11.0/examples/graphs/chain_with_program.py +33 -0
  16. interlacing-0.11.0/examples/graphs/desks_between_blocks.py +56 -0
  17. interlacing-0.11.0/examples/graphs/files_checked.py +55 -0
  18. interlacing-0.11.0/examples/graphs/governed_tree.py +58 -0
  19. interlacing-0.11.0/examples/graphs/grown_at_run_time.py +56 -0
  20. interlacing-0.11.0/examples/graphs/planner_and_arm.py +139 -0
  21. interlacing-0.11.0/examples/graphs/review_loop.py +55 -0
  22. interlacing-0.11.0/pyproject.toml +45 -0
  23. interlacing-0.11.0/scripts/bench_engine.py +44 -0
  24. interlacing-0.11.0/scripts/package_smoke.py +61 -0
  25. interlacing-0.11.0/src/interlacing/__init__.py +50 -0
  26. interlacing-0.11.0/src/interlacing/base/__init__.py +25 -0
  27. interlacing-0.11.0/src/interlacing/base/canonical.py +14 -0
  28. interlacing-0.11.0/src/interlacing/base/edge.py +61 -0
  29. interlacing-0.11.0/src/interlacing/base/envelope.py +25 -0
  30. interlacing-0.11.0/src/interlacing/base/graph.py +271 -0
  31. interlacing-0.11.0/src/interlacing/base/method.py +21 -0
  32. interlacing-0.11.0/src/interlacing/base/node.py +151 -0
  33. interlacing-0.11.0/src/interlacing/base/port.py +87 -0
  34. interlacing-0.11.0/src/interlacing/base/protocol.py +90 -0
  35. interlacing-0.11.0/src/interlacing/base/schema.py +176 -0
  36. interlacing-0.11.0/src/interlacing/blocks/__init__.py +107 -0
  37. interlacing-0.11.0/src/interlacing/cli.py +154 -0
  38. interlacing-0.11.0/src/interlacing/engine/__init__.py +8 -0
  39. interlacing-0.11.0/src/interlacing/engine/engine.py +1062 -0
  40. interlacing-0.11.0/src/interlacing/engine/journal.py +97 -0
  41. interlacing-0.11.0/src/interlacing/engine/stream.py +42 -0
  42. interlacing-0.11.0/src/interlacing/engine/transport.py +44 -0
  43. interlacing-0.11.0/src/interlacing/methods/__init__.py +8 -0
  44. interlacing-0.11.0/src/interlacing/methods/chain.py +49 -0
  45. interlacing-0.11.0/src/interlacing/methods/fan.py +68 -0
  46. interlacing-0.11.0/src/interlacing/methods/loop.py +63 -0
  47. interlacing-0.11.0/src/interlacing/methods/pool.py +78 -0
  48. interlacing-0.11.0/src/interlacing/methods/tree.py +117 -0
  49. interlacing-0.11.0/src/interlacing/models/__init__.py +9 -0
  50. interlacing-0.11.0/src/interlacing/models/providers.py +511 -0
  51. interlacing-0.11.0/src/interlacing/nodes/__init__.py +10 -0
  52. interlacing-0.11.0/src/interlacing/nodes/agent.py +242 -0
  53. interlacing-0.11.0/src/interlacing/nodes/flow.py +185 -0
  54. interlacing-0.11.0/src/interlacing/nodes/function.py +44 -0
  55. interlacing-0.11.0/src/interlacing/nodes/graph.py +49 -0
  56. interlacing-0.11.0/src/interlacing/nodes/outside.py +110 -0
  57. interlacing-0.11.0/src/interlacing/nodes/process.py +127 -0
  58. interlacing-0.11.0/src/interlacing/protocols/__init__.py +24 -0
  59. interlacing-0.11.0/src/interlacing/protocols/artifact.py +44 -0
  60. interlacing-0.11.0/src/interlacing/protocols/file.py +197 -0
  61. interlacing-0.11.0/src/interlacing/protocols/standard.py +54 -0
  62. interlacing-0.11.0/src/interlacing/protocols/store.py +92 -0
  63. interlacing-0.11.0/src/interlacing/protocols/value.py +7 -0
  64. interlacing-0.11.0/src/interlacing/py.typed +0 -0
  65. interlacing-0.11.0/src/interlacing/runs.py +307 -0
  66. interlacing-0.11.0/src/interlacing/serve.py +190 -0
  67. interlacing-0.11.0/src/interlacing/spec/__init__.py +6 -0
  68. interlacing-0.11.0/src/interlacing/spec/catalog.py +111 -0
  69. interlacing-0.11.0/src/interlacing/spec/kinds.py +167 -0
  70. interlacing-0.11.0/src/interlacing/spec/load.py +194 -0
  71. interlacing-0.11.0/src/interlacing/spec/spawn.py +129 -0
  72. interlacing-0.11.0/tests/test_agent.py +207 -0
  73. interlacing-0.11.0/tests/test_base.py +269 -0
  74. interlacing-0.11.0/tests/test_blocks.py +202 -0
  75. interlacing-0.11.0/tests/test_budget.py +196 -0
  76. interlacing-0.11.0/tests/test_checks.py +368 -0
  77. interlacing-0.11.0/tests/test_cli.py +140 -0
  78. interlacing-0.11.0/tests/test_desk.py +171 -0
  79. interlacing-0.11.0/tests/test_engine.py +209 -0
  80. interlacing-0.11.0/tests/test_example_graphs.py +90 -0
  81. interlacing-0.11.0/tests/test_faults.py +205 -0
  82. interlacing-0.11.0/tests/test_journal.py +347 -0
  83. interlacing-0.11.0/tests/test_methods.py +270 -0
  84. interlacing-0.11.0/tests/test_protocols.py +354 -0
  85. interlacing-0.11.0/tests/test_room.py +283 -0
  86. interlacing-0.11.0/tests/test_serve.py +109 -0
  87. interlacing-0.11.0/tests/test_spawn.py +195 -0
  88. interlacing-0.11.0/tests/test_spec.py +182 -0
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .pytest_cache/
6
+ build/
7
+ dist/
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ Versions before 1.0 may change any interface; each change is listed here. From 1.0 on, the names exported
4
+ by `interlacing` and the formats of graph files, run directories and journals change incompatibly only
5
+ with a new major version.
6
+
7
+ ## 0.11.0
8
+
9
+ First release on the package index.
10
+
11
+ - Nodes (`FunctionNode`, `AgentNode`, `ProcessNode`, `SourceNode`, `SinkNode`, `HumanNode`, `GraphNode`),
12
+ protocols (`value`, `latest`, `state`, `signal`, `stream`, `call`, `artifact`, `file`) and methods (`Chain`, `Loop`,
13
+ `Fan`, `Tree`, `Star`, `Governed`, `Pool`).
14
+ - The engine: workers, journal and `resume`, time limits, budgets, an observer, output checks with repairs.
15
+ - `Block`, `seq`, `par`; graphs as JSON with `Catalog`; `SpawnNode` for graphs written during a run.
16
+ - `Desk`: a node that takes in from several places, sorts, and hands out to several; `Fan` joins a split and a
17
+ join to its branches by name, and shows everything they take and give.
18
+ - Run directories, the `interlacing` command, and an MCP server over stdio.
19
+ - Graphs that are not trusted are held to the run's time limit, node count and workers (`firm`, `strict`).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 li2o0577
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.5
2
+ Name: interlacing
3
+ Version: 0.11.0
4
+ Summary: Nodes, edges and methods: join models, programs, devices and people into graphs, and run them
5
+ Project-URL: Homepage, https://github.com/Li2O0577/interlacing
6
+ Project-URL: Documentation, https://github.com/Li2O0577/interlacing/blob/main/docs/GRAPH.md
7
+ Project-URL: Changelog, https://github.com/Li2O0577/interlacing/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/Li2O0577/interlacing/issues
9
+ Author: li2o0577
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,graph,mcp,multi-agent,orchestration,workflow
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+
25
+ # interlacing
26
+
27
+ **English** · [中文](https://github.com/Li2O0577/interlacing/blob/main/README.zh-CN.md)
28
+
29
+ Join anything into a graph and run it.
30
+
31
+ - A **node** is anything that takes information in and gives information out: a model, a function, an external program, a device, a person, or another graph.
32
+ - An **edge** carries information from one node to another under a written protocol: queued values, latest value only, state, signals, streams, calls that are answered, files passed by path.
33
+ - A **method** is a ready-made way of joining nodes: chain, loop, fan-out and join, tree, star, a tree that needs a person's consent, a pool of members that appear when called.
34
+
35
+ The engine runs the graph and looks after the run: outputs are checked, usage is counted against a budget, activations are timed, everything is journaled so an interrupted run carries on, and an observer is told what happens as it happens. The package is a kernel to build frameworks on. It depends on the Python standard library and nothing else.
36
+
37
+ ## Install
38
+
39
+ ```sh
40
+ pip install interlacing
41
+ pip install -e . # or from a checkout
42
+ ```
43
+
44
+ Python 3.11 or later, on Linux, macOS and Windows.
45
+
46
+ ## A first graph
47
+
48
+ ```python
49
+ from interlacing import DEFAULT, Chain, Engine, FunctionNode, Port
50
+
51
+ double = FunctionNode("double", lambda x: x * 2, inputs=[Port("x", int)], outputs=[Port("y", int)])
52
+ show = FunctionNode("show", lambda x: f"={x}", inputs=[Port("x", int)], outputs=[Port("y", str)])
53
+
54
+ graph = Chain(double, show).build(DEFAULT)
55
+ print(Engine(graph, DEFAULT).run({"in": 21}).last("out")) # =42
56
+ ```
57
+
58
+ ## What it does
59
+
60
+ **A model with tools.** A tool is a call edge to an ordinary node, so a dictionary, a program and a robot arm are all answered the same way.
61
+
62
+ ```python
63
+ from interlacing import AgentNode, GraphBuilder, SinkNode, Tool
64
+
65
+ agent = AgentNode("agent", model, system="...", tools=[Tool("lookup", "Look a word up"), Tool("move", "Move the arm")])
66
+ graph = (GraphBuilder().add(agent, dictionary, SinkNode("arm", arm.move, answers=True))
67
+ .connect("agent.lookup", "dictionary.x", "call")
68
+ .connect("agent.move", "arm.x", "call")
69
+ .input("in", "agent.task").output("out", "agent.out").compile(DEFAULT))
70
+ ```
71
+
72
+ `model` is any object with `chat(system, messages, tools, max_tokens)`. `interlacing.models` has an Anthropic interface, an OpenAI-compatible interface, and `FakeProvider` for tests.
73
+
74
+ **Outputs that are checked.** A port says what may leave through it and what happens when something else does: the node fails, the payload goes on with a warning, or the node is told what is wrong and may send again.
75
+
76
+ ```python
77
+ Port("plan", dict, check=valid_plan, repairs=2) # a model is told why its plan was refused, twice at most
78
+ ```
79
+
80
+ **Files passed by path.** Nodes send relative paths, not contents. The file is checked when it is sent and again when it arrives.
81
+
82
+ ```python
83
+ protocols = with_files(DEFAULT, "work")
84
+ Port("chart", FileOf(".png", max_size=5_000_000), repairs=2)
85
+ builder.connect("draw.chart", "report.chart", "file")
86
+ ```
87
+
88
+ **Designs used as units.** A graph that has been worked out becomes a block, used many times and joined with others.
89
+
90
+ ```python
91
+ review = Block(lambda: Loop(writer(), critic(), max_rounds=3), name="review")
92
+ study = seq(clean, par(review, review, review), publish) # in a row, side by side, nested
93
+ graph = Chain(intake, study("study"), report).build(DEFAULT)
94
+ ```
95
+
96
+ **Desks at the doors of a block.** A `Desk` takes in what arrives from several places, sorts it, and hands it out to several places. At the way into a block it shares the work among the members; at the way out it makes one answer of what they gave. Where the sorting needs judgement, an agent with the same inputs and outputs stands in the same place.
97
+
98
+ ```python
99
+ team = Fan(analyst, modeller, # each member gets its own part, by name
100
+ split=Desk("door", takes=["brief", "data"], gives=["analyst", "modeller"], sort=share),
101
+ join=Desk("exit", takes=["analyst", "modeller"], sort=report))
102
+ ```
103
+
104
+ **Graphs that are not fixed in advance.** `Pool` keeps places for members that do nothing until they are called for. `SpawnNode` builds and runs a graph that a node wrote during the run, within limits set by whoever made the node.
105
+
106
+ ```python
107
+ grow = SpawnNode("grow", Catalog(allow=["my_steps"]), max_nodes=20)
108
+ graph = Chain(planner, grow).build(DEFAULT) # planner sends {"graph": ..., "inputs": ...}
109
+ ```
110
+
111
+ **Runs that are looked after.**
112
+
113
+ ```python
114
+ engine = Engine(graph, DEFAULT, workers=4,
115
+ journal=Journal("run.jsonl"), # an interrupted run is taken up with resume()
116
+ time_limit=600, # the longest one activation may take
117
+ budget={"tokens": 2_000_000}, # nodes report what they use; over it, the run stops
118
+ observe=print) # every event, as it happens
119
+ ```
120
+
121
+ A node that must not do something twice outside the graph uses `ctx.key`, which stays the same when an interrupted activation runs again.
122
+
123
+ **Graphs written as data.** A graph file holds the structure and whatever differs from the defaults; a prompt is a file that is referred to.
124
+
125
+ ```json
126
+ {
127
+ "defaults": {"agent": {"model": "anthropic:some-model", "time_limit": 600}},
128
+ "method": "pool", "size": 10, "max_rounds": 3,
129
+ "root": {"id": "lead", "kind": "agent", "system": {"file": "prompts/lead.md"}, "firing": "any",
130
+ "inputs": ["task", {"name": "reply", "required": false}], "outputs": ["out", "send"]},
131
+ "member": {"kind": "agent", "system": {"file": "prompts/scout.md"}}
132
+ }
133
+ ```
134
+
135
+ ```sh
136
+ cd examples/data && export PYTHONPATH=.
137
+ interlacing check release.json --allow steps
138
+ interlacing run release.json --dir /tmp/run --allow steps --input topic=tides # waiting for a person, exit code 3
139
+ interlacing answer --dir /tmp/run --json '{"approved": true}'
140
+ interlacing resume --dir /tmp/run # done, exit code 0
141
+ ```
142
+
143
+ A run is a directory. A person in the graph who has not answered yet does not keep a process waiting: the command exits, and the run goes on once the answer has been given. One process at a time runs a directory.
144
+
145
+ **As an MCP server.** The same operations can be offered to a model in another application. What its graphs may reach, how much they may use and how long a node may take are set on the command line by whoever starts the server, and the client cannot change them.
146
+
147
+ ```json
148
+ {"mcpServers": {"interlacing": {"command": "interlacing",
149
+ "args": ["serve", "--runs", "/path/to/runs", "--allow", "mypkg", "--budget", "tokens=500000"]}}}
150
+ ```
151
+
152
+ Model keys are read from the environment (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY` and their `_BASE_URL`), and are never written to a graph file, a run directory or a journal.
153
+
154
+ ## What it is for
155
+
156
+ Three worked cases are in [docs/CASES.md](https://github.com/Li2O0577/interlacing/blob/main/docs/CASES.md) (in Chinese). In short:
157
+
158
+ | Case | How it is built |
159
+ |---|---|
160
+ | A physical agent: a multimodal model plans gripper poses as points in space, a controller executes, and the next observation decides what follows | `Loop(observe, planning, act)`; the model's measuring tools are nodes on call edges; the action contract is a check on the plan port, so a bad plan goes back to the model with the reason and never reaches the controller; plans and seconds are budgets. Runnable with stand-ins: [examples/graphs/planner_and_arm.py](https://github.com/Li2O0577/interlacing/blob/main/examples/graphs/planner_and_arm.py) |
161
+ | A modelling assistant: a lead, one owner per question, a solver and a critic who argue until the answer is proved, a person who may step in anywhere | each question is a `Block` holding a `Loop(solve, critic)`; "no proof, not done" is a port check; questions run in `seq`; the person is the operator of a `Governed` tree; the run directory survives restarts |
162
+ | A mathematician: a lead sends out explorers in parallel, each hands its work to a proof checker and reports back when it passes | `Pool(lead, explorer, size=10)` with `workers`; each explorer is a `Loop(prove, check)`; three directions given, three explorers exist |
163
+
164
+ The kernel carries information and looks after the run. How a model thinks, how an arm moves and how a paper is typeset are the business of the nodes.
165
+
166
+ ## Layout
167
+
168
+ | Layer | Package | Main names |
169
+ |---|---|---|
170
+ | Abstractions | `interlacing.base` | `Node`, `Port`, `Schema`, `Edge`, `Protocol`, `Envelope`, `Graph`, `GraphBuilder`, `Method`, `Context` |
171
+ | Engine | `interlacing.engine` | `Engine`, `Result`, `RunError`, `Journal`, `Transport` |
172
+ | Parts | `interlacing.nodes`, `.protocols`, `.methods`, `.models` | `FunctionNode`, `AgentNode`, `ProcessNode`, `SourceNode`, `SinkNode`, `HumanNode`, `GraphNode`, `Desk`; `Chain`, `Loop`, `Fan`, `Tree`, `Star`, `Governed`, `Pool` |
173
+ | Blocks | `interlacing.blocks` | `Block`, `seq`, `par` |
174
+ | Graphs as data | `interlacing.spec` | `dump`, `load`, `read`, `write`, `Catalog`, `SpawnNode` |
175
+ | Outside | `interlacing.runs`, `interlacing.cli`, `interlacing.serve` | `Run`; the command `interlacing`; the MCP server |
176
+
177
+ Each layer uses only the layers below it. The engine knows no particular node, protocol or method; a test in `tests/test_engine.py` holds it to that.
178
+
179
+ The full description is [docs/GRAPH.md](https://github.com/Li2O0577/interlacing/blob/main/docs/GRAPH.md) (in Chinese). Nine examples that run as they stand are in [examples/graphs/](https://github.com/Li2O0577/interlacing/tree/main/examples/graphs), and graphs written as JSON in [examples/data/](https://github.com/Li2O0577/interlacing/tree/main/examples/data).
180
+
181
+ ## Tests
182
+
183
+ ```sh
184
+ python -m pip install -e . pytest
185
+ python -m pytest -q tests
186
+ python scripts/bench_engine.py # what the engine adds per envelope (about 11 microseconds)
187
+ ```
188
+
189
+ The tests run on Linux, macOS and Windows, with scripted models and stand-in devices. They include runs broken at random places, with the journal cut short as a dying process leaves it, which must end as unbroken runs do. The package passes `mypy`.
190
+
191
+ ## Limits
192
+
193
+ - Taking a run up again is at-least-once: an interrupted node runs again, and what it did outside the graph is done again unless the node uses `ctx.key` to prevent it.
194
+ - Delivery is in-process only. `Transport` is the place for carrying envelopes between processes or machines, and nothing is there yet.
195
+ - Both model interfaces have been tried once, with very small requests, against one compatible service. Thinking output, picture input and retries on rate limits have not been tried against a real service. Device nodes have only met stand-ins.
196
+ - A node that overruns its time is abandoned, not killed: a thread cannot be stopped from outside. A program started by `ProcessNode` is killed.
197
+ - `allow` is a list of modules whose functions a graph may name. It is not a sandbox.
198
+ - The MCP server speaks over stdio only, handles one call at a time, and has not been tried with a real client.
199
+ - There is no asynchronous interface; the engine uses threads.
200
+
201
+ ## License
202
+
203
+ MIT
@@ -0,0 +1,179 @@
1
+ # interlacing
2
+
3
+ **English** · [中文](https://github.com/Li2O0577/interlacing/blob/main/README.zh-CN.md)
4
+
5
+ Join anything into a graph and run it.
6
+
7
+ - A **node** is anything that takes information in and gives information out: a model, a function, an external program, a device, a person, or another graph.
8
+ - An **edge** carries information from one node to another under a written protocol: queued values, latest value only, state, signals, streams, calls that are answered, files passed by path.
9
+ - A **method** is a ready-made way of joining nodes: chain, loop, fan-out and join, tree, star, a tree that needs a person's consent, a pool of members that appear when called.
10
+
11
+ The engine runs the graph and looks after the run: outputs are checked, usage is counted against a budget, activations are timed, everything is journaled so an interrupted run carries on, and an observer is told what happens as it happens. The package is a kernel to build frameworks on. It depends on the Python standard library and nothing else.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pip install interlacing
17
+ pip install -e . # or from a checkout
18
+ ```
19
+
20
+ Python 3.11 or later, on Linux, macOS and Windows.
21
+
22
+ ## A first graph
23
+
24
+ ```python
25
+ from interlacing import DEFAULT, Chain, Engine, FunctionNode, Port
26
+
27
+ double = FunctionNode("double", lambda x: x * 2, inputs=[Port("x", int)], outputs=[Port("y", int)])
28
+ show = FunctionNode("show", lambda x: f"={x}", inputs=[Port("x", int)], outputs=[Port("y", str)])
29
+
30
+ graph = Chain(double, show).build(DEFAULT)
31
+ print(Engine(graph, DEFAULT).run({"in": 21}).last("out")) # =42
32
+ ```
33
+
34
+ ## What it does
35
+
36
+ **A model with tools.** A tool is a call edge to an ordinary node, so a dictionary, a program and a robot arm are all answered the same way.
37
+
38
+ ```python
39
+ from interlacing import AgentNode, GraphBuilder, SinkNode, Tool
40
+
41
+ agent = AgentNode("agent", model, system="...", tools=[Tool("lookup", "Look a word up"), Tool("move", "Move the arm")])
42
+ graph = (GraphBuilder().add(agent, dictionary, SinkNode("arm", arm.move, answers=True))
43
+ .connect("agent.lookup", "dictionary.x", "call")
44
+ .connect("agent.move", "arm.x", "call")
45
+ .input("in", "agent.task").output("out", "agent.out").compile(DEFAULT))
46
+ ```
47
+
48
+ `model` is any object with `chat(system, messages, tools, max_tokens)`. `interlacing.models` has an Anthropic interface, an OpenAI-compatible interface, and `FakeProvider` for tests.
49
+
50
+ **Outputs that are checked.** A port says what may leave through it and what happens when something else does: the node fails, the payload goes on with a warning, or the node is told what is wrong and may send again.
51
+
52
+ ```python
53
+ Port("plan", dict, check=valid_plan, repairs=2) # a model is told why its plan was refused, twice at most
54
+ ```
55
+
56
+ **Files passed by path.** Nodes send relative paths, not contents. The file is checked when it is sent and again when it arrives.
57
+
58
+ ```python
59
+ protocols = with_files(DEFAULT, "work")
60
+ Port("chart", FileOf(".png", max_size=5_000_000), repairs=2)
61
+ builder.connect("draw.chart", "report.chart", "file")
62
+ ```
63
+
64
+ **Designs used as units.** A graph that has been worked out becomes a block, used many times and joined with others.
65
+
66
+ ```python
67
+ review = Block(lambda: Loop(writer(), critic(), max_rounds=3), name="review")
68
+ study = seq(clean, par(review, review, review), publish) # in a row, side by side, nested
69
+ graph = Chain(intake, study("study"), report).build(DEFAULT)
70
+ ```
71
+
72
+ **Desks at the doors of a block.** A `Desk` takes in what arrives from several places, sorts it, and hands it out to several places. At the way into a block it shares the work among the members; at the way out it makes one answer of what they gave. Where the sorting needs judgement, an agent with the same inputs and outputs stands in the same place.
73
+
74
+ ```python
75
+ team = Fan(analyst, modeller, # each member gets its own part, by name
76
+ split=Desk("door", takes=["brief", "data"], gives=["analyst", "modeller"], sort=share),
77
+ join=Desk("exit", takes=["analyst", "modeller"], sort=report))
78
+ ```
79
+
80
+ **Graphs that are not fixed in advance.** `Pool` keeps places for members that do nothing until they are called for. `SpawnNode` builds and runs a graph that a node wrote during the run, within limits set by whoever made the node.
81
+
82
+ ```python
83
+ grow = SpawnNode("grow", Catalog(allow=["my_steps"]), max_nodes=20)
84
+ graph = Chain(planner, grow).build(DEFAULT) # planner sends {"graph": ..., "inputs": ...}
85
+ ```
86
+
87
+ **Runs that are looked after.**
88
+
89
+ ```python
90
+ engine = Engine(graph, DEFAULT, workers=4,
91
+ journal=Journal("run.jsonl"), # an interrupted run is taken up with resume()
92
+ time_limit=600, # the longest one activation may take
93
+ budget={"tokens": 2_000_000}, # nodes report what they use; over it, the run stops
94
+ observe=print) # every event, as it happens
95
+ ```
96
+
97
+ A node that must not do something twice outside the graph uses `ctx.key`, which stays the same when an interrupted activation runs again.
98
+
99
+ **Graphs written as data.** A graph file holds the structure and whatever differs from the defaults; a prompt is a file that is referred to.
100
+
101
+ ```json
102
+ {
103
+ "defaults": {"agent": {"model": "anthropic:some-model", "time_limit": 600}},
104
+ "method": "pool", "size": 10, "max_rounds": 3,
105
+ "root": {"id": "lead", "kind": "agent", "system": {"file": "prompts/lead.md"}, "firing": "any",
106
+ "inputs": ["task", {"name": "reply", "required": false}], "outputs": ["out", "send"]},
107
+ "member": {"kind": "agent", "system": {"file": "prompts/scout.md"}}
108
+ }
109
+ ```
110
+
111
+ ```sh
112
+ cd examples/data && export PYTHONPATH=.
113
+ interlacing check release.json --allow steps
114
+ interlacing run release.json --dir /tmp/run --allow steps --input topic=tides # waiting for a person, exit code 3
115
+ interlacing answer --dir /tmp/run --json '{"approved": true}'
116
+ interlacing resume --dir /tmp/run # done, exit code 0
117
+ ```
118
+
119
+ A run is a directory. A person in the graph who has not answered yet does not keep a process waiting: the command exits, and the run goes on once the answer has been given. One process at a time runs a directory.
120
+
121
+ **As an MCP server.** The same operations can be offered to a model in another application. What its graphs may reach, how much they may use and how long a node may take are set on the command line by whoever starts the server, and the client cannot change them.
122
+
123
+ ```json
124
+ {"mcpServers": {"interlacing": {"command": "interlacing",
125
+ "args": ["serve", "--runs", "/path/to/runs", "--allow", "mypkg", "--budget", "tokens=500000"]}}}
126
+ ```
127
+
128
+ Model keys are read from the environment (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY` and their `_BASE_URL`), and are never written to a graph file, a run directory or a journal.
129
+
130
+ ## What it is for
131
+
132
+ Three worked cases are in [docs/CASES.md](https://github.com/Li2O0577/interlacing/blob/main/docs/CASES.md) (in Chinese). In short:
133
+
134
+ | Case | How it is built |
135
+ |---|---|
136
+ | A physical agent: a multimodal model plans gripper poses as points in space, a controller executes, and the next observation decides what follows | `Loop(observe, planning, act)`; the model's measuring tools are nodes on call edges; the action contract is a check on the plan port, so a bad plan goes back to the model with the reason and never reaches the controller; plans and seconds are budgets. Runnable with stand-ins: [examples/graphs/planner_and_arm.py](https://github.com/Li2O0577/interlacing/blob/main/examples/graphs/planner_and_arm.py) |
137
+ | A modelling assistant: a lead, one owner per question, a solver and a critic who argue until the answer is proved, a person who may step in anywhere | each question is a `Block` holding a `Loop(solve, critic)`; "no proof, not done" is a port check; questions run in `seq`; the person is the operator of a `Governed` tree; the run directory survives restarts |
138
+ | A mathematician: a lead sends out explorers in parallel, each hands its work to a proof checker and reports back when it passes | `Pool(lead, explorer, size=10)` with `workers`; each explorer is a `Loop(prove, check)`; three directions given, three explorers exist |
139
+
140
+ The kernel carries information and looks after the run. How a model thinks, how an arm moves and how a paper is typeset are the business of the nodes.
141
+
142
+ ## Layout
143
+
144
+ | Layer | Package | Main names |
145
+ |---|---|---|
146
+ | Abstractions | `interlacing.base` | `Node`, `Port`, `Schema`, `Edge`, `Protocol`, `Envelope`, `Graph`, `GraphBuilder`, `Method`, `Context` |
147
+ | Engine | `interlacing.engine` | `Engine`, `Result`, `RunError`, `Journal`, `Transport` |
148
+ | Parts | `interlacing.nodes`, `.protocols`, `.methods`, `.models` | `FunctionNode`, `AgentNode`, `ProcessNode`, `SourceNode`, `SinkNode`, `HumanNode`, `GraphNode`, `Desk`; `Chain`, `Loop`, `Fan`, `Tree`, `Star`, `Governed`, `Pool` |
149
+ | Blocks | `interlacing.blocks` | `Block`, `seq`, `par` |
150
+ | Graphs as data | `interlacing.spec` | `dump`, `load`, `read`, `write`, `Catalog`, `SpawnNode` |
151
+ | Outside | `interlacing.runs`, `interlacing.cli`, `interlacing.serve` | `Run`; the command `interlacing`; the MCP server |
152
+
153
+ Each layer uses only the layers below it. The engine knows no particular node, protocol or method; a test in `tests/test_engine.py` holds it to that.
154
+
155
+ The full description is [docs/GRAPH.md](https://github.com/Li2O0577/interlacing/blob/main/docs/GRAPH.md) (in Chinese). Nine examples that run as they stand are in [examples/graphs/](https://github.com/Li2O0577/interlacing/tree/main/examples/graphs), and graphs written as JSON in [examples/data/](https://github.com/Li2O0577/interlacing/tree/main/examples/data).
156
+
157
+ ## Tests
158
+
159
+ ```sh
160
+ python -m pip install -e . pytest
161
+ python -m pytest -q tests
162
+ python scripts/bench_engine.py # what the engine adds per envelope (about 11 microseconds)
163
+ ```
164
+
165
+ The tests run on Linux, macOS and Windows, with scripted models and stand-in devices. They include runs broken at random places, with the journal cut short as a dying process leaves it, which must end as unbroken runs do. The package passes `mypy`.
166
+
167
+ ## Limits
168
+
169
+ - Taking a run up again is at-least-once: an interrupted node runs again, and what it did outside the graph is done again unless the node uses `ctx.key` to prevent it.
170
+ - Delivery is in-process only. `Transport` is the place for carrying envelopes between processes or machines, and nothing is there yet.
171
+ - Both model interfaces have been tried once, with very small requests, against one compatible service. Thinking output, picture input and retries on rate limits have not been tried against a real service. Device nodes have only met stand-ins.
172
+ - A node that overruns its time is abandoned, not killed: a thread cannot be stopped from outside. A program started by `ProcessNode` is killed.
173
+ - `allow` is a list of modules whose functions a graph may name. It is not a sandbox.
174
+ - The MCP server speaks over stdio only, handles one call at a time, and has not been tried with a real client.
175
+ - There is no asynchronous interface; the engine uses threads.
176
+
177
+ ## License
178
+
179
+ MIT
@@ -0,0 +1,179 @@
1
+ # interlacing
2
+
3
+ [English](README.md) · **中文**
4
+
5
+ 把任何东西连成一张图,然后运行它。
6
+
7
+ - **节点**是任何接收信息并给出信息的东西:模型、函数、外部程序、设备、人,或者另一张图。
8
+ - **边**按一份写明的协议把信息从一个节点带到另一个节点:排队的值、只留最新值、状态、信号、流、有应答的调用、按路径传递的文件。
9
+ - **方法**是把节点连起来的现成方式:链、环、分发再汇合、树、星形、需要人同意的树、成员在被叫到时才出现的池。
10
+
11
+ 引擎运行这张图并照看这次运行:输出要经过检查,用量按预算计数,每次激活都计时,所有事情写入日志,被打断的运行可以接着跑,观察者在事情发生的当时就被告知。这个包是一个内核,用来在它上面搭框架。它只依赖 Python 标准库。
12
+
13
+ ## 安装
14
+
15
+ ```sh
16
+ pip install interlacing
17
+ pip install -e . # 或者从代码检出安装
18
+ ```
19
+
20
+ 需要 Python 3.11 或更高版本,支持 Linux、macOS 和 Windows。
21
+
22
+ ## 第一张图
23
+
24
+ ```python
25
+ from interlacing import DEFAULT, Chain, Engine, FunctionNode, Port
26
+
27
+ double = FunctionNode("double", lambda x: x * 2, inputs=[Port("x", int)], outputs=[Port("y", int)])
28
+ show = FunctionNode("show", lambda x: f"={x}", inputs=[Port("x", int)], outputs=[Port("y", str)])
29
+
30
+ graph = Chain(double, show).build(DEFAULT)
31
+ print(Engine(graph, DEFAULT).run({"in": 21}).last("out")) # =42
32
+ ```
33
+
34
+ ## 能做什么
35
+
36
+ **带工具的模型。** 工具是一条通向普通节点的调用边,所以一本词典、一个程序和一条机械臂的应答方式完全一样。
37
+
38
+ ```python
39
+ from interlacing import AgentNode, GraphBuilder, SinkNode, Tool
40
+
41
+ agent = AgentNode("agent", model, system="...", tools=[Tool("lookup", "查一个词"), Tool("move", "移动机械臂")])
42
+ graph = (GraphBuilder().add(agent, dictionary, SinkNode("arm", arm.move, answers=True))
43
+ .connect("agent.lookup", "dictionary.x", "call")
44
+ .connect("agent.move", "arm.x", "call")
45
+ .input("in", "agent.task").output("out", "agent.out").compile(DEFAULT))
46
+ ```
47
+
48
+ `model` 是任何带有 `chat(system, messages, tools, max_tokens)` 的对象。`interlacing.models` 里有 Anthropic 接口、OpenAI 兼容接口,以及测试用的 `FakeProvider`。
49
+
50
+ **经过检查的输出。** 端口写明什么可以从它出去,以及出去的是别的东西时怎么办:节点失败,或者带着警告继续走,或者告诉节点哪里不对并允许它重发。
51
+
52
+ ```python
53
+ Port("plan", dict, check=valid_plan, repairs=2) # 模型会被告知计划为什么被拒,最多两次
54
+ ```
55
+
56
+ **按路径传递的文件。** 节点发送的是相对路径,不是内容。文件在发出时检查一次,到达时再检查一次。
57
+
58
+ ```python
59
+ protocols = with_files(DEFAULT, "work")
60
+ Port("chart", FileOf(".png", max_size=5_000_000), repairs=2)
61
+ builder.connect("draw.chart", "report.chart", "file")
62
+ ```
63
+
64
+ **把设计当作单元使用。** 一张已经想清楚的图成为一个块,可以多次使用,也可以和别的块拼接。
65
+
66
+ ```python
67
+ review = Block(lambda: Loop(writer(), critic(), max_rounds=3), name="review")
68
+ study = seq(clean, par(review, review, review), publish) # 顺排、并排、嵌套
69
+ graph = Chain(intake, study("study"), report).build(DEFAULT)
70
+ ```
71
+
72
+ **块门口的收发台。** `Desk` 接收从多处到来的东西,整理之后分发到多处。放在块的入口,它把工作分给块内的成员;放在块的出口,它把成员给出的结果整合成一份答复。整理需要判断力时,换成一个输入输出相同的模型节点站在同一个位置。
73
+
74
+ ```python
75
+ team = Fan(analyst, modeller, # 每个成员按名字拿到自己的那一份
76
+ split=Desk("door", takes=["brief", "data"], gives=["analyst", "modeller"], sort=share),
77
+ join=Desk("exit", takes=["analyst", "modeller"], sort=report))
78
+ ```
79
+
80
+ **不事先固定的图。** `Pool` 为成员留好位置,成员在被叫到之前什么也不做。`SpawnNode` 构建并运行一张由节点在运行中写出的图,范围由创建这个节点的人限定。
81
+
82
+ ```python
83
+ grow = SpawnNode("grow", Catalog(allow=["my_steps"]), max_nodes=20)
84
+ graph = Chain(planner, grow).build(DEFAULT) # planner 发出 {"graph": ..., "inputs": ...}
85
+ ```
86
+
87
+ **被照看的运行。**
88
+
89
+ ```python
90
+ engine = Engine(graph, DEFAULT, workers=4,
91
+ journal=Journal("run.jsonl"), # 被打断的运行用 resume() 接着跑
92
+ time_limit=600, # 一次激活最长可以用多久
93
+ budget={"tokens": 2_000_000}, # 节点上报用量;超出则运行停止
94
+ observe=print) # 每个事件,在发生的当时
95
+ ```
96
+
97
+ 不能在图外把同一件事做两遍的节点使用 `ctx.key`,被打断的激活重跑时它保持不变。
98
+
99
+ **写成数据的图。** 图文件里只有结构和与默认值不同的部分;提示词是一个被引用的文件。
100
+
101
+ ```json
102
+ {
103
+ "defaults": {"agent": {"model": "anthropic:some-model", "time_limit": 600}},
104
+ "method": "pool", "size": 10, "max_rounds": 3,
105
+ "root": {"id": "lead", "kind": "agent", "system": {"file": "prompts/lead.md"}, "firing": "any",
106
+ "inputs": ["task", {"name": "reply", "required": false}], "outputs": ["out", "send"]},
107
+ "member": {"kind": "agent", "system": {"file": "prompts/scout.md"}}
108
+ }
109
+ ```
110
+
111
+ ```sh
112
+ cd examples/data && export PYTHONPATH=.
113
+ interlacing check release.json --allow steps
114
+ interlacing run release.json --dir /tmp/run --allow steps --input topic=tides # 在等人回答,退出码 3
115
+ interlacing answer --dir /tmp/run --json '{"approved": true}'
116
+ interlacing resume --dir /tmp/run # 完成,退出码 0
117
+ ```
118
+
119
+ 一次运行就是一个目录。图里的人还没有回答时,不会有进程一直等着:命令退出,等回答给出后运行再继续。同一个目录同一时间只由一个进程运行。
120
+
121
+ **作为 MCP 服务。** 同样的操作可以提供给另一个应用里的模型。它的图能触及什么、能用多少、一个节点能跑多久,由启动服务的人在命令行上设定,客户端改不了。
122
+
123
+ ```json
124
+ {"mcpServers": {"interlacing": {"command": "interlacing",
125
+ "args": ["serve", "--runs", "/path/to/runs", "--allow", "mypkg", "--budget", "tokens=500000"]}}}
126
+ ```
127
+
128
+ 模型密钥从环境变量读取(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY` 以及对应的 `_BASE_URL`),不会写入图文件、运行目录或日志。
129
+
130
+ ## 用来做什么
131
+
132
+ 三个展开讲的案例在 [docs/CASES.md](docs/CASES.md)。简要如下:
133
+
134
+ | 案例 | 怎么搭 |
135
+ |---|---|
136
+ | 具身智能体:多模态模型把夹爪位姿规划成空间中的点,控制器执行,下一次观测决定接下来做什么 | `Loop(observe, planning, act)`;模型的测量工具是调用边上的节点;动作约定是计划端口上的检查,所以坏计划带着原因回到模型手里,到不了控制器;计划次数和秒数是预算。用替身可以直接运行:[examples/graphs/planner_and_arm.py](examples/graphs/planner_and_arm.py) |
137
+ | 建模助手:一个组长,每个问题一个负责人,求解者和质疑者争论到答案被证明为止,人可以在任何地方介入 | 每个问题是一个装着 `Loop(solve, critic)` 的 `Block`;“没有证明就不算完成”是一个端口检查;问题用 `seq` 依次进行;人是 `Governed` 树的操作者;运行目录在重启后仍然有效 |
138
+ | 数学家:组长并行派出探索者,每个探索者把成果交给证明检查器,通过后才汇报 | `Pool(lead, explorer, size=10)` 加 `workers`;每个探索者是一个 `Loop(prove, check)`;给出三个方向,就只存在三个探索者 |
139
+
140
+ 内核负责传递信息和照看运行。模型怎么想、机械臂怎么动、论文怎么排版,是节点自己的事。
141
+
142
+ ## 组成
143
+
144
+ | 层 | 包 | 主要名字 |
145
+ |---|---|---|
146
+ | 抽象 | `interlacing.base` | `Node`、`Port`、`Schema`、`Edge`、`Protocol`、`Envelope`、`Graph`、`GraphBuilder`、`Method`、`Context` |
147
+ | 引擎 | `interlacing.engine` | `Engine`、`Result`、`RunError`、`Journal`、`Transport` |
148
+ | 部件 | `interlacing.nodes`、`.protocols`、`.methods`、`.models` | `FunctionNode`、`AgentNode`、`ProcessNode`、`SourceNode`、`SinkNode`、`HumanNode`、`GraphNode`、`Desk`;`Chain`、`Loop`、`Fan`、`Tree`、`Star`、`Governed`、`Pool` |
149
+ | 块 | `interlacing.blocks` | `Block`、`seq`、`par` |
150
+ | 写成数据的图 | `interlacing.spec` | `dump`、`load`、`read`、`write`、`Catalog`、`SpawnNode` |
151
+ | 对外 | `interlacing.runs`、`interlacing.cli`、`interlacing.serve` | `Run`;命令 `interlacing`;MCP 服务 |
152
+
153
+ 每一层只使用它下面的层。引擎不认识任何具体的节点、协议或方法,`tests/test_engine.py` 里有一个测试守着这一点。
154
+
155
+ 完整说明见 [docs/GRAPH.md](docs/GRAPH.md)。九个可以直接运行的例子在 [examples/graphs/](examples/graphs/),写成 JSON 的图在 [examples/data/](examples/data/)。
156
+
157
+ ## 测试
158
+
159
+ ```sh
160
+ python -m pip install -e . pytest
161
+ python -m pytest -q tests
162
+ python scripts/bench_engine.py # 引擎给每个信封增加的开销(约 11 微秒)
163
+ ```
164
+
165
+ 测试在 Linux、macOS 和 Windows 上运行,使用按脚本应答的模型和替身设备。其中包括在随机位置打断的运行,日志像进程突然死掉时那样被截断,这些运行必须和没被打断的运行得到同样的结局。这个包通过 `mypy` 检查。
166
+
167
+ ## 限制
168
+
169
+ - 接着跑是“至少一次”:被打断的节点会重跑,它在图外做过的事会再做一遍,除非节点用 `ctx.key` 防止。
170
+ - 投递只在进程内。`Transport` 是在进程或机器之间传递信封的位置,目前那里什么都没有。
171
+ - 两种模型接口各用很小的请求在一个兼容服务上试过一次。思考输出、图片输入和限流重试还没有在真实服务上试过。设备节点只接触过替身。
172
+ - 超时的节点是被放弃,而不是被杀掉:线程无法从外部停止。`ProcessNode` 启动的程序会被杀掉。
173
+ - `allow` 是一份模块清单,图可以点名其中的函数。它不是沙箱。
174
+ - MCP 服务只走 stdio,一次处理一个调用,还没有接过真实客户端。
175
+ - 没有异步接口;引擎使用线程。
176
+
177
+ ## 许可
178
+
179
+ MIT