@semiont/jobs 0.6.7 → 0.6.8

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 CHANGED
@@ -6,11 +6,11 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@semiont/jobs.svg)](https://www.npmjs.com/package/@semiont/jobs)
7
7
  [![License](https://img.shields.io/npm/l/@semiont/jobs.svg)](https://github.com/The-AI-Alliance/semiont/blob/main/LICENSE)
8
8
 
9
- Job queue, worker infrastructure, and annotation workers for [Semiont](https://github.com/The-AI-Alliance/semiont).
9
+ The job worker for [Semiont](https://github.com/The-AI-Alliance/semiont): the processors for each job type, the job types, and the worker process that claims jobs over the bus. The job queue is the dispatcher's ([apps/dispatcher](../../apps/dispatcher/README.md)), and what it does with a job is [docs/protocol/JOBS.md](../../docs/protocol/JOBS.md).
10
10
 
11
11
  ## Architecture Context
12
12
 
13
- Workers run in a separate process and connect to the Knowledge System (KS) over HTTP/SSE using a `SemiontSession` (from `@semiont/sdk`) driven by a `JobClaimAdapter`. Workers receive job assignments via an SSE `job:queued` subscription, claim jobs atomically, and emit domain events back to the KS via `session.client.transport.emit(...)`. The KS ingests these events onto its EventBus for SSE delivery to the Browser.
13
+ Workers run in a separate process and connect to the Knowledge System (KS) over HTTP/SSE using a `SemiontSession` (from `@semiont/sdk`) driven by a `JobClaimAdapter`. Whenever a worker is idle it claims the next pending job of its types with `job:claim` (a `job:queued` announcement wakes a parked worker), and it emits domain events back to the KS via `session.client.transport.emit(...)`. The KS ingests these events onto its EventBus for SSE delivery to the Browser.
14
14
 
15
15
  ## Installation
16
16
 
@@ -19,8 +19,8 @@ npm install @semiont/jobs
19
19
  ```
20
20
 
21
21
  **Dependencies:**
22
- - `@semiont/core` — Core types, `SemiontProject`, EventBus
23
- - `@semiont/sdk` — `SemiontSession`, `WorkerBus` (worker process)
22
+ - `@semiont/core` — Core types, `SemiontProject`, EventBus, `BusRequestPrimitive`
23
+ - `@semiont/sdk` — `SemiontSession`, `SemiontClient` (worker process)
24
24
  - `@semiont/http-transport` — HTTP transport, OpenAPI types
25
25
  - `@semiont/inference` — InferenceClient for AI operations
26
26
  - `@semiont/content` — Content storage URI derivation
@@ -29,44 +29,10 @@ npm install @semiont/jobs
29
29
 
30
30
  ## Quick Start
31
31
 
32
- ```typescript
33
- import { FsJobQueue, type PendingJob, type DetectionParams } from '@semiont/jobs';
34
- import { EventBus, userId, resourceId, jobId } from '@semiont/core';
35
- import { SemiontProject } from '@semiont/core/node';
36
-
37
- // Initialize — jobs are stored under project.jobsDir
38
- const eventBus = new EventBus();
39
- const project = new SemiontProject('/path/to/project', {
40
- anchoredTextDir: process.env.SEMIONT_ANCHORED_TEXT_DIR!,
41
- });
42
- const jobQueue = new FsJobQueue(project, logger, eventBus);
43
- await jobQueue.initialize();
44
-
45
- // Create a job
46
- const job: PendingJob<DetectionParams> = {
47
- status: 'pending',
48
- metadata: {
49
- id: jobId('job-abc123'),
50
- type: 'reference-annotation',
51
- userId: userId('did:web:example.com:users:f47ac10b-58cc-4372-a567-0e02b2c3d479'),
52
- created: new Date().toISOString(),
53
- retryCount: 0,
54
- maxRetries: 1,
55
- },
56
- params: {
57
- resourceId: resourceId('doc-456'),
58
- entityTypes: ['Person', 'Organization'],
59
- },
60
- };
61
-
62
- await jobQueue.createJob(job);
63
- ```
64
-
65
- Generation jobs are enqueued the same way, but their params are a
66
- [`GenerationJobParams`](./docs/JobTypes.md#generation-generation) bag whose
67
- `context` carries the anchor — writing one straight to the queue bypasses the
68
- dispatcher that normally derives and stamps `resourceId` from
69
- `context.focus`, so prefer `client.yield.fromContext(...)`.
32
+ A job is created on the bus (`job:create`), answered by the dispatcher, and claimed by a worker. A
33
+ client starts one through the SDK — `semiont.mark.assist(...)` for an annotation pass,
34
+ `semiont.yield.fromContext(...)` for generation — and follows it with `semiont.job`. A worker is this
35
+ package's `worker-main`, run as the `semiont-worker` image.
70
36
 
71
37
  ## Job Types
72
38
 
@@ -92,12 +58,17 @@ interface JobMetadata {
92
58
  created: string;
93
59
  retryCount: number;
94
60
  maxRetries: number;
95
- completedUnits?: string[]; // Checkpoint: work units (entity types) already
96
- // persisted by a failed attempt — the retry skips them
61
+ completedUnits?: string[]; // Checkpoint: work units already persisted —
62
+ // the retry skips them
63
+ unitCursors?: Record<string, UnitCursor>; // How far each unfinished unit got
97
64
  }
98
65
  ```
99
66
 
100
- `userId` is the job's only identity — the DID the gateway verified on the `job:create`. The dispatcher records it as the requester when it accepts a claim, and that record is what lets the knowledge base attribute a write citing this job; a worker never states it. `completedUnits` is written only by `failJob`, unioned across attempts, and carried on the `job:fail` event — see Failure discipline below.
67
+ `userId` is the job's only identity — the DID the gateway verified on the `job:create`. The dispatcher records it as the requester when it accepts a claim, and that record is what lets the knowledge base attribute a write citing this job; a worker never states it. `completedUnits` and `unitCursors` are written by `checkpointUnits` (from `job:checkpoint`, as work lands) and by `failJob` (from `job:fail`), merged across attempts — see Failure discipline below.
68
+
69
+ ## Progress
70
+
71
+ A running job's `progress` is `StoredProgress`: the last `JobProgress` its worker reported with `job:report-progress`, or `{}` before the first report. It is one shape for every job type — the spec's `JobRunning.progress` — so there are no per-type progress types. See [Job Types Guide](./docs/JobTypes.md#progress).
101
72
 
102
73
  ## Annotation Workers
103
74
 
@@ -112,9 +83,9 @@ The worker process (`worker-main.ts` → `startWorkerProcess` in `worker-process
112
83
  | `comment-annotation` | `processCommentJob` |
113
84
  | `tag-annotation` | `processTagJob` |
114
85
 
115
- Detection logic lives in the `AnnotationDetection` class (`src/workers/annotation-detection.ts`); generation synthesis in `generateResourceFromTopic()` (`src/workers/generation/resource-generation.ts`). Processors never fetch content themselves — the worker process fetches it via `session.client.browse.resourceContent(resourceId)` and passes it in.
86
+ Detection logic lives in the `AnnotationDetection` class (`src/workers/annotation-detection.ts`); generation synthesis in `generateResourceFromTopic()` (`src/workers/generation/resource-generation.ts`). Processors never fetch content themselves — the worker process prepares it with `prepareDetection` (bytes through `contentReads` for text media, the Smelter's anchored text for geometry-bearing media such as PDF) and passes it in.
116
87
 
117
- Workers emit lifecycle events via `session.client.transport.emit('job:start' | 'job:report-progress' | 'job:checkpoint' | 'job:complete' | 'job:fail', payload)` and persist annotations through the **awaited `mark:commit` operation** — a batch per unit of work that resolves only once the Stower actor in @semiont/make-meaning has appended every annotation to the event log. Unit completion and `job:complete` gate on that acknowledgement, never on emission, so a down persistence sink is a retryable failure instead of silent loss. The job command handlers mirror the lifecycle events into the queue files (completion, retry-on-failure with `maxRetries`, progress-as-heartbeat). `job:fail` carries the fields the worker computes: `completedUnits` (the checkpoint), `failureClass`, and `willRetry` — see Failure discipline.
88
+ Workers emit lifecycle events via `session.client.transport.emit('job:start' | 'job:report-progress' | 'job:checkpoint' | 'job:complete' | 'job:fail', payload)` and persist annotations through the **awaited `mark:commit` operation** — a batch per unit of work that resolves only once the Stower actor in @semiont/make-meaning has appended every annotation to the event log. Unit completion and `job:complete` gate on that acknowledgement, never on emission, so a down persistence sink is a retryable failure instead of silent loss. The dispatcher's job command handlers mirror the lifecycle events into the queue (completion, checkpoints, retry-on-failure with `maxRetries`, progress-as-heartbeat). `job:fail` carries the fields the worker computes: `completedUnits` and `unitCursors` (the checkpoint), `failureClass`, and `willRetry` — see Failure discipline.
118
89
 
119
90
  ## Failure discipline
120
91
 
@@ -126,13 +97,13 @@ Long inference work fails in bounded, classified, resumable ways — every piece
126
97
  - **Successful-looking extractions are verified** (`assertYieldNotCollapsed`). A local model can return a clean, schema-conforming response carrying a fraction of the entities present — deterministic and otherwise invisible. When the provider declares `verifyDetectionYield` (all real providers do), each chunk's item count is checked against a cheap parallel count call; an extraction under half the count is flagged and subdivided. Every anchoring outcome and every call — including flagged and failed ones — is recorded to `semiont.detection.*` metrics (`@semiont/observability`).
127
98
  - **Entity types run concurrently up to the provider's declared capacity** (`client.maxConcurrency`): a hosted API with rate headroom runs several types at once; a local single-model server runs them sequentially, because concurrent requests only split one GPU. Jobs never switches on provider identity — both behaviors are capabilities declared on the `InferenceClient`.
128
99
  - **Failures are classified at the worker, where errors are still typed** (`failure-class.ts`). Only KNOWN-deterministic failures — truncation at the subdivision floor, unsupported media, non-throttle 4xx — skip the retry budget; everything unrecognized stays retryable. The class rides `job:fail` as `failureClass`.
129
- - **Retries resume from the checkpoint.** Reference-annotation persists each entity type's annotations as that unit completes; completed units ride `job:fail` into `metadata.completedUnits`, and the retry processes only what's left.
100
+ - **Retries resume from the checkpoint.** Reference-annotation persists each entity type's annotations as that unit completes; completed units ride `job:checkpoint` and `job:fail` into `metadata.completedUnits`, and the retry processes only what's left.
130
101
 
131
102
  ## Adding a Job Type
132
103
 
133
104
  Workers are not subclassed. To add a job type:
134
105
 
135
- 1. Add the new `JobType` and its params/result/progress types in `src/types.ts`.
106
+ 1. Add the new `JobType` and its params type in `src/types.ts`, and in the spec: the type in `JobType.json`, its category in `specs/src/jobs/storage.json`, and its result schema in the `JobResult` union. There is no progress type — every job reports `JobProgress`.
136
107
  2. Add a `process*Job` function in `src/processors.ts` that runs the inference and returns the annotations/result.
137
108
  3. Dispatch the new `jobType` to that processor in `handleJobInner()` in `src/worker-process.ts`.
138
109
 
@@ -145,7 +116,7 @@ Jobs use TypeScript discriminated unions for type safety:
145
116
  ```typescript
146
117
  function handleJob(job: AnyJob) {
147
118
  if (job.status === 'running') {
148
- console.log(job.progress); // Available
119
+ console.log(job.progress); // Available — StoredProgress, `{}` until the first report
149
120
  // console.log(job.result); // Compile error
150
121
  }
151
122
  if (job.status === 'complete') {
@@ -157,24 +128,16 @@ function handleJob(job: AnyJob) {
157
128
 
158
129
  ## Storage Format
159
130
 
160
- Jobs are stored as individual JSON files organized by status:
161
-
162
- ```
163
- {project.jobsDir}/
164
- pending/job-abc123.json
165
- running/job-def456.json
166
- complete/job-ghi789.json
167
- failed/job-jkl012.json
168
- cancelled/job-mno345.json
169
- ```
131
+ The dispatcher keeps one `JobRecord` per job in a JetStream key-value bucket, in the layout
132
+ `specs/src/jobs/storage.json` states. A worker never reads it: it sees a job only as the dispatcher
133
+ hands it over on `job:claimed`.
170
134
 
171
135
  ## Documentation
172
136
 
173
- - **[Job Queue Guide](./docs/JobQueue.md)** — JobQueue API and job management
174
137
  - **[Workers Guide](./docs/Workers.md)** — Building custom workers
175
138
  - **[Job Types Guide](./docs/JobTypes.md)** — All job type definitions
176
139
  - **[Type System Guide](./docs/TYPES.md)** — Discriminated unions and type safety
177
- - **[Configuration Guide](./docs/Configuration.md)** — Setup and options
140
+ - **[Configuration Guide](./docs/Configuration.md)** — Running a worker
178
141
  - **[API Reference](./docs/API.md)** — Complete API reference
179
142
 
180
143
  ## License
@@ -183,8 +146,8 @@ Apache-2.0
183
146
 
184
147
  ## Related Packages
185
148
 
186
- - [`@semiont/core`](../core/) — Domain types, `SemiontProject`, EventBus
187
- - [`@semiont/sdk`](../sdk/) — `SemiontSession`, `WorkerBus`
149
+ - [`@semiont/core`](../core/) — Domain types, `SemiontProject`, EventBus, `BusRequestPrimitive`
150
+ - [`@semiont/sdk`](../sdk/) — `SemiontSession`, `SemiontClient`
188
151
  - [`@semiont/http-transport`](../http-transport/) — HTTP transport, OpenAPI types
189
152
  - [`@semiont/inference`](../inference/) — AI inference client
190
153
  - [`@semiont/make-meaning`](../make-meaning/) — Actor model, Knowledge Base, service orchestration