@ossy/platform 3.15.0 → 3.17.0

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 (34) hide show
  1. package/package.json +9 -8
  2. package/src/audit/audit-helpers.js +60 -6
  3. package/src/audit/audit-helpers.spec.js +42 -0
  4. package/src/audit/task-run-list.aggregate.js +3 -0
  5. package/src/audit/task-run-service.js +10 -2
  6. package/src/audit/task-run.aggregate.js +2 -0
  7. package/src/file.schema.js +0 -4
  8. package/src/metering/assert-llm-usage-budget.js +83 -0
  9. package/src/metering/assert-llm-usage-budget.spec.js +129 -0
  10. package/src/metering/get-workspace-llm-usage.action.js +5 -0
  11. package/src/metering/get-workspace-llm-usage.task.js +31 -0
  12. package/src/metering/get-workspace-llm-usage.task.spec.js +74 -0
  13. package/src/metering/get-workspace-task-metering.action.js +5 -0
  14. package/src/metering/get-workspace-task-metering.task.js +30 -0
  15. package/src/metering/get-workspace-task-metering.task.spec.js +43 -0
  16. package/src/metering/index.js +11 -0
  17. package/src/metering/llm-usage.aggregate.js +127 -0
  18. package/src/metering/llm-usage.aggregate.spec.js +95 -0
  19. package/src/metering/metering-service.js +13 -0
  20. package/src/metering/metering-service.spec.js +62 -0
  21. package/src/metering/task-metering.aggregate.js +108 -0
  22. package/src/metering/task-metering.aggregate.spec.js +71 -0
  23. package/src/metering/usage-month.js +7 -0
  24. package/src/runtime.js +2 -0
  25. package/src/server.js +2 -0
  26. package/src/ssr/detect-mobile-device.js +39 -0
  27. package/src/ssr/detect-mobile-device.spec.js +50 -0
  28. package/src/storage/storage-keys.js +24 -0
  29. package/src/storage/storage-keys.spec.js +12 -1
  30. package/src/tasks/in-process-sdk.js +1 -49
  31. package/src/tasks/in-process-sdk.spec.js +1 -39
  32. package/src/tasks/task-service.js +270 -41
  33. package/src/tasks/task-service.spec.js +108 -0
  34. package/src/text-completion.schema.js +37 -0
@@ -63,6 +63,73 @@ describe('TaskService triggers', () => {
63
63
  )).toBe(true)
64
64
  })
65
65
 
66
+ it('AND-narrows events with instance watch locations and MIME', () => {
67
+ const event = {
68
+ type: '@ossy/platform/schema/file',
69
+ event: 'Created',
70
+ resourceId: 'f1',
71
+ payload: {
72
+ belongsTo: 'ws1',
73
+ location: '/incoming/photos/a.jpg',
74
+ content: { ContentType: 'image/jpeg' },
75
+ },
76
+ }
77
+
78
+ expect(TaskService._matchesEventTrigger({
79
+ kind: 'on_event',
80
+ type: '@ossy/platform/schema/file',
81
+ event: 'Created',
82
+ location: { startsWith: '/incoming/' },
83
+ match: { 'content.ContentType': 'image/*' },
84
+ }, event)).toBe(true)
85
+
86
+ expect(TaskService._matchesEventTrigger({
87
+ kind: 'on_event',
88
+ type: '@ossy/platform/schema/file',
89
+ event: 'Created',
90
+ location: { startsWith: '/other/' },
91
+ }, event)).toBe(false)
92
+
93
+ expect(TaskService._matchesEventTrigger({
94
+ kind: 'on_event',
95
+ type: '@ossy/platform/schema/file',
96
+ event: 'Created',
97
+ match: { 'content.ContentType': 'video/*' },
98
+ }, event)).toBe(false)
99
+ })
100
+
101
+ it('treats configurable tasks without instance triggers as unarmed', async () => {
102
+ TaskService.setInstanceTriggersResolver(async () => [])
103
+ const result = await TaskService._eventTriggersForDispatch(
104
+ { id: 't1', configurable: true, triggers: [{ type: 'x', event: 'Created' }] },
105
+ 'ws1',
106
+ )
107
+ expect(result.armed).toBe(false)
108
+ expect(result.triggers).toEqual([])
109
+ TaskService.setInstanceTriggersResolver(null)
110
+ })
111
+
112
+ it('uses instance triggers for configurable tasks', async () => {
113
+ const instanceTriggers = [{
114
+ kind: 'on_event',
115
+ type: '@ossy/platform/schema/file',
116
+ event: 'Created',
117
+ location: { startsWith: '/incoming/' },
118
+ }]
119
+ TaskService.setInstanceTriggersResolver(async () => instanceTriggers)
120
+ const result = await TaskService._eventTriggersForDispatch(
121
+ {
122
+ id: 't1',
123
+ configurable: true,
124
+ triggers: [{ type: '@ossy/platform/schema/file', event: 'Created' }],
125
+ },
126
+ 'ws1',
127
+ )
128
+ expect(result.armed).toBe(true)
129
+ expect(result.triggers).toEqual(instanceTriggers)
130
+ TaskService.setInstanceTriggersResolver(null)
131
+ })
132
+
66
133
  it('skips primary derived task in on_action dispatch', () => {
67
134
  expect(isPrimaryTaskForAction('@ossy/booking/actions/create', '@ossy/booking/tasks/create')).toBe(true)
68
135
  expect(isPrimaryTaskForAction('@ossy/booking/actions/create', '@ossy/booking/tasks/send-reminder')).toBe(false)
@@ -120,6 +187,11 @@ describe('TaskRunList projection', () => {
120
187
  moduleId: 'media-tasks',
121
188
  trigger: 'on_event',
122
189
  belongsTo: 'ws1',
190
+ triggeredBy: {
191
+ resourceId: 'file1',
192
+ type: '@ossy/platform/schema/file',
193
+ event: 'Created',
194
+ },
123
195
  },
124
196
  }
125
197
 
@@ -127,6 +199,11 @@ describe('TaskRunList projection', () => {
127
199
  state = TaskRunListProjection.Apply(started, state)
128
200
  expect(state.runs).toHaveLength(1)
129
201
  expect(state.runs[0].status).toBe('in progress')
202
+ expect(state.runs[0].triggeredBy).toEqual({
203
+ resourceId: 'file1',
204
+ type: '@ossy/platform/schema/file',
205
+ event: 'Created',
206
+ })
130
207
 
131
208
  state = TaskRunListProjection.Apply({
132
209
  resourceId: 'run1',
@@ -145,6 +222,37 @@ describe('TaskRunList projection', () => {
145
222
  expect(state.runs[0].durationMs).toBe(1000)
146
223
  })
147
224
 
225
+ it('keeps resolved inputs on the listed run', () => {
226
+ let state = TaskRunListProjection.initialState('ws1')
227
+ state = TaskRunListProjection.Apply({
228
+ resourceId: 'run-inputs',
229
+ type: '@ossy/platform/schema/task-run',
230
+ event: 'Started',
231
+ created: 1000,
232
+ payload: {
233
+ taskId: '@ossy/media-tasks/tasks/visual-content-descriptors',
234
+ moduleId: 'media-tasks',
235
+ trigger: 'on_event',
236
+ belongsTo: 'ws1',
237
+ },
238
+ }, state)
239
+ state = TaskRunListProjection.Apply({
240
+ resourceId: 'run-inputs',
241
+ event: 'Completed',
242
+ created: 2000,
243
+ payload: {
244
+ durationMs: 40,
245
+ resultSummary: { title: 'A cat' },
246
+ inputs: { model: 'google/gemini-2.5-flash-lite' },
247
+ belongsTo: 'ws1',
248
+ taskId: '@ossy/media-tasks/tasks/visual-content-descriptors',
249
+ moduleId: 'media-tasks',
250
+ },
251
+ }, state)
252
+
253
+ expect(state.runs[0].inputs).toEqual({ model: 'google/gemini-2.5-flash-lite' })
254
+ })
255
+
148
256
  it('scopes Completed/Failed from payload.belongsTo (changestream dispatch)', () => {
149
257
  expect(TaskRunListProjection.scopeFromEvent({
150
258
  payload: { durationMs: 10 },
@@ -0,0 +1,37 @@
1
+ import { TASK_OUTPUT_PROVENANCE_FIELDS } from '@ossy/schema'
2
+
3
+ /**
4
+ * Platform schema for freeform text/Markdown LLM completion artifacts.
5
+ *
6
+ * Any task that produces unstructured prose (a chat completion, a
7
+ * generated summary, …) placed as a document should declare this schema
8
+ * as its output `schemaId` instead of inventing a feature-specific one.
9
+ * Structured JSON results (e.g. `{ title, tags, alt }`) still get their
10
+ * own feature schema — this one is only for the "freeform text/md" shape.
11
+ *
12
+ * See `docs/concepts/TaskOutput.md`.
13
+ */
14
+ export default {
15
+ id: '@ossy/platform/schema/text-completion',
16
+ name: 'Text completion',
17
+ categoryName: 'Platform',
18
+ icon: 'comment',
19
+ fields: [
20
+ {
21
+ name: 'text',
22
+ type: 'textarea',
23
+ description: 'Model completion (freeform text/Markdown)',
24
+ },
25
+ {
26
+ name: 'model',
27
+ type: 'text',
28
+ description: 'Model slug that produced the completion',
29
+ },
30
+ {
31
+ name: 'prompt',
32
+ type: 'textarea',
33
+ description: 'Prompt that was sent, before resource context',
34
+ },
35
+ ...TASK_OUTPUT_PROVENANCE_FIELDS,
36
+ ],
37
+ }