@dxos/functions 0.8.4-main.7ace549 → 0.8.4-main.937b3ca
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/dist/lib/browser/index.mjs +273 -74
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +273 -74
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/errors.d.ts +24 -32
- package/dist/types/src/errors.d.ts.map +1 -1
- package/dist/types/src/operation-compatibility.test.d.ts +2 -0
- package/dist/types/src/operation-compatibility.test.d.ts.map +1 -0
- package/dist/types/src/protocol/functions-ai-http-client.d.ts +12 -0
- package/dist/types/src/protocol/functions-ai-http-client.d.ts.map +1 -0
- package/dist/types/src/protocol/protocol.d.ts.map +1 -1
- package/dist/types/src/sdk.d.ts +18 -4
- package/dist/types/src/sdk.d.ts.map +1 -1
- package/dist/types/src/services/credentials.d.ts +6 -4
- package/dist/types/src/services/credentials.d.ts.map +1 -1
- package/dist/types/src/services/event-logger.d.ts +25 -31
- package/dist/types/src/services/event-logger.d.ts.map +1 -1
- package/dist/types/src/services/function-invocation-service.d.ts +5 -0
- package/dist/types/src/services/function-invocation-service.d.ts.map +1 -1
- package/dist/types/src/services/index.d.ts +0 -1
- package/dist/types/src/services/index.d.ts.map +1 -1
- package/dist/types/src/services/tracing.d.ts +37 -3
- package/dist/types/src/services/tracing.d.ts.map +1 -1
- package/dist/types/src/types/Function.d.ts +33 -44
- package/dist/types/src/types/Function.d.ts.map +1 -1
- package/dist/types/src/types/Script.d.ts +8 -15
- package/dist/types/src/types/Script.d.ts.map +1 -1
- package/dist/types/src/types/Trigger.d.ts +37 -57
- package/dist/types/src/types/Trigger.d.ts.map +1 -1
- package/dist/types/src/types/TriggerEvent.d.ts +33 -2
- package/dist/types/src/types/TriggerEvent.d.ts.map +1 -1
- package/dist/types/src/types/url.d.ts +4 -3
- package/dist/types/src/types/url.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -16
- package/src/errors.ts +4 -4
- package/src/operation-compatibility.test.ts +185 -0
- package/src/protocol/functions-ai-http-client.ts +67 -0
- package/src/protocol/protocol.ts +118 -12
- package/src/sdk.ts +55 -6
- package/src/services/credentials.ts +31 -15
- package/src/services/event-logger.ts +2 -2
- package/src/services/function-invocation-service.ts +14 -0
- package/src/services/index.ts +0 -2
- package/src/services/tracing.ts +63 -4
- package/src/types/Function.ts +12 -10
- package/src/types/Script.ts +3 -2
- package/src/types/Trigger.ts +9 -6
- package/src/types/TriggerEvent.ts +9 -3
- package/src/types/url.ts +4 -3
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/functions",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.937b3ca",
|
|
4
4
|
"description": "Functions API.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "info@dxos.org",
|
|
9
13
|
"sideEffects": false,
|
|
@@ -23,21 +27,23 @@
|
|
|
23
27
|
"src"
|
|
24
28
|
],
|
|
25
29
|
"dependencies": {
|
|
26
|
-
"@effect/
|
|
27
|
-
"effect": "
|
|
28
|
-
"
|
|
29
|
-
"@dxos/
|
|
30
|
-
"@dxos/
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/echo": "0.8.4-main.
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
30
|
+
"@effect/ai-anthropic": "0.22.0",
|
|
31
|
+
"@effect/platform": "0.93.6",
|
|
32
|
+
"effect": "3.19.11",
|
|
33
|
+
"@dxos/ai": "0.8.4-main.937b3ca",
|
|
34
|
+
"@dxos/echo-db": "0.8.4-main.937b3ca",
|
|
35
|
+
"@dxos/context": "0.8.4-main.937b3ca",
|
|
36
|
+
"@dxos/echo": "0.8.4-main.937b3ca",
|
|
37
|
+
"@dxos/effect": "0.8.4-main.937b3ca",
|
|
38
|
+
"@dxos/errors": "0.8.4-main.937b3ca",
|
|
39
|
+
"@dxos/invariant": "0.8.4-main.937b3ca",
|
|
40
|
+
"@dxos/keys": "0.8.4-main.937b3ca",
|
|
41
|
+
"@dxos/log": "0.8.4-main.937b3ca",
|
|
42
|
+
"@dxos/node-std": "0.8.4-main.937b3ca",
|
|
43
|
+
"@dxos/protocols": "0.8.4-main.937b3ca",
|
|
44
|
+
"@dxos/schema": "0.8.4-main.937b3ca",
|
|
45
|
+
"@dxos/operation": "0.8.4-main.937b3ca",
|
|
46
|
+
"@dxos/types": "0.8.4-main.937b3ca"
|
|
41
47
|
},
|
|
42
48
|
"publishConfig": {
|
|
43
49
|
"access": "public"
|
package/src/errors.ts
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
import { BaseError, type BaseErrorOptions } from '@dxos/errors';
|
|
6
6
|
|
|
7
|
-
export class ServiceNotAvailableError extends BaseError.extend('
|
|
7
|
+
export class ServiceNotAvailableError extends BaseError.extend('ServiceNotAvailable', 'Service not available') {
|
|
8
8
|
constructor(service: string, options?: Omit<BaseErrorOptions, 'context'>) {
|
|
9
9
|
super({ context: { service }, ...options });
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export class FunctionNotFoundError extends BaseError.extend('
|
|
13
|
+
export class FunctionNotFoundError extends BaseError.extend('FunctionNotFound', 'Function not found') {
|
|
14
14
|
constructor(functionKey: string, options?: Omit<BaseErrorOptions, 'context'>) {
|
|
15
15
|
super({ context: { function: functionKey }, ...options });
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export class FunctionError extends BaseError.extend('
|
|
19
|
+
export class FunctionError extends BaseError.extend('FunctionError', 'Function invocation error') {}
|
|
20
20
|
|
|
21
|
-
export class TriggerStateNotFoundError extends BaseError.extend('
|
|
21
|
+
export class TriggerStateNotFoundError extends BaseError.extend('TriggerStateNotFound', 'Trigger state not found') {}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import { describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { FunctionDefinition, defineFunction } from './sdk';
|
|
10
|
+
|
|
11
|
+
describe('Function/Operation Compatibility', () => {
|
|
12
|
+
test('can convert FunctionDefinition to OperationDefinition', () => {
|
|
13
|
+
const func = defineFunction({
|
|
14
|
+
key: 'test.function',
|
|
15
|
+
name: 'Test Function',
|
|
16
|
+
description: 'A test function',
|
|
17
|
+
inputSchema: Schema.Struct({ value: Schema.Number }),
|
|
18
|
+
outputSchema: Schema.Struct({ result: Schema.String }),
|
|
19
|
+
handler: async ({ data }) => {
|
|
20
|
+
return { result: data.value.toString() };
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const op = FunctionDefinition.toOperation(func);
|
|
25
|
+
|
|
26
|
+
expect(op.meta.key).toBe('test.function');
|
|
27
|
+
expect(op.meta.name).toBe('Test Function');
|
|
28
|
+
expect(op.meta.description).toBe('A test function');
|
|
29
|
+
expect(Schema.isSchema(op.schema.input)).toBe(true);
|
|
30
|
+
expect(Schema.isSchema(op.schema.output)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('converted operation has matching schemas', () => {
|
|
34
|
+
const inputSchema = Schema.Struct({
|
|
35
|
+
name: Schema.String,
|
|
36
|
+
age: Schema.Number,
|
|
37
|
+
});
|
|
38
|
+
const outputSchema = Schema.Struct({
|
|
39
|
+
greeting: Schema.String,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const func = defineFunction({
|
|
43
|
+
key: 'test.greet',
|
|
44
|
+
name: 'Greet',
|
|
45
|
+
inputSchema,
|
|
46
|
+
outputSchema,
|
|
47
|
+
handler: async ({ data }) => {
|
|
48
|
+
return { greeting: `Hello, ${data.name}!` };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const op = FunctionDefinition.toOperation(func);
|
|
53
|
+
|
|
54
|
+
// Verify schemas match
|
|
55
|
+
const testInput = { name: 'Alice', age: 30 };
|
|
56
|
+
const validatedInput = Schema.decodeSync(op.schema.input)(testInput);
|
|
57
|
+
expect(validatedInput).toEqual(testInput);
|
|
58
|
+
|
|
59
|
+
const testOutput = { greeting: 'Hello, Alice!' };
|
|
60
|
+
const validatedOutput = Schema.decodeSync(op.schema.output)(testOutput);
|
|
61
|
+
expect(validatedOutput).toEqual(testOutput);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('converted operation preserves metadata', () => {
|
|
65
|
+
const func = defineFunction({
|
|
66
|
+
key: 'test.meta',
|
|
67
|
+
name: 'Meta Test',
|
|
68
|
+
description: 'Tests metadata preservation',
|
|
69
|
+
inputSchema: Schema.Void,
|
|
70
|
+
outputSchema: Schema.Void,
|
|
71
|
+
handler: async () => {},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const op = FunctionDefinition.toOperation(func);
|
|
75
|
+
|
|
76
|
+
expect(op.meta.key).toBe('test.meta');
|
|
77
|
+
expect(op.meta.name).toBe('Meta Test');
|
|
78
|
+
expect(op.meta.description).toBe('Tests metadata preservation');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('converted operation is pipeable', () => {
|
|
82
|
+
const func = defineFunction({
|
|
83
|
+
key: 'test.pipe',
|
|
84
|
+
name: 'Pipe Test',
|
|
85
|
+
inputSchema: Schema.Void,
|
|
86
|
+
outputSchema: Schema.Void,
|
|
87
|
+
handler: async () => {},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const op = FunctionDefinition.toOperation(func);
|
|
91
|
+
|
|
92
|
+
expect(typeof op.pipe).toBe('function');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('handles functions without output schema', () => {
|
|
96
|
+
const func = defineFunction({
|
|
97
|
+
key: 'test.no-output',
|
|
98
|
+
name: 'No Output',
|
|
99
|
+
inputSchema: Schema.Struct({ value: Schema.Number }),
|
|
100
|
+
handler: async ({ data }) => {
|
|
101
|
+
return data.value * 2;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const op = FunctionDefinition.toOperation(func);
|
|
106
|
+
|
|
107
|
+
expect(op.meta.key).toBe('test.no-output');
|
|
108
|
+
expect(Schema.isSchema(op.schema.output)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('converted operation includes handler', () => {
|
|
112
|
+
const func = defineFunction({
|
|
113
|
+
key: 'test.handler',
|
|
114
|
+
name: 'Handler Test',
|
|
115
|
+
inputSchema: Schema.Struct({ value: Schema.Number }),
|
|
116
|
+
outputSchema: Schema.Struct({ doubled: Schema.Number }),
|
|
117
|
+
handler: async ({ data }) => {
|
|
118
|
+
return { doubled: data.value * 2 };
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const op = FunctionDefinition.toOperation(func);
|
|
123
|
+
|
|
124
|
+
expect(op.handler).toBeDefined();
|
|
125
|
+
expect(typeof op.handler).toBe('function');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('converted operation has handler function', () => {
|
|
129
|
+
const func = defineFunction({
|
|
130
|
+
key: 'test.execute',
|
|
131
|
+
name: 'Execute Test',
|
|
132
|
+
inputSchema: Schema.Struct({ value: Schema.Number }),
|
|
133
|
+
outputSchema: Schema.Struct({ result: Schema.Number }),
|
|
134
|
+
handler: async ({ data }) => {
|
|
135
|
+
return { result: data.value * 3 };
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const op = FunctionDefinition.toOperation(func);
|
|
140
|
+
|
|
141
|
+
// Handler is present and is a function that returns an Effect
|
|
142
|
+
expect(op.handler).toBeDefined();
|
|
143
|
+
const effect = op.handler({ value: 5 });
|
|
144
|
+
// Verify it returns an Effect-like object
|
|
145
|
+
expect(effect).toHaveProperty('pipe');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('converted operation works with Effect-based handlers', () => {
|
|
149
|
+
const func = defineFunction({
|
|
150
|
+
key: 'test.effect',
|
|
151
|
+
name: 'Effect Test',
|
|
152
|
+
inputSchema: Schema.Struct({ value: Schema.Number }),
|
|
153
|
+
outputSchema: Schema.Struct({ result: Schema.Number }),
|
|
154
|
+
handler: Effect.fn(function* ({ data }) {
|
|
155
|
+
return { result: data.value * 4 };
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const op = FunctionDefinition.toOperation(func);
|
|
160
|
+
|
|
161
|
+
// Handler is present and produces an Effect
|
|
162
|
+
expect(op.handler).toBeDefined();
|
|
163
|
+
const effect = op.handler({ value: 3 });
|
|
164
|
+
expect(effect).toHaveProperty('pipe');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('converted operation works with synchronous handlers', () => {
|
|
168
|
+
const func = defineFunction({
|
|
169
|
+
key: 'test.sync',
|
|
170
|
+
name: 'Sync Test',
|
|
171
|
+
inputSchema: Schema.Struct({ value: Schema.Number }),
|
|
172
|
+
outputSchema: Schema.Struct({ result: Schema.Number }),
|
|
173
|
+
handler: ({ data }) => {
|
|
174
|
+
return { result: data.value * 5 };
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const op = FunctionDefinition.toOperation(func);
|
|
179
|
+
|
|
180
|
+
// Handler is present and produces an Effect
|
|
181
|
+
expect(op.handler).toBeDefined();
|
|
182
|
+
const effect = op.handler({ value: 2 });
|
|
183
|
+
expect(effect).toHaveProperty('pipe');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Headers from '@effect/platform/Headers';
|
|
6
|
+
import * as HttpClient from '@effect/platform/HttpClient';
|
|
7
|
+
import * as HttpClientError from '@effect/platform/HttpClientError';
|
|
8
|
+
import * as HttpClientResponse from '@effect/platform/HttpClientResponse';
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as FiberRef from 'effect/FiberRef';
|
|
11
|
+
import * as Layer from 'effect/Layer';
|
|
12
|
+
import * as Stream from 'effect/Stream';
|
|
13
|
+
|
|
14
|
+
import { log } from '@dxos/log';
|
|
15
|
+
import { type EdgeFunctionEnv, ErrorCodec } from '@dxos/protocols';
|
|
16
|
+
/**
|
|
17
|
+
* Copy pasted from https://github.com/Effect-TS/effect/blob/main/packages/platform/src/internal/fetchHttpClient.ts
|
|
18
|
+
*/
|
|
19
|
+
export const requestInitTagKey = '@effect/platform/FetchHttpClient/FetchOptions';
|
|
20
|
+
|
|
21
|
+
export class FunctionsAiHttpClient {
|
|
22
|
+
static make = (service: EdgeFunctionEnv.FunctionsAiService) =>
|
|
23
|
+
HttpClient.make((request, url, signal, fiber) => {
|
|
24
|
+
const context = fiber.getFiberRef(FiberRef.currentContext);
|
|
25
|
+
const options: RequestInit = context.unsafeMap.get(requestInitTagKey) ?? {};
|
|
26
|
+
const headers = options.headers
|
|
27
|
+
? Headers.merge(Headers.fromInput(options.headers), request.headers)
|
|
28
|
+
: request.headers;
|
|
29
|
+
|
|
30
|
+
const send = (body: BodyInit | undefined) =>
|
|
31
|
+
Effect.tryPromise({
|
|
32
|
+
try: () =>
|
|
33
|
+
service.fetch(
|
|
34
|
+
new Request(url, {
|
|
35
|
+
...options,
|
|
36
|
+
method: request.method,
|
|
37
|
+
headers,
|
|
38
|
+
body,
|
|
39
|
+
// Note: Don't pass signal - it can't be serialized through RPC
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
catch: (cause) => {
|
|
43
|
+
log.error('Failed to fetch', { errorSerialized: ErrorCodec.encode(cause as Error) });
|
|
44
|
+
return new HttpClientError.RequestError({
|
|
45
|
+
request,
|
|
46
|
+
reason: 'Transport',
|
|
47
|
+
cause,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
}).pipe(Effect.map((response) => HttpClientResponse.fromWeb(request, response)));
|
|
51
|
+
|
|
52
|
+
switch (request.body._tag) {
|
|
53
|
+
case 'Raw':
|
|
54
|
+
case 'Uint8Array':
|
|
55
|
+
return send(request.body.body as any);
|
|
56
|
+
case 'FormData':
|
|
57
|
+
return send(request.body.formData);
|
|
58
|
+
case 'Stream':
|
|
59
|
+
return Stream.toReadableStreamEffect(request.body.stream).pipe(Effect.flatMap(send));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return send(undefined);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
static layer = (service: EdgeFunctionEnv.FunctionsAiService) =>
|
|
66
|
+
Layer.succeed(HttpClient.HttpClient, FunctionsAiHttpClient.make(service));
|
|
67
|
+
}
|
package/src/protocol/protocol.ts
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as AnthropicClient from '@effect/ai-anthropic/AnthropicClient';
|
|
5
6
|
import * as Effect from 'effect/Effect';
|
|
6
7
|
import * as Layer from 'effect/Layer';
|
|
7
8
|
import * as Schema from 'effect/Schema';
|
|
8
9
|
import * as SchemaAST from 'effect/SchemaAST';
|
|
9
10
|
|
|
10
|
-
import { AiService } from '@dxos/ai';
|
|
11
|
+
import { AiModelResolver, AiService } from '@dxos/ai';
|
|
12
|
+
import { AnthropicResolver } from '@dxos/ai/resolvers';
|
|
11
13
|
import { LifecycleState, Resource } from '@dxos/context';
|
|
12
|
-
import { Type } from '@dxos/echo';
|
|
14
|
+
import { Database, Ref, Type } from '@dxos/echo';
|
|
15
|
+
import { refFromEncodedReference } from '@dxos/echo/internal';
|
|
13
16
|
import { EchoClient, type EchoDatabaseImpl, type QueueFactory } from '@dxos/echo-db';
|
|
17
|
+
import { runAndForwardErrors } from '@dxos/effect';
|
|
14
18
|
import { assertState, failedInvariant, invariant } from '@dxos/invariant';
|
|
15
19
|
import { PublicKey } from '@dxos/keys';
|
|
16
20
|
import { type FunctionProtocol } from '@dxos/protocols';
|
|
17
21
|
|
|
18
22
|
import { FunctionError } from '../errors';
|
|
19
23
|
import { FunctionDefinition, type FunctionServices } from '../sdk';
|
|
20
|
-
import { CredentialsService,
|
|
21
|
-
|
|
24
|
+
import { CredentialsService, FunctionInvocationService, QueueService, TracingService } from '../services';
|
|
25
|
+
|
|
26
|
+
import { FunctionsAiHttpClient } from './functions-ai-http-client';
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Wraps a function handler made with `defineFunction` to a protocol that the functions-runtime expects.
|
|
@@ -39,7 +44,7 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
|
|
|
39
44
|
},
|
|
40
45
|
handler: async ({ data, context }) => {
|
|
41
46
|
if (
|
|
42
|
-
(func.services.includes(
|
|
47
|
+
(func.services.includes(Database.Service.key) || func.services.includes(QueueService.key)) &&
|
|
43
48
|
(!context.services.dataService || !context.services.queryService)
|
|
44
49
|
) {
|
|
45
50
|
throw new FunctionError({
|
|
@@ -61,17 +66,22 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
|
|
|
61
66
|
|
|
62
67
|
if (func.types.length > 0) {
|
|
63
68
|
invariant(funcContext.db, 'Database is required for functions with types');
|
|
64
|
-
funcContext.db.graph.schemaRegistry.
|
|
69
|
+
await funcContext.db.graph.schemaRegistry.register(func.types as Type.Entity.Any[]);
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
const dataWithDecodedRefs =
|
|
73
|
+
funcContext.db && !SchemaAST.isAnyKeyword(func.inputSchema.ast)
|
|
74
|
+
? decodeRefsFromSchema(func.inputSchema.ast, data, funcContext.db)
|
|
75
|
+
: data;
|
|
76
|
+
|
|
67
77
|
let result = await func.handler({
|
|
68
78
|
// TODO(dmaretskyi): Fix the types.
|
|
69
79
|
context: context as any,
|
|
70
|
-
data,
|
|
80
|
+
data: dataWithDecodedRefs,
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
if (Effect.isEffect(result)) {
|
|
74
|
-
result = await
|
|
84
|
+
result = await runAndForwardErrors(
|
|
75
85
|
(result as Effect.Effect<unknown, unknown, FunctionServices>).pipe(
|
|
76
86
|
Effect.orDie,
|
|
77
87
|
Effect.provide(funcContext.createLayer()),
|
|
@@ -121,6 +131,7 @@ class FunctionContext extends Resource {
|
|
|
121
131
|
spaceId: this.context.spaceId ?? failedInvariant(),
|
|
122
132
|
spaceKey: PublicKey.fromHex(this.context.spaceKey ?? failedInvariant('spaceKey missing in context')),
|
|
123
133
|
reactiveSchemaQuery: false,
|
|
134
|
+
preloadSchemaOnOpen: false,
|
|
124
135
|
})
|
|
125
136
|
: undefined;
|
|
126
137
|
|
|
@@ -138,19 +149,114 @@ class FunctionContext extends Resource {
|
|
|
138
149
|
createLayer(): Layer.Layer<FunctionServices> {
|
|
139
150
|
assertState(this._lifecycleState === LifecycleState.OPEN, 'FunctionContext is not open');
|
|
140
151
|
|
|
141
|
-
const dbLayer = this.db ?
|
|
152
|
+
const dbLayer = this.db ? Database.Service.layer(this.db) : Database.Service.notAvailable;
|
|
142
153
|
const queuesLayer = this.queues ? QueueService.layer(this.queues) : QueueService.notAvailable;
|
|
143
154
|
const credentials = dbLayer
|
|
144
|
-
? CredentialsService.layerFromDatabase().pipe(Layer.provide(dbLayer))
|
|
155
|
+
? CredentialsService.layerFromDatabase({ caching: true }).pipe(Layer.provide(dbLayer))
|
|
145
156
|
: CredentialsService.configuredLayer([]);
|
|
146
157
|
const functionInvocationService = MockedFunctionInvocationService;
|
|
147
|
-
const aiService = AiService.notAvailable;
|
|
148
158
|
const tracing = TracingService.layerNoop;
|
|
149
159
|
|
|
150
|
-
|
|
160
|
+
const aiLayer = this.context.services.functionsAiService
|
|
161
|
+
? AiModelResolver.AiModelResolver.buildAiService.pipe(
|
|
162
|
+
Layer.provide(
|
|
163
|
+
AnthropicResolver.make().pipe(
|
|
164
|
+
Layer.provide(
|
|
165
|
+
AnthropicClient.layer({
|
|
166
|
+
// Note: It doesn't matter what is base url here, it will be proxied to ai gateway in edge.
|
|
167
|
+
apiUrl: 'http://internal/provider/anthropic',
|
|
168
|
+
}).pipe(Layer.provide(FunctionsAiHttpClient.layer(this.context.services.functionsAiService))),
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
: AiService.notAvailable;
|
|
174
|
+
|
|
175
|
+
return Layer.mergeAll(
|
|
176
|
+
dbLayer, //
|
|
177
|
+
queuesLayer,
|
|
178
|
+
credentials,
|
|
179
|
+
functionInvocationService,
|
|
180
|
+
aiLayer,
|
|
181
|
+
tracing,
|
|
182
|
+
);
|
|
151
183
|
}
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
const MockedFunctionInvocationService = Layer.succeed(FunctionInvocationService, {
|
|
155
187
|
invokeFunction: () => Effect.die('Calling functions from functions is not implemented yet.'),
|
|
188
|
+
resolveFunction: () => Effect.die('Not implemented.'),
|
|
156
189
|
});
|
|
190
|
+
|
|
191
|
+
const decodeRefsFromSchema = (ast: SchemaAST.AST, value: unknown, db: EchoDatabaseImpl): unknown => {
|
|
192
|
+
if (value == null) {
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const encoded = SchemaAST.encodedBoundAST(ast);
|
|
197
|
+
if (Ref.isRefType(encoded)) {
|
|
198
|
+
if (Ref.isRef(value)) {
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (typeof value === 'object' && value !== null && typeof (value as any)['/'] === 'string') {
|
|
203
|
+
const resolver = db.graph.createRefResolver({ context: { space: db.spaceId } });
|
|
204
|
+
return refFromEncodedReference(value as any, resolver);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
switch (encoded._tag) {
|
|
211
|
+
case 'TypeLiteral': {
|
|
212
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
const result: Record<string, unknown> = { ...(value as any) };
|
|
216
|
+
for (const prop of SchemaAST.getPropertySignatures(encoded)) {
|
|
217
|
+
const key = prop.name.toString();
|
|
218
|
+
if (key in result) {
|
|
219
|
+
result[key] = decodeRefsFromSchema(prop.type, (result as any)[key], db);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'TupleType': {
|
|
226
|
+
if (!Array.isArray(value)) {
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// For arrays, effect uses TupleType with empty elements and a single rest element.
|
|
231
|
+
if (encoded.elements.length === 0 && encoded.rest.length === 1) {
|
|
232
|
+
const elementType = encoded.rest[0].type;
|
|
233
|
+
return (value as unknown[]).map((item) => decodeRefsFromSchema(elementType, item, db));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'Union': {
|
|
240
|
+
// Optional values are represented as union with undefined.
|
|
241
|
+
const nonUndefined = encoded.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
|
|
242
|
+
if (nonUndefined.length === 1) {
|
|
243
|
+
return decodeRefsFromSchema(nonUndefined[0], value, db);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// For other unions we can't safely pick a branch without validating.
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case 'Suspend': {
|
|
251
|
+
return decodeRefsFromSchema(encoded.f(), value, db);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
case 'Refinement': {
|
|
255
|
+
return decodeRefsFromSchema(encoded.from, value, db);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
default: {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
package/src/sdk.ts
CHANGED
|
@@ -8,8 +8,9 @@ import * as Schema from 'effect/Schema';
|
|
|
8
8
|
|
|
9
9
|
import { type AiService } from '@dxos/ai';
|
|
10
10
|
import { Obj, Type } from '@dxos/echo';
|
|
11
|
-
import { type
|
|
11
|
+
import { type Database } from '@dxos/echo';
|
|
12
12
|
import { assertArgument, failedInvariant } from '@dxos/invariant';
|
|
13
|
+
import { Operation } from '@dxos/operation';
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
type CredentialsService,
|
|
@@ -37,7 +38,7 @@ export type FunctionServices =
|
|
|
37
38
|
| InvocationServices
|
|
38
39
|
| AiService.AiService
|
|
39
40
|
| CredentialsService
|
|
40
|
-
|
|
|
41
|
+
| Database.Service
|
|
41
42
|
| QueueService
|
|
42
43
|
| FunctionInvocationService;
|
|
43
44
|
|
|
@@ -79,7 +80,7 @@ export type FunctionDefinition<T = any, O = any, S extends FunctionServices = Fu
|
|
|
79
80
|
* List of types the function uses.
|
|
80
81
|
* This is used to ensure that the types are available when the function is executed.
|
|
81
82
|
*/
|
|
82
|
-
types: readonly Type.
|
|
83
|
+
types: readonly Type.Entity.Any[];
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Keys of the required services.
|
|
@@ -119,7 +120,7 @@ export type FunctionProps<T, O> = {
|
|
|
119
120
|
* List of types the function uses.
|
|
120
121
|
* This is used to ensure that the types are available when the function is executed.
|
|
121
122
|
*/
|
|
122
|
-
types?: readonly Type.
|
|
123
|
+
types?: readonly Type.Entity.Any[];
|
|
123
124
|
// TODO(dmaretskyi): This currently doesn't cause a compile-time error if the handler requests a service that is not specified
|
|
124
125
|
services?: readonly Context.Tag<any, any>[];
|
|
125
126
|
|
|
@@ -184,11 +185,58 @@ const getServiceKeys = (services: readonly Context.Tag<any, any>[]) => {
|
|
|
184
185
|
if (typeof tag.key === 'string') {
|
|
185
186
|
return tag.key;
|
|
186
187
|
}
|
|
187
|
-
console.log(tag);
|
|
188
188
|
failedInvariant();
|
|
189
189
|
});
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Converts a FunctionDefinition to an OperationDefinition with handler.
|
|
194
|
+
* The function handler is adapted to the OperationHandler format.
|
|
195
|
+
*
|
|
196
|
+
* Note: FunctionDefinition stores service keys as strings, not Tag types,
|
|
197
|
+
* so we can't use Operation.withHandler's type inference here.
|
|
198
|
+
*/
|
|
199
|
+
export const toOperation = <T, O, S extends FunctionServices = FunctionServices>(
|
|
200
|
+
functionDef: FunctionDefinition<T, O, S>,
|
|
201
|
+
): Operation.Definition<T, O> & { handler: Operation.Handler<T, O, any, S> } => {
|
|
202
|
+
const op = Operation.make({
|
|
203
|
+
schema: {
|
|
204
|
+
input: functionDef.inputSchema,
|
|
205
|
+
output: functionDef.outputSchema ?? Schema.Any,
|
|
206
|
+
},
|
|
207
|
+
meta: {
|
|
208
|
+
key: functionDef.key,
|
|
209
|
+
name: functionDef.name,
|
|
210
|
+
description: functionDef.description,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Adapt FunctionHandler signature to OperationHandler format.
|
|
215
|
+
// FunctionHandler expects { context, data }, OperationHandler expects just input.
|
|
216
|
+
const operationHandler: Operation.Handler<T, O, any, S> = (input: T) => {
|
|
217
|
+
const result = functionDef.handler({
|
|
218
|
+
context: {} as FunctionContext,
|
|
219
|
+
data: input,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Convert Promise or plain value to Effect.
|
|
223
|
+
if (Effect.isEffect(result)) {
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
if (result instanceof Promise) {
|
|
227
|
+
return Effect.tryPromise(() => result);
|
|
228
|
+
}
|
|
229
|
+
return Effect.succeed(result as O);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Manually attach handler since FunctionDefinition stores service keys as strings,
|
|
233
|
+
// not Tag types, so withHandler's type inference doesn't apply.
|
|
234
|
+
return {
|
|
235
|
+
...op,
|
|
236
|
+
handler: operationHandler,
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
192
240
|
export const FunctionDefinition = {
|
|
193
241
|
make: defineFunction,
|
|
194
242
|
isFunction: (value: unknown): value is FunctionDefinition.Any => {
|
|
@@ -202,6 +250,7 @@ export const FunctionDefinition = {
|
|
|
202
250
|
assertArgument(Obj.instanceOf(Function.Function, functionObj), 'functionObj');
|
|
203
251
|
return deserializeFunction(functionObj);
|
|
204
252
|
},
|
|
253
|
+
toOperation,
|
|
205
254
|
};
|
|
206
255
|
|
|
207
256
|
export const serializeFunction = (functionDef: FunctionDefinition.Any): Function.Function => {
|
|
@@ -215,7 +264,7 @@ export const serializeFunction = (functionDef: FunctionDefinition.Any): Function
|
|
|
215
264
|
services: functionDef.services,
|
|
216
265
|
});
|
|
217
266
|
if (functionDef.meta?.deployedFunctionId) {
|
|
218
|
-
setUserFunctionIdInMetadata(Obj.getMeta(fn), functionDef.meta
|
|
267
|
+
Obj.change(fn, (fn) => setUserFunctionIdInMetadata(Obj.getMeta(fn), functionDef.meta!.deployedFunctionId!));
|
|
219
268
|
}
|
|
220
269
|
return fn;
|
|
221
270
|
};
|