@semiont/jobs 0.6.9 → 0.6.10
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 +16 -16
- package/dist/index.d.ts +117 -130
- package/dist/index.js +23 -25
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +43 -45
- package/dist/worker-main.js.map +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The job worker for [Semiont](https://github.com/The-AI-Alliance/semiont): the pr
|
|
|
10
10
|
|
|
11
11
|
## Architecture Context
|
|
12
12
|
|
|
13
|
-
Workers run in a separate process and connect to the
|
|
13
|
+
Workers run in a separate process and connect to the knowledge base's gateway over HTTP/SSE using a `SemiontSession` (from `@semiont/sdk`), with a `JobClaimAdapter` on the session's bus connection. 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 its lifecycle events onto the bus via `session.client.transport.emit(...)`. The gateway relays them over SSE to the dispatcher, the Stower and clients such as the Browser.
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -19,11 +19,11 @@ npm install @semiont/jobs
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
**Dependencies:**
|
|
22
|
-
- `@semiont/core` — Core types, `
|
|
22
|
+
- `@semiont/core` — Core types, the bus protocol (`busRequest`, `BusRequestPrimitive`), the config loader
|
|
23
23
|
- `@semiont/sdk` — `SemiontSession`, `SemiontClient` (worker process)
|
|
24
|
-
- `@semiont/http-transport` —
|
|
24
|
+
- `@semiont/http-transport` — `HttpTransport`, `HttpContentTransport`
|
|
25
25
|
- `@semiont/inference` — InferenceClient for AI operations
|
|
26
|
-
- `@semiont/content` —
|
|
26
|
+
- `@semiont/content` — Resource bytes read from the Archivist (`archivistContentReads`), PDF text-layer extraction, the output byte budget
|
|
27
27
|
- `@semiont/event-sourcing` — Annotation id generation
|
|
28
28
|
- `@semiont/observability` — Spans and job-outcome metrics
|
|
29
29
|
|
|
@@ -64,7 +64,7 @@ interface JobMetadata {
|
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
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
|
|
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 the dispatcher, from `job:checkpoint` (as work lands) and from `job:fail`, merged across attempts — see Failure discipline below.
|
|
68
68
|
|
|
69
69
|
## Progress
|
|
70
70
|
|
|
@@ -72,7 +72,7 @@ A running job's `progress` is `StoredProgress`: the last `JobProgress` its worke
|
|
|
72
72
|
|
|
73
73
|
## Annotation Workers
|
|
74
74
|
|
|
75
|
-
The worker process (`worker-main.ts` → `startWorkerProcess` in `worker-process.ts`) claims jobs over the bus via a `JobClaimAdapter` and dispatches by `jobType` to a processor function. There are no per-type worker classes; each job type maps to one `process*Job` function:
|
|
75
|
+
The worker process (`worker-main.ts` → `startAgentWorker` in `worker-runtime.ts` → `startWorkerProcess` in `worker-process.ts`) claims jobs over the bus via a `JobClaimAdapter` and dispatches by `jobType` to a processor function. There are no per-type worker classes; each job type maps to one `process*Job` function:
|
|
76
76
|
|
|
77
77
|
| Job Type | Processor |
|
|
78
78
|
|----------|-----------|
|
|
@@ -83,28 +83,28 @@ The worker process (`worker-main.ts` → `startWorkerProcess` in `worker-process
|
|
|
83
83
|
| `comment-annotation` | `processCommentJob` |
|
|
84
84
|
| `tag-annotation` | `processTagJob` |
|
|
85
85
|
|
|
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.
|
|
86
|
+
Detection logic lives in the `AnnotationDetection` class (`src/workers/annotation-detection.ts`) and, for references, in `extractEntities` (`src/workers/detection/entity-extractor.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.
|
|
87
87
|
|
|
88
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.
|
|
89
89
|
|
|
90
90
|
## Failure discipline
|
|
91
91
|
|
|
92
|
-
Long inference work fails in bounded, classified, resumable ways
|
|
92
|
+
Long inference work fails in bounded, classified, resumable ways:
|
|
93
93
|
|
|
94
94
|
- **Every inference call is bounded and truly cancelled.** A call gets 10 minutes (`INFERENCE_TIMEOUT_MS`); at the bound the worker aborts it at the transport (`AbortSignal` through the provider SDK to the socket — milliseconds to rejection, no zombie billing on) and fails the job with a typed `InferenceTimeoutError`. An in-flight heartbeat reports elapsed-time liveness every 15 s during long calls.
|
|
95
|
-
- **Budgets are derived, never tuned** (`workers/detection/detection-chunking.ts`). Input:output allocation is 1:2 per entity type asked for (`input ≤ outputBudget / (2 × typesPerCall)`), and **every** provider gets a duration cap: per-call output is bounded at what the provider's worst-case rate finishes in HALF the bound — the published rate when there is one, a conservative assumed floor (`ASSUMED_OUTPUT_TOKENS_PER_HOUR`, 30 tok/s) for rate-silent providers like Ollama, where an unbounded budget
|
|
96
|
-
- **Size-shaped failures subdivide in place** (`callChunkSubdividing`).
|
|
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
|
|
95
|
+
- **Budgets are derived, never tuned** (`workers/detection/detection-chunking.ts`). Input:output allocation is 1:2 per entity type asked for (`input ≤ outputBudget / (2 × typesPerCall)`), and **every** provider gets a duration cap: per-call output is bounded at what the provider's worst-case rate finishes in HALF the bound — the published rate when there is one, a conservative assumed floor (`ASSUMED_OUTPUT_TOKENS_PER_HOUR`, 30 tok/s) for rate-silent providers like Ollama, where an unbounded budget turns a model repetition loop into an hour-long transient burn.
|
|
96
|
+
- **Size-shaped failures subdivide in place** (`callChunkSubdividing`). Five failure families descend, each with its own floor: a **truncation** descends by size and gets one same-size re-roll at the floor; a **timeout** descends two levels then propagates; an **`'unknown'`-stop unreadable response** (garbage output with no stop reason — measured size-correlated on real documents) descends by size and propagates at the floor; an **unreadable response from a model that finished** (`end_turn`) descends two levels then propagates, as a timeout does; and a **flagged under-report** (below) descends by size, and at the floor its salvage — everything it did find, every span write-time-verified — is **accepted loudly** rather than discarded. A piece that cannot actually shrink is at its floor regardless of arithmetic: at temperature 0, an identical re-run returns the identical failure. Sub-piece overlap duplicates fall to the existing span-keyed dedupe.
|
|
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 count call over the same text; 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`).
|
|
98
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`.
|
|
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,
|
|
100
|
-
- **Retries resume from the checkpoint.**
|
|
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, a 4xx other than 408 or 429 — skip the retry budget; everything unrecognized stays retryable. The class rides `job:fail` as `failureClass`.
|
|
100
|
+
- **Retries resume from the checkpoint.** Every detection job commits its annotations chunk by chunk; each committed chunk's cursor rides `job:checkpoint` and `job:fail` into `metadata.unitCursors`, and reference-annotation also records each finished entity type in `metadata.completedUnits`. The retry skips the completed units and resumes each unfinished one from its cursor.
|
|
101
101
|
|
|
102
102
|
## Adding a Job Type
|
|
103
103
|
|
|
104
104
|
Workers are not subclassed. To add a job type:
|
|
105
105
|
|
|
106
|
-
1. Add the new
|
|
107
|
-
2. Add a `process*Job` function in `src/processors.ts` that runs the inference and returns the
|
|
106
|
+
1. Add the new type to the spec: its name in `JobType.json` (`JobType` in `src/types.ts` is generated from it), its category in `specs/src/jobs/storage.json`, and its result schema in the `JobResult` union. Add its params type in `src/types.ts`. There is no progress type — every job reports `JobProgress`.
|
|
107
|
+
2. Add a `process*Job` function in `src/processors.ts` that runs the inference, commits its annotations through the per-chunk callback and returns the result.
|
|
108
108
|
3. Dispatch the new `jobType` to that processor in `handleJobInner()` in `src/worker-process.ts`.
|
|
109
109
|
|
|
110
110
|
Processors are transport-agnostic: they take content, an `InferenceClient`, the job params, a `buildAnnotation` closure (which carries the `generator` — the worker's own `Software` agent), an `onProgress` callback and a per-chunk commit callback, and return a result. No user identity reaches a processor: an annotation states what produced it, and who requested it is derived by the knowledge base from the job the commit cites. The worker process handles claiming, content fetching, committing, and lifecycle event emission.
|
|
@@ -148,6 +148,6 @@ Apache-2.0
|
|
|
148
148
|
|
|
149
149
|
- [`@semiont/core`](../core/) — Domain types, `SemiontProject`, EventBus, `BusRequestPrimitive`
|
|
150
150
|
- [`@semiont/sdk`](../sdk/) — `SemiontSession`, `SemiontClient`
|
|
151
|
-
- [`@semiont/http-transport`](../http-transport/) —
|
|
151
|
+
- [`@semiont/http-transport`](../http-transport/) — `HttpTransport`, `HttpContentTransport`
|
|
152
152
|
- [`@semiont/inference`](../inference/) — AI inference client
|
|
153
153
|
- [`@semiont/make-meaning`](../make-meaning/) — Actor model, Knowledge Base, service orchestration
|
package/dist/index.d.ts
CHANGED
|
@@ -35,25 +35,24 @@ interface JobMetadata {
|
|
|
35
35
|
retryCount: number;
|
|
36
36
|
maxRetries: number;
|
|
37
37
|
/**
|
|
38
|
-
* Checkpointed resume
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* because
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* duplicated.
|
|
38
|
+
* Checkpointed resume: the entity-type units whose annotations are fully
|
|
39
|
+
* committed. Written by the dispatcher's queue from `job:checkpoint` (as
|
|
40
|
+
* each unit lands) and from `job:fail`, unioned across attempts — and
|
|
41
|
+
* because a retried record keeps its metadata, the checkpoint survives
|
|
42
|
+
* every retry. A retried claim skips these units, so completed work is
|
|
43
|
+
* neither redone nor duplicated.
|
|
45
44
|
*/
|
|
46
45
|
completedUnits?: string[];
|
|
47
46
|
/**
|
|
48
|
-
* The finer grain `completedUnits` cannot express
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
47
|
+
* The finer grain `completedUnits` cannot express: how far each UNFINISHED
|
|
48
|
+
* unit got, keyed by unit. Written per committed chunk, so a job that dies
|
|
49
|
+
* mid-unit resumes there rather than at the top — which for a one-unit job
|
|
50
|
+
* (every motivation job, and the 1958 document's single `Person` type) is
|
|
51
|
+
* the difference between resuming and restarting.
|
|
53
52
|
*
|
|
54
53
|
* A unit here is in progress, never complete; the two sets are disjoint by
|
|
55
|
-
* construction
|
|
56
|
-
*
|
|
54
|
+
* construction — the dispatcher's merge drops a completed unit's cursor.
|
|
55
|
+
* Merged monotonically per unit (the furthest cursor wins), never unioned.
|
|
57
56
|
*/
|
|
58
57
|
unitCursors?: Record<string, UnitCursor>;
|
|
59
58
|
}
|
|
@@ -226,16 +225,16 @@ declare function isFailedJob(job: AnyJob): job is FailedJob<any>;
|
|
|
226
225
|
declare function isCancelledJob(job: AnyJob): job is CancelledJob<any>;
|
|
227
226
|
|
|
228
227
|
/**
|
|
229
|
-
* Citation-token resolver
|
|
228
|
+
* Citation-token resolver.
|
|
230
229
|
*
|
|
231
230
|
* Under `cite`, the generation prompt asks the model to emit `[[<resourceId>]]` /
|
|
232
231
|
* `[[<resourceId>/<annotationId>]]` transport tokens after each claim, citing ids
|
|
233
|
-
* the context embedding put in front of it
|
|
234
|
-
* TRANSPORT, not content: this resolver parses
|
|
235
|
-
* ids actually present in the embedded context
|
|
236
|
-
* unknown id is dropped loudly, never silently
|
|
237
|
-
* content before upload, and returns claim-span
|
|
238
|
-
* linking annotations on the derived resource.
|
|
232
|
+
* the context embedding put in front of it: every embedded excerpt carries a
|
|
233
|
+
* `[<resourceId>]` handle. Tokens are TRANSPORT, not content: this resolver parses
|
|
234
|
+
* them, validates each id against the ids actually present in the embedded context
|
|
235
|
+
* (the hallucination guard — an unknown id is dropped loudly, never silently
|
|
236
|
+
* linked), STRIPS them from the content before upload, and returns claim-span
|
|
237
|
+
* citations the worker mints as W3C linking annotations on the derived resource.
|
|
239
238
|
*/
|
|
240
239
|
|
|
241
240
|
interface GenerationCitation {
|
|
@@ -286,13 +285,12 @@ type BuildAnnotation = (motivation: Motivation, match: SpanMatch, body?: Annotat
|
|
|
286
285
|
* on EVERY call: the client's `progress$` replaces its value per event, so a
|
|
287
286
|
* field sent once disappears on the next tick.
|
|
288
287
|
*
|
|
289
|
-
* `message` is a CODE plus typed params, never a prose sentence
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* new sentence here.
|
|
288
|
+
* `message` is a CODE plus typed params, never a prose sentence. The producer
|
|
289
|
+
* reports what happened; each client renders it in the user's language —
|
|
290
|
+
* react-ui from its 29 locales, the Go launcher from its English map. The
|
|
291
|
+
* vocabulary is frozen by the census of these call sites: adding a shape
|
|
292
|
+
* means adding a variant to `JobProgressMessage.json` and copy in every
|
|
293
|
+
* client, not composing a new sentence here.
|
|
296
294
|
*/
|
|
297
295
|
type OnProgress = (percentage: number, message: JobProgressMessage, extra?: Partial<JobProgress>) => void;
|
|
298
296
|
type JobProgress = components['schemas']['JobProgress'];
|
|
@@ -309,8 +307,9 @@ interface ProcessorResult<R> {
|
|
|
309
307
|
result: R;
|
|
310
308
|
}
|
|
311
309
|
/**
|
|
312
|
-
* Where one unit stands once the chunk just handed over is durable
|
|
313
|
-
*
|
|
310
|
+
* Where one unit stands once the chunk just handed over is durable: the
|
|
311
|
+
* cursor a retry resumes that unit from, instead of re-running the chunks it
|
|
312
|
+
* already committed.
|
|
314
313
|
*
|
|
315
314
|
* The unit is named HERE rather than in the detection layer, which knows about
|
|
316
315
|
* chunks and nothing about jobs: for `reference-annotation` a unit is an entity
|
|
@@ -324,32 +323,32 @@ interface UnitCheckpoint {
|
|
|
324
323
|
declare function processHighlightJob(content: string, inferenceClient: InferenceClient, params: HighlightDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
|
|
325
324
|
/** This chunk's novel annotations, awaited: the durability write. */
|
|
326
325
|
onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
|
|
327
|
-
/** Where earlier attempts left each unit
|
|
328
|
-
*
|
|
329
|
-
*
|
|
326
|
+
/** Where earlier attempts left each unit, keyed the same way the checkpoint
|
|
327
|
+
* is. A unit absent here starts at the top, which is every unit of a first
|
|
328
|
+
* attempt. */
|
|
330
329
|
resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobHighlightAnnotationResult>>;
|
|
331
330
|
declare function processCommentJob(content: string, inferenceClient: InferenceClient, params: CommentDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
|
|
332
331
|
/** This chunk's novel annotations, awaited: the durability write. */
|
|
333
332
|
onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
|
|
334
|
-
/** Where earlier attempts left each unit
|
|
335
|
-
*
|
|
336
|
-
*
|
|
333
|
+
/** Where earlier attempts left each unit, keyed the same way the checkpoint
|
|
334
|
+
* is. A unit absent here starts at the top, which is every unit of a first
|
|
335
|
+
* attempt. */
|
|
337
336
|
resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobCommentAnnotationResult>>;
|
|
338
337
|
declare function processAssessmentJob(content: string, inferenceClient: InferenceClient, params: AssessmentDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
|
|
339
338
|
/** This chunk's novel annotations, awaited: the durability write. */
|
|
340
339
|
onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
|
|
341
|
-
/** Where earlier attempts left each unit
|
|
342
|
-
*
|
|
343
|
-
*
|
|
340
|
+
/** Where earlier attempts left each unit, keyed the same way the checkpoint
|
|
341
|
+
* is. A unit absent here starts at the top, which is every unit of a first
|
|
342
|
+
* attempt. */
|
|
344
343
|
resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobAssessmentAnnotationResult>>;
|
|
345
344
|
/**
|
|
346
|
-
* Reference detection
|
|
347
|
-
* `onUnitComplete
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
345
|
+
* Reference detection checkpoints per UNIT — one entity type — through
|
|
346
|
+
* `onUnitComplete`: only after the callback resolves does the unit count as
|
|
347
|
+
* complete, and a retried claim skips the units recorded that way.
|
|
348
|
+
* Annotations leave per chunk through `onChunkComplete`; the processor
|
|
349
|
+
* returns only the result — returning the annotations as well would make a
|
|
350
|
+
* post-run batch, where one failed call discards every completed unit's
|
|
351
|
+
* work wholesale.
|
|
353
352
|
*/
|
|
354
353
|
declare function processReferenceJob(content: string, inferenceClient: InferenceClient, params: DetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress, logger: Logger,
|
|
355
354
|
/**
|
|
@@ -361,18 +360,18 @@ declare function processReferenceJob(content: string, inferenceClient: Inference
|
|
|
361
360
|
onUnitComplete: (entityType: string) => Promise<void>, signal?: AbortSignal,
|
|
362
361
|
/** This chunk's novel annotations, awaited: the durability write. */
|
|
363
362
|
onChunkComplete?: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
|
|
364
|
-
/** Where earlier attempts left each entity-type unit
|
|
365
|
-
*
|
|
366
|
-
*
|
|
363
|
+
/** Where earlier attempts left each entity-type unit. A unit absent here
|
|
364
|
+
* starts at the top; units already COMPLETE never reach this function at
|
|
365
|
+
* all, the caller having filtered them out. */
|
|
367
366
|
resumeCursors?: Record<string, UnitCursor>): Promise<{
|
|
368
367
|
result: JobReferenceAnnotationResult;
|
|
369
368
|
}>;
|
|
370
369
|
declare function processTagJob(content: string, inferenceClient: InferenceClient, params: TagDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
|
|
371
370
|
/** This chunk's novel annotations, awaited: the durability write. */
|
|
372
371
|
onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
|
|
373
|
-
/** Where earlier attempts left each CATEGORY
|
|
374
|
-
*
|
|
375
|
-
*
|
|
372
|
+
/** Where earlier attempts left each CATEGORY — a tag job's units are its
|
|
373
|
+
* categories, not its motivation: each walks the whole document, so one
|
|
374
|
+
* shared cursor would skip text for all but one of them. */
|
|
376
375
|
resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobTagAnnotationResult>>;
|
|
377
376
|
declare function processGenerationJob(inferenceClient: InferenceClient, params: GenerationJobParams, onProgress: OnProgress, logger: Logger): Promise<{
|
|
378
377
|
content: Uint8Array;
|
|
@@ -396,22 +395,17 @@ declare function processGenerationJob(inferenceClient: InferenceClient, params:
|
|
|
396
395
|
* `num_ctx`. What remains after the prompt scaffold is split
|
|
397
396
|
* input:output = 1:2 (annotation JSON echoes each span plus a fixed
|
|
398
397
|
* key/context envelope, so output needs the larger share). The 1:2 ratio
|
|
399
|
-
* is the
|
|
400
|
-
*
|
|
398
|
+
* is the one allocation policy — doc-independent, tuned only on live
|
|
399
|
+
* evidence.
|
|
401
400
|
* - Separate ceilings (Anthropic): output takes its full ceiling (duration-
|
|
402
401
|
* capped below), and input follows the same 1:2 allocation — never more
|
|
403
402
|
* than half the output budget. Input does NOT get "the rest of the
|
|
404
|
-
* window":
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* not a cost.
|
|
403
|
+
* window": a window-sized chunk of entity-dense text demands more output
|
|
404
|
+
* than any budget holds, so the model grinds toward max_tokens for minutes
|
|
405
|
+
* (killed at the call bound as a "stall") or collapses to the degenerate
|
|
406
|
+
* []. Large documents chunk; that is the fix, not a cost.
|
|
409
407
|
*/
|
|
410
408
|
|
|
411
|
-
/** One chunk handed out by `runAdaptiveChunks`, with the cursor either side of
|
|
412
|
-
* it. `at`/`next` over `totalChars` is exact progress — and, once
|
|
413
|
-
* CHUNK-GRAIN-RESUME lands, the checkpoint identity a variable boundary forces
|
|
414
|
-
* (an ordinal cannot name a chunk whose size is decided while the job runs). */
|
|
415
409
|
/**
|
|
416
410
|
* The half of a `UnitCursor` the DETECTION layer can honestly report: where the
|
|
417
411
|
* walk stands and how it is cutting. The tallies belong to the processor, which
|
|
@@ -423,9 +417,9 @@ type ChunkCursor = Pick<UnitCursor, 'next' | 'size'>;
|
|
|
423
417
|
/**
|
|
424
418
|
* Response parsers for annotation detection motivations
|
|
425
419
|
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
420
|
+
* Static methods that validate the model's already-parsed elements for each
|
|
421
|
+
* motivation type and anchor each span in the document: the model emits no
|
|
422
|
+
* offsets, `reconcileSelector` computes them.
|
|
429
423
|
*
|
|
430
424
|
* NOTE: These are static utility methods without logger access.
|
|
431
425
|
* Console statements kept for debugging - consider adding logger parameter in future.
|
|
@@ -497,8 +491,7 @@ declare class AnnotationDetection {
|
|
|
497
491
|
* full discussion.
|
|
498
492
|
*/
|
|
499
493
|
static detectComments(content: string, client: InferenceClient, instructions?: string, tone?: string, density?: number, language?: string, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
|
|
500
|
-
/**
|
|
501
|
-
/** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
|
|
494
|
+
/** Where an earlier attempt left this unit. */
|
|
502
495
|
resume?: UnitCursor,
|
|
503
496
|
/** This chunk's matches, as the chunk completes. Kept LAST. */
|
|
504
497
|
onChunkResults?: (matches: CommentMatch[], cursor: ChunkCursor) => Promise<void>): Promise<CommentMatch[]>;
|
|
@@ -510,8 +503,7 @@ declare class AnnotationDetection {
|
|
|
510
503
|
* correctly.
|
|
511
504
|
*/
|
|
512
505
|
static detectHighlights(content: string, client: InferenceClient, instructions?: string, density?: number, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
|
|
513
|
-
/**
|
|
514
|
-
/** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
|
|
506
|
+
/** Where an earlier attempt left this unit. */
|
|
515
507
|
resume?: UnitCursor,
|
|
516
508
|
/** This chunk's matches, as the chunk completes. Kept LAST. */
|
|
517
509
|
onChunkResults?: (matches: HighlightMatch[], cursor: ChunkCursor) => Promise<void>): Promise<HighlightMatch[]>;
|
|
@@ -523,8 +515,7 @@ declare class AnnotationDetection {
|
|
|
523
515
|
* being analyzed (source-resource locale).
|
|
524
516
|
*/
|
|
525
517
|
static detectAssessments(content: string, client: InferenceClient, instructions?: string, tone?: string, density?: number, language?: string, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
|
|
526
|
-
/**
|
|
527
|
-
/** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
|
|
518
|
+
/** Where an earlier attempt left this unit. */
|
|
528
519
|
resume?: UnitCursor,
|
|
529
520
|
/** This chunk's matches, as the chunk completes. Kept LAST. */
|
|
530
521
|
onChunkResults?: (matches: AssessmentMatch[], cursor: ChunkCursor) => Promise<void>): Promise<AssessmentMatch[]>;
|
|
@@ -541,21 +532,22 @@ declare class AnnotationDetection {
|
|
|
541
532
|
* site, not here.
|
|
542
533
|
*/
|
|
543
534
|
static detectTags(content: string, client: InferenceClient, schema: TagSchema, category: string, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
|
|
535
|
+
/** Where an earlier attempt left this unit. */
|
|
536
|
+
resume?: UnitCursor,
|
|
544
537
|
/**
|
|
545
|
-
* This chunk's matches,
|
|
546
|
-
* raw tags, so this path runs
|
|
547
|
-
*
|
|
538
|
+
* This chunk's matches, as the chunk completes, ANCHORED before they
|
|
539
|
+
* leave: `parse` here yields raw tags, so this path runs
|
|
540
|
+
* `validateTagOffsets` per chunk — a per-item anchor against the full
|
|
541
|
+
* document, so partitioning changes nothing. Kept LAST.
|
|
548
542
|
*/
|
|
549
|
-
/** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
|
|
550
|
-
resume?: UnitCursor,
|
|
551
|
-
/** This chunk's matches, as the chunk completes. Kept LAST. */
|
|
552
543
|
onChunkResults?: (matches: TagMatch[], cursor: ChunkCursor) => Promise<void>): Promise<TagMatch[]>;
|
|
553
544
|
}
|
|
554
545
|
|
|
555
546
|
/**
|
|
556
547
|
* Resource Generation
|
|
557
548
|
*
|
|
558
|
-
* Generates
|
|
549
|
+
* Generates resource content from a topic using AI inference: markdown, plain
|
|
550
|
+
* text, or Typst source (which the worker compiles to PDF), by `outputMediaType`.
|
|
559
551
|
*/
|
|
560
552
|
|
|
561
553
|
/**
|
|
@@ -582,12 +574,11 @@ declare function generateResourceFromTopic(topic: string, entityTypes: string[],
|
|
|
582
574
|
* Job Claim Adapter — worker-side job lifecycle glue on top of a
|
|
583
575
|
* shared bus.
|
|
584
576
|
*
|
|
585
|
-
* Replaces the old `WorkerStateUnit`, which owned its own actor and
|
|
586
|
-
* duplicated the SSE connection that `SemiontClient` already held.
|
|
587
577
|
* Workers construct a `SemiontSession` normally (one actor, one
|
|
588
578
|
* SSE connection) and use this adapter to attach job-claim behaviour
|
|
589
|
-
* on top of the session's bus
|
|
590
|
-
* concerns, and has no modal
|
|
579
|
+
* on top of the session's bus, so the worker holds no second connection.
|
|
580
|
+
* It does **not** own the bus, has no HTTP concerns, and has no modal
|
|
581
|
+
* state.
|
|
591
582
|
*
|
|
592
583
|
* THE MODEL — a worker asks the queue at every moment it becomes idle,
|
|
593
584
|
* and never otherwise. `job:claim` carries this worker's types; the
|
|
@@ -619,9 +610,9 @@ declare function generateResourceFromTopic(topic: string, entityTypes: string[],
|
|
|
619
610
|
* can never claim has nothing to do here and the launcher's preflight
|
|
620
611
|
* names the repair.
|
|
621
612
|
*
|
|
622
|
-
* The queue drivers' 30 s re-announce tick
|
|
623
|
-
*
|
|
624
|
-
*
|
|
613
|
+
* The queue drivers' 30 s re-announce tick is INSURANCE against a lost
|
|
614
|
+
* wake-up on an idle worker — never dispatch. A healthy stack never sees it
|
|
615
|
+
* act.
|
|
625
616
|
*
|
|
626
617
|
* The `bus` parameter is typed against the small `BusRequestPrimitive`
|
|
627
618
|
* interface (from `@semiont/core`) so the adapter is transport-neutral.
|
|
@@ -641,25 +632,27 @@ interface ActiveJob {
|
|
|
641
632
|
resourceId: ResourceId;
|
|
642
633
|
params: ClaimedJob['params'];
|
|
643
634
|
/**
|
|
644
|
-
* Entity-type units earlier
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
* neither redoes nor duplicates completed work. Empty on first
|
|
635
|
+
* Entity-type units earlier attempts fully committed — the checkpoint a
|
|
636
|
+
* resume reads, carried on the claimed record's metadata, where the queue
|
|
637
|
+
* writes it from `job:checkpoint` and `job:fail`. The worker skips them, so
|
|
638
|
+
* a retry neither redoes nor duplicates completed work. Empty on first
|
|
639
|
+
* attempts.
|
|
648
640
|
*/
|
|
649
641
|
completedUnits: string[];
|
|
650
642
|
/**
|
|
651
|
-
* How far each UNFINISHED unit got on an earlier attempt
|
|
652
|
-
*
|
|
653
|
-
* the only checkpoint a one-unit
|
|
654
|
-
* on first attempts, and never
|
|
655
|
-
* either finished or partway,
|
|
643
|
+
* How far each UNFINISHED unit got on an earlier attempt: the offset where
|
|
644
|
+
* its last committed chunk ended and the size that chunk was cut at — the
|
|
645
|
+
* grain `completedUnits` cannot express, and the only checkpoint a one-unit
|
|
646
|
+
* job can produce before it finishes. Empty on first attempts, and never
|
|
647
|
+
* overlapping `completedUnits`: a unit is either finished or partway,
|
|
648
|
+
* never both.
|
|
656
649
|
*/
|
|
657
650
|
unitCursors: Record<string, UnitCursor>;
|
|
658
651
|
/**
|
|
659
652
|
* The claimed record's retry budget, carried so the worker can report
|
|
660
|
-
* `willRetry` on `job:fail
|
|
661
|
-
*
|
|
662
|
-
*
|
|
653
|
+
* `willRetry` on `job:fail`. It is the same budget the queue re-reads at
|
|
654
|
+
* `failJob`, and only `failJob` changes it, so the two evaluations of
|
|
655
|
+
* `willRetryAfter` agree.
|
|
663
656
|
*/
|
|
664
657
|
retryCount: number;
|
|
665
658
|
maxRetries: number;
|
|
@@ -686,9 +679,9 @@ interface ClaimRefusal {
|
|
|
686
679
|
message: string;
|
|
687
680
|
}
|
|
688
681
|
/**
|
|
689
|
-
* Point-in-time liveness snapshot
|
|
690
|
-
*
|
|
691
|
-
*
|
|
682
|
+
* Point-in-time liveness snapshot. The adapter is the only component that
|
|
683
|
+
* sees every wake-up, claim, and finish, so its snapshot is what `/health`
|
|
684
|
+
* reports and the stall watchdog reads.
|
|
692
685
|
*
|
|
693
686
|
* `lastQueuedEventAt` is any `job:queued` received, matching or not. It
|
|
694
687
|
* proves the transport delivered a broadcast, but on an idle stack with an
|
|
@@ -766,53 +759,47 @@ declare function createJobClaimAdapter(options: JobClaimAdapterOptions): JobClai
|
|
|
766
759
|
* a unit test needs to reach lives here instead, fully parameterized — no
|
|
767
760
|
* module-scope env reads, no side effects at import.
|
|
768
761
|
*
|
|
769
|
-
* The load-bearing contract this module owns
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
* previously got two DIDs that way (.plans/bugs/agent-did-host-skew.md).
|
|
762
|
+
* The load-bearing contract this module owns: a worker's stamped identity is
|
|
763
|
+
* the DID the `/api/tokens/agent` exchange MINTED for it, carried verbatim —
|
|
764
|
+
* never re-derived from the URL the worker happens to dial: re-deriving gives
|
|
765
|
+
* one logical agent two DIDs.
|
|
774
766
|
*/
|
|
775
767
|
|
|
776
768
|
/**
|
|
777
|
-
* Stall watchdog
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
* restart policy the deployment chose.
|
|
769
|
+
* Stall watchdog — the fail-fast line behind the inference timeout. There
|
|
770
|
+
* is no poll timer to heartbeat — the worker pulls when idle, and a parked
|
|
771
|
+
* idle worker is not a stall; the honest stall signal is *processing
|
|
772
|
+
* without activity*: an agent holding a claimed job whose `lastActivityAt`
|
|
773
|
+
* (claim / progress / finish) has stopped advancing is wedged — the adapter
|
|
774
|
+
* defers every wake-up while a job is held and pulls at settle, so a wedged
|
|
775
|
+
* agent never settles and never recovers on its own. Silent hang → loud
|
|
776
|
+
* crash → whatever restart policy the deployment chose.
|
|
786
777
|
*
|
|
787
778
|
* Thresholds are fixed by design (no env knobs) and deliberately
|
|
788
|
-
* layered: inference timeout (10 min
|
|
779
|
+
* layered: inference timeout (10 min) fires first; this watchdog
|
|
789
780
|
* (15 min) catches wedges where the loop still turns but activity has
|
|
790
781
|
* stopped; the gateway's dead-worker janitor (30 min) re-queues the job
|
|
791
782
|
* regardless.
|
|
792
783
|
*
|
|
793
784
|
* The layering matters because this watchdog has a hard limit: it is an
|
|
794
785
|
* IN-PROCESS timer, so it cannot fire while the event loop itself is
|
|
795
|
-
* blocked — the exact condition a blocked loop creates
|
|
796
|
-
* (
|
|
797
|
-
*
|
|
798
|
-
* wedged). The unbounded emit that caused that specific hang is now bounded
|
|
799
|
-
* at the transport (`EMIT_TIMEOUT_MS`), so the loop errors instead of
|
|
800
|
-
* blocking; but for any future blocked-loop bug the ONLY backstop is the
|
|
786
|
+
* blocked — the exact condition a blocked loop creates. An emit is bounded
|
|
787
|
+
* at the transport (`EMIT_TIMEOUT_MS`), so the loop errors on one instead of
|
|
788
|
+
* blocking; but for any other blocked-loop bug the ONLY backstop is the
|
|
801
789
|
* out-of-process one — the dispatcher's sweep, which fails a running job
|
|
802
|
-
* whose worker has gone silent (docs/protocol/JOBS.md). A liveness guarantee
|
|
803
|
-
* blocked loop defeats is not one; the sweep is the guarantee.
|
|
790
|
+
* whose worker has gone silent (docs/protocol/JOBS.md). A liveness guarantee
|
|
791
|
+
* a blocked loop defeats is not one; the sweep is the guarantee.
|
|
804
792
|
*/
|
|
805
793
|
declare const STALL_THRESHOLD_MS: number;
|
|
806
794
|
/**
|
|
807
795
|
* The broadcasts a worker CONSUMES — announcements nobody replies to, which
|
|
808
796
|
* therefore derive from no operation.
|
|
809
797
|
*
|
|
810
|
-
* One home, because the alternative
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
* logged.
|
|
798
|
+
* One home, because the alternative fails silently: as `addChannels` calls
|
|
799
|
+
* beside their consumers, the worker's complete set exists nowhere and a
|
|
800
|
+
* widening can be deleted without any list getting shorter — every worker
|
|
801
|
+
* then idles on a `job:queued` its transport never carries,
|
|
802
|
+
* `lastQueuedEventAt: null`, nothing thrown and nothing logged.
|
|
816
803
|
*
|
|
817
804
|
* An entry here must have a consumer and a consumer must have an entry; the
|
|
818
805
|
* census beside this file asserts both directions.
|