@vention/vention-skills 0.10.26 → 0.10.28

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vention",
3
- "version": "0.10.26",
3
+ "version": "0.10.28",
4
4
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
5
5
  "author": {
6
6
  "name": "Vention"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vention skills
2
2
 
3
- Version 0.10.26. Skills for coding agents that work with Vention machines, packaged as one plugin, `vention`, in the `vention-skills` marketplace.
3
+ Version 0.10.28. Skills for coding agents that work with Vention machines, packaged as one plugin, `vention`, in the `vention-skills` marketplace.
4
4
 
5
5
  | Skill | What it does |
6
6
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vention/vention-skills",
3
- "version": "0.10.26",
3
+ "version": "0.10.28",
4
4
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "vention",
4
- "version": "0.10.26",
4
+ "version": "0.10.28",
5
5
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
6
6
  "author": {
7
7
  "name": "Vention"
@@ -3,7 +3,7 @@ name: vention-design
3
3
  description: Create, build and edit Vention machine designs and design graphs for agentic layout. Use when the user wants to start a new design, lay out an automation cell, add or reposition components in a Vention design, generate a machine layout from a description, add or place a floor plan image under a design, list a design's versions, or inspect an existing design graph. Trigger on MachineBuilder designs, design graphs, assembly graphs, extrusions, actuators, robot mounting, cell footprints, floor plans, or a machine-builder URL.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.26"
6
+ version: "0.10.28"
7
7
  author: Vention
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ name: vention-machine-logic
3
3
  description: Write, edit, validate, and deploy MachineLogic Python applications for Vention machines with the machine-logic-sdk and the Vention CLI. Load it before writing or changing any Python that talks to a Vention machine, however short, since device names and version-specific SDK calls come from here. Use to program a Vention machine or MachineMotion controller, drive actuators, conveyors, robots, pneumatics, or digital I/O, change an existing application, build a state machine, debug from logs, or pull, push, or link with the Vention CLI (vention or vn). Trigger on MachineLogic, machine-logic-sdk, MachineMotion, MachineBuilder applications, the Vention CLI, or a directory holding .machine-code-app-directory-info.json. Printing what the machine or robot holds now (positions, taught targets, calibration, I/O) is a program: files on disk hold only as-designed values. Not for explaining an existing machine or its configuration in words when no program is asked for, nor for Python that never touches the machine.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.26"
6
+ version: "0.10.28"
7
7
  machine-logic-sdk: ">=3.0,<4"
8
8
  author: Vention
9
9
  ---
@@ -175,8 +175,9 @@ async def feed_board(conveyor: IActuator, line: LineState, travel_mm: float) ->
175
175
  line.boards_out += 1 # quick calls and shared state stay on the loop
176
176
  ```
177
177
 
178
- A blocking call with no `_async` version, such as `home`, goes through `asyncio.to_thread` too; home once, before the tasks start.
178
+ Any other blocking call goes through `asyncio.to_thread` too. Home once, before the tasks start: `await asyncio.to_thread(axis.home, 30)`, or, on SDK releases after 3.1.0, `axis.home_async()` followed by the same wait, which leaves the axis free for a fault path to `stop()` mid-home.
179
179
 
180
+ - An actuator's `state.position`, `state.speed` and `state.output_torque` each make a blocking HTTP request to the controller. Inside `async def`, read them with `await asyncio.to_thread(lambda: axis.state.position)`.
180
181
  - Start the tasks with `asyncio.gather` or `asyncio.wait`; the target runs Python 3.10, which has no `TaskGroup`.
181
182
  - SDK callbacks (`on_state_change`, `machine.on_system_state_change`, MQTT) run on SDK threads. Hand their work to the loop with `loop.call_soon_threadsafe`, never by touching shared state from the callback.
182
183
  - Cancelling a task does not stop a move a worker thread is waiting on. The shutdown path stops every device it started, in a `finally`.
@@ -3,7 +3,7 @@ name: vention-machine-logic-hmi
3
3
  description: Build or edit the custom operator HMI of a MachineLogic application, the React touch screen in customui/ that runs in MachineBuilder and on the MachineMotion pendant, wired to the application's Python process through vention-communication. Use when the user asks for an operator screen, a custom HMI or UI, a touch panel, start and stop buttons, live machine state on screen, or recipes an operator picks, for a Vention machine or inside a directory holding .machine-code-app-directory-info.json. Uses @vention/machine-ui, @vention/machine-apps-components and @vention/machine-logic-ui-sdk. Do not use for the no-code HMI Builder, or for the Python motion logic alone (that is vention-machine-logic).
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.26"
6
+ version: "0.10.28"
7
7
  author: Vention
8
8
  ---
9
9
 
@@ -66,15 +66,15 @@ async def start(request: StartRequest) -> CommandResponse:
66
66
 
67
67
 
68
68
  app.register_rpc_plugin(build_state_machine_rpc_bundle(station, triggers=[Triggers.stop.name, BaseTriggers.RESET.value]))
69
- app.register_rpc_plugin(build_storage_rpc_bundle(accessors=[recipe_accessor], max_records_per_model=None))
69
+ app.register_rpc_plugin(build_storage_rpc_bundle(accessors=[recipe_accessor], max_records_per_model=None, enable_db_actions=False))
70
70
  app.finalize()
71
71
  ```
72
72
 
73
73
  - Create `Machine()` in the FastAPI `lifespan`, through `asyncio.to_thread`, never at import. Importing `server.py` must work with no machine attached: that is how the proto is emitted. When it fails, show the failure as a fault the operator can read.
74
74
  - `@action("Start")` names the RPC `Start`. Without the name the RPC takes the function's name as written.
75
- - The state machine bundle adds `GetState`, `GetHistory` and one `Trigger_<Name>` per trigger you list. List only the triggers the operator may fire.
75
+ - The state machine bundle adds `GetState`, `GetHistory` and one `Trigger_<Name>` per trigger. Always pass `triggers=` with only the ones the operator may fire: without it the bundle exposes every trigger, `to_fault` included, and the HMI can fire them around the checks your `@action` handlers make.
76
76
  - The storage bundle adds `Recipe_ListRecords`, `Recipe_CreateRecord`, `Recipe_UpdateRecord` and `Recipe_DeleteRecord` among others, named after the accessor. `max_records_per_model=None` lifts its default cap of 5 records.
77
- - The storage bundle also adds `Database_*` RPCs, including a whole-database restore. Never expose them to operators.
77
+ - Pass `enable_db_actions=False` unless the HMI has a backup or restore screen. Otherwise the bundle adds `Database_*` RPCs the app never uses, including a whole-database restore.
78
78
  - Give every storage model field a zero-value default (`dwell_ms: int = 0`). The client leaves zero values out of the JSON it sends, and a field with no default then fails as NULL.
79
79
  - Every storage write takes a non-empty `actor`, the name recorded in the audit log.
80
80
 
@@ -101,7 +101,7 @@ async def publish_event(level: str, message: str, detail: str) -> Event:
101
101
  return Event(timestamp=datetime.now(timezone.utc).isoformat(), level=level, message=message, detail=detail)
102
102
  ```
103
103
 
104
- Awaiting the function publishes its result. Publish status from an `@on_state_change` method on the state machine (`self.spawn(publish_status())`) and after every count or fault change. `replay=True` hands a new subscriber the last status at once; the event log uses `replay=False` so a reconnect does not repeat old events. Also republish status every `STATUS_HEARTBEAT_S` from a task the `lifespan` starts, as the example does: a connection can hang without closing, and the HMI only notices when the heartbeat stops.
104
+ Awaiting the function publishes its result. Publish status from an `@on_state_change` method on the state machine (`self.spawn(publish_status())`) and after every count or fault change. `replay=True` hands a new subscriber the last status at once; the event log uses `replay=False` so a reconnect does not repeat old events. Keep the last events in a `deque(maxlen=RECENT_EVENT_COUNT)` that `publish_event` appends to and an `@action("RecentEvents")` returns: the HMI loads them each time it connects, so the Logs page is not empty after a reload. Also republish status every `STATUS_HEARTBEAT_S` from a task the `lifespan` starts, as the example does: a connection can hang without closing, and the HMI only notices when the heartbeat stops.
105
105
 
106
106
  ## The generated client
107
107
 
@@ -190,7 +190,7 @@ The HMI drives physical machinery that can injure someone.
190
190
 
191
191
  `examples/indexing-station-hmi/` indexes a part on a conveyor, clamps it, proves the clamp closed, dwells and releases, for the cycle count of the recipe the operator picked.
192
192
 
193
- - `server.py`: the state machine, the streams, the Start action, both bundles, `Machine()` in the `lifespan`, the emergency stop routed to fault, and every device stopped on shutdown.
193
+ - `server.py`: the state machine, the streams and recent-event buffer, the Start and RecentEvents actions, both bundles, `Machine()` in the `lifespan`, the emergency stop routed to fault, and every device stopped on shutdown.
194
194
  - `models.py`: the `Recipe` storage model, every field with a zero default.
195
195
  - `project.json`, `requirements.txt`: the one process and the exact pins.
196
196
  - `proto/app.proto`: what `finalize()` emits.
@@ -15,9 +15,9 @@
15
15
  "@emotion/styled": "11.14.1",
16
16
  "@mui/material": "5.16.7",
17
17
  "@tabler/icons-react": "3.44.0",
18
- "@vention/machine-apps-components": "1.30.24",
18
+ "@vention/machine-apps-components": "1.30.25",
19
19
  "@vention/machine-logic-ui-sdk": "0.47.4",
20
- "@vention/machine-ui": "6.6.21",
20
+ "@vention/machine-ui": "6.7.0",
21
21
  "react": "17.0.2",
22
22
  "react-dom": "17.0.2",
23
23
  "react-router-dom": "6.30.3",
@@ -28,29 +28,68 @@ function toLogLevel(level: string): LogType {
28
28
  return "info"
29
29
  }
30
30
 
31
+ function eventKey(entry: LogEntry): string {
32
+ return `${entry.date}|${entry.message}|${entry.description}`
33
+ }
34
+
35
+ // The stream and RecentEvents can both deliver an event published while the HMI connects, in either order.
36
+ function mergeNewestFirst(previous: LogEntry[], incoming: LogEntry[]): LogEntry[] {
37
+ const shown = new Set(previous.map(eventKey))
38
+ const merged = [...previous, ...incoming.filter(entry => !shown.has(eventKey(entry)))]
39
+ return merged.sort((left, right) => right.date.localeCompare(left.date)).slice(0, MAX_LOG_ENTRIES)
40
+ }
41
+
31
42
  /** The server's events, plus the HMI's own command failures, newest first. */
32
- function useEventLog(): { entries: LogEntry[]; addEntry: (level: LogType, message: string, description: string) => string } {
43
+ function useEventLog(connected: boolean): {
44
+ entries: LogEntry[]
45
+ addEntry: (level: LogType, message: string, description: string) => string
46
+ } {
33
47
  const { value: latestEvent } = useStream<Event>(openEvents)
34
48
  const [entries, setEntries] = useState<LogEntry[]>([])
35
49
  const nextId = useRef(0)
36
50
 
37
- const addEntry = useCallback((level: LogType, message: string, description: string, date = new Date().toISOString()) => {
51
+ const toLogEntry = useCallback((level: LogType, message: string, description: string, date: string): LogEntry => {
38
52
  nextId.current += 1
39
- const entry: LogEntry = { id: String(nextId.current), date, level, code: level.toUpperCase(), message, description }
40
- setEntries(previous => [entry, ...previous].slice(0, MAX_LOG_ENTRIES))
41
- return entry.id
53
+ return { id: String(nextId.current), date, level, code: level.toUpperCase(), message, description }
42
54
  }, [])
43
55
 
56
+ const addEntry = useCallback(
57
+ (level: LogType, message: string, description: string, date = new Date().toISOString()) => {
58
+ const entry = toLogEntry(level, message, description, date)
59
+ setEntries(previous => mergeNewestFirst(previous, [entry]))
60
+ return entry.id
61
+ },
62
+ [toLogEntry]
63
+ )
64
+
44
65
  useEffect(() => {
45
66
  if (latestEvent !== undefined) addEntry(toLogLevel(latestEvent.level), latestEvent.message, latestEvent.detail, latestEvent.timestamp)
46
67
  }, [latestEvent, addEntry])
47
68
 
69
+ // The Events stream does not replay, so each (re)connect loads what the server kept.
70
+ useEffect(() => {
71
+ if (!connected) return
72
+ const controller = new AbortController()
73
+ client
74
+ .recentEvents({}, { signal: controller.signal })
75
+ .then(({ events }) =>
76
+ setEntries(previous =>
77
+ mergeNewestFirst(
78
+ previous,
79
+ events.map(event => toLogEntry(toLogLevel(event.level), event.message, event.detail, event.timestamp))
80
+ )
81
+ )
82
+ )
83
+ .catch(() => undefined)
84
+ return () => controller.abort()
85
+ }, [connected, toLogEntry])
86
+
48
87
  return { entries, addEntry }
49
88
  }
50
89
 
51
90
  export function App() {
52
91
  const { value: status, connected } = useStream(openStatus, STATUS_STALL_MS)
53
- const { entries: logEntries, addEntry } = useEventLog()
92
+ const { entries: logEntries, addEntry } = useEventLog(connected)
54
93
  const navigate = useNavigate()
55
94
  const isOnLogsPage = useLocation().pathname === "/logs"
56
95
  // The latest error shows as a banner on every page until it fades; View opens the whole entry on the Logs page.
@@ -4,6 +4,17 @@ package vention.app.v1;
4
4
  import "google/protobuf/empty.proto";
5
5
 
6
6
 
7
+ message Event {
8
+ string timestamp = 1;
9
+ string level = 2;
10
+ string message = 3;
11
+ string detail = 4;
12
+ }
13
+
14
+ message EventHistory {
15
+ repeated Event events = 1;
16
+ }
17
+
7
18
  message StartRequest {
8
19
  int32 recipe_id = 1;
9
20
  }
@@ -117,42 +128,6 @@ message RecipeRestoreResponse {
117
128
  Recipe record = 1;
118
129
  }
119
130
 
120
- message HealthResponse {
121
- string status = 1;
122
- }
123
-
124
- message AuditQueryRequest {
125
- string component = 1;
126
- int32 record_id = 2;
127
- string actor = 3;
128
- string operation = 4;
129
- string since = 5;
130
- string until = 6;
131
- int32 limit = 7;
132
- int32 offset = 8;
133
- }
134
-
135
- message AuditListResponse {
136
- repeated string records = 1;
137
- }
138
-
139
- message FileResponse {
140
- string filename = 1;
141
- bytes bytes = 2;
142
- }
143
-
144
- message DatabaseRestoreRequest {
145
- bytes bytes = 1;
146
- string filename = 2;
147
- bool integrity_check = 3;
148
- bool dry_run = 4;
149
- }
150
-
151
- message DatabaseRestoreResponse {
152
- bool ok = 1;
153
- string message = 2;
154
- }
155
-
156
131
  message StationStatus {
157
132
  string state = 1;
158
133
  string recipe_name = 2;
@@ -165,14 +140,8 @@ message StationStatus {
165
140
  string fault = 9;
166
141
  }
167
142
 
168
- message Event {
169
- string timestamp = 1;
170
- string level = 2;
171
- string message = 3;
172
- string detail = 4;
173
- }
174
-
175
143
  service IndexingStationService {
144
+ rpc RecentEvents (google.protobuf.Empty) returns (EventHistory);
176
145
  rpc Start (StartRequest) returns (CommandResponse);
177
146
  rpc GetState (google.protobuf.Empty) returns (StateResponse);
178
147
  rpc GetHistory (google.protobuf.Empty) returns (HistoryResponse);
@@ -185,11 +154,6 @@ service IndexingStationService {
185
154
  rpc Recipe_UpdateRecord (RecipeUpdateRequest) returns (RecipeUpdateResponse);
186
155
  rpc Recipe_DeleteRecord (CrudDeleteRequest) returns (RecipeStatusResponse);
187
156
  rpc Recipe_RestoreRecord (CrudRestoreRequest) returns (RecipeRestoreResponse);
188
- rpc Database_Health (google.protobuf.Empty) returns (HealthResponse);
189
- rpc Database_ReadAudit (AuditQueryRequest) returns (AuditListResponse);
190
- rpc Database_ExportZip (google.protobuf.Empty) returns (FileResponse);
191
- rpc Database_BackupSqlite (google.protobuf.Empty) returns (FileResponse);
192
- rpc Database_RestoreSqlite (DatabaseRestoreRequest) returns (DatabaseRestoreResponse);
193
157
  rpc Status (google.protobuf.Empty) returns (stream StationStatus);
194
158
  rpc Events (google.protobuf.Empty) returns (stream Event);
195
159
  }
@@ -1,5 +1,5 @@
1
1
  machine-logic-sdk==3.1.0
2
2
  vention-communication==0.15.35
3
- vention-state-machine==0.16.29
3
+ vention-state-machine==0.16.30
4
4
  vention-storage==0.20.13
5
5
  uvicorn==0.35.0
@@ -6,6 +6,7 @@ An emergency stop sends the station to fault, and shutting the server down stops
6
6
 
7
7
  import asyncio
8
8
  import time
9
+ from collections import deque
9
10
  from contextlib import asynccontextmanager
10
11
  from datetime import datetime, timezone
11
12
  from pathlib import Path
@@ -33,6 +34,8 @@ CLAMP_PROOF_TIMEOUT_S = 2.0
33
34
  CLAMP_POLL_S = 0.05
34
35
  # The HMI treats a Status stream that stays silent for three heartbeats as dropped.
35
36
  STATUS_HEARTBEAT_S = 2.0
37
+ # The Events stream does not replay, so the HMI loads these each time it connects.
38
+ RECENT_EVENT_COUNT = 100
36
39
  ACCELERATION_MM_S2 = 400.0
37
40
  DATA_DIR = Path(__file__).parent / "data"
38
41
 
@@ -79,6 +82,10 @@ class Event(BaseModel):
79
82
  detail: str
80
83
 
81
84
 
85
+ class EventHistory(BaseModel):
86
+ events: list[Event]
87
+
88
+
82
89
  class StartRequest(BaseModel):
83
90
  recipe_id: int
84
91
 
@@ -224,6 +231,7 @@ DATA_DIR.mkdir(exist_ok=True)
224
231
  bootstrap(database_url=f"sqlite:///{DATA_DIR / 'recipes.db'}")
225
232
  recipe_accessor = ModelAccessor(Recipe, "recipe")
226
233
  station = IndexingStation()
234
+ recent_events: deque[Event] = deque(maxlen=RECENT_EVENT_COUNT)
227
235
 
228
236
 
229
237
  async def repeat_status() -> None:
@@ -266,7 +274,14 @@ async def publish_status() -> StationStatus:
266
274
 
267
275
  @stream(name="Events", payload=Event, replay=False, queue_maxsize=100, policy="fifo")
268
276
  async def publish_event(level: str, message: str, detail: str) -> Event:
269
- return Event(timestamp=datetime.now(timezone.utc).isoformat(), level=level, message=message, detail=detail)
277
+ event = Event(timestamp=datetime.now(timezone.utc).isoformat(), level=level, message=message, detail=detail)
278
+ recent_events.append(event)
279
+ return event
280
+
281
+
282
+ @action("RecentEvents")
283
+ async def get_recent_events() -> EventHistory:
284
+ return EventHistory(events=list(recent_events))
270
285
 
271
286
 
272
287
  @action("Start")
@@ -286,5 +301,5 @@ async def start(request: StartRequest) -> CommandResponse:
286
301
 
287
302
 
288
303
  app.register_rpc_plugin(build_state_machine_rpc_bundle(station, triggers=[Triggers.stop.name, BaseTriggers.RESET.value]))
289
- app.register_rpc_plugin(build_storage_rpc_bundle(accessors=[recipe_accessor], max_records_per_model=None))
304
+ app.register_rpc_plugin(build_storage_rpc_bundle(accessors=[recipe_accessor], max_records_per_model=None, enable_db_actions=False))
290
305
  app.finalize()
@@ -3,7 +3,7 @@ name: vention-monitoring
3
3
  description: Diagnose deployed Vention machines from controller state, history, health, and activity data. Use when the user reports a machine that is down, faulted, offline, slow, or underperforming, asks why a cycle time regressed, wants to interpret MachineMotion event data or drive and brake status, asks about OEE or uptime, or wants to know which machine in a fleet needs attention. Trigger on a MachineMotion serial number plus any complaint.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.26"
6
+ version: "0.10.28"
7
7
  author: Vention
8
8
  ---
9
9