@modelprofile.com/flexharness 10.0.0 → 10.2.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 (48) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.d.ts +13 -1
  3. package/dist_ts/classes.flexharness.js +302 -188
  4. package/dist_ts/classes.stores.js +11 -119
  5. package/dist_ts/errors.d.ts +7 -2
  6. package/dist_ts/errors.js +1 -1
  7. package/dist_ts/interfaces.d.ts +18 -0
  8. package/dist_ts/interfaces.js +1 -1
  9. package/dist_ts/utils.storerules.d.ts +28 -0
  10. package/dist_ts/utils.storerules.js +107 -0
  11. package/dist_ts_chat_harness/classes.transcript.d.ts +103 -0
  12. package/dist_ts_chat_harness/classes.transcript.js +296 -0
  13. package/dist_ts_chat_harness/functions.intents.d.ts +62 -0
  14. package/dist_ts_chat_harness/functions.intents.js +96 -0
  15. package/dist_ts_chat_harness/functions.mapping.d.ts +33 -0
  16. package/dist_ts_chat_harness/functions.mapping.js +220 -0
  17. package/dist_ts_chat_harness/index.d.ts +3 -0
  18. package/dist_ts_chat_harness/index.js +4 -0
  19. package/dist_ts_chat_harness/plugins.d.ts +1 -0
  20. package/dist_ts_chat_harness/plugins.js +2 -0
  21. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +31 -0
  22. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +312 -0
  23. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +110 -0
  24. package/dist_ts_stores_nosqldb/classes.recordstore.js +437 -0
  25. package/dist_ts_stores_nosqldb/index.d.ts +2 -0
  26. package/dist_ts_stores_nosqldb/index.js +3 -0
  27. package/dist_ts_stores_nosqldb/plugins.d.ts +7 -0
  28. package/dist_ts_stores_nosqldb/plugins.js +10 -0
  29. package/package.json +24 -4
  30. package/readme.md +6 -0
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/classes.flexharness.ts +324 -205
  33. package/ts/classes.stores.ts +28 -172
  34. package/ts/errors.ts +8 -2
  35. package/ts/interfaces.ts +19 -0
  36. package/ts/readme.md +6 -0
  37. package/ts/utils.storerules.ts +177 -0
  38. package/ts_chat_harness/classes.transcript.ts +399 -0
  39. package/ts_chat_harness/functions.intents.ts +169 -0
  40. package/ts_chat_harness/functions.mapping.ts +267 -0
  41. package/ts_chat_harness/index.ts +33 -0
  42. package/ts_chat_harness/plugins.ts +18 -0
  43. package/ts_chat_harness/readme.md +117 -0
  44. package/ts_stores_nosqldb/classes.nosqldbstores.ts +465 -0
  45. package/ts_stores_nosqldb/classes.recordstore.ts +560 -0
  46. package/ts_stores_nosqldb/index.ts +9 -0
  47. package/ts_stores_nosqldb/plugins.ts +38 -0
  48. package/ts_stores_nosqldb/readme.md +53 -0
@@ -0,0 +1,267 @@
1
+ import type * as plugins from './plugins.js';
2
+ import type {
3
+ IFlexAttachmentMessagePart,
4
+ IFlexMessage,
5
+ IFlexModelIdentity,
6
+ IFlexPermissionRequest,
7
+ IFlexProjectStateResult,
8
+ IFlexSession,
9
+ IFlexToolMessagePart,
10
+ IFlexUsage,
11
+ TFlexToolPartStatus,
12
+ } from '../ts/interfaces.js';
13
+
14
+ /** When a streamed part started and ended, as the events that carried it stated. */
15
+ export interface IFlexPartTiming {
16
+ startedAt?: number;
17
+ endedAt?: number;
18
+ }
19
+
20
+ export type TFlexPartTimings = ReadonlyMap<string, IFlexPartTiming>;
21
+
22
+ /** The transcript row of one part of an assistant message. */
23
+ export const flexPartRowId = (messageId: string, partId: string): string => `${messageId}:${partId}`;
24
+
25
+ export const flexPartTimingKey = flexPartRowId;
26
+
27
+ const epoch = (timestamp: string | undefined): number | undefined => {
28
+ if (timestamp === undefined) return undefined;
29
+ const value = Date.parse(timestamp);
30
+ return Number.isFinite(value) ? value : undefined;
31
+ };
32
+
33
+ export const flexModelLabel = (model: IFlexModelIdentity): string => `${model.provider}/${model.model}`;
34
+
35
+ export const flexUsageToHarness = (usage: IFlexUsage): plugins.IHarnessUsage => ({
36
+ inputTokens: usage.inputTokens,
37
+ outputTokens: usage.outputTokens,
38
+ totalTokens: usage.totalTokens,
39
+ cacheReadTokens: usage.cacheReadTokens,
40
+ cacheWriteTokens: usage.cacheWriteTokens,
41
+ });
42
+
43
+ const toolStatuses: Record<TFlexToolPartStatus, plugins.THarnessToolStatus> = {
44
+ running: 'running',
45
+ completed: 'completed',
46
+ failed: 'error',
47
+ cancelled: 'stopped',
48
+ };
49
+
50
+ export function flexToolPartToHarness(
51
+ part: IFlexToolMessagePart,
52
+ timing?: IFlexPartTiming,
53
+ ): plugins.IHarnessToolCall {
54
+ return {
55
+ id: part.toolCallId,
56
+ name: part.toolName,
57
+ status: toolStatuses[part.status],
58
+ input: part.input,
59
+ ...(part.output === undefined ? {} : { output: part.output }),
60
+ ...(part.status === 'failed' ? { isError: true } : {}),
61
+ ...(part.error === undefined ? {} : { errorText: part.error }),
62
+ ...(part.childSessionId === undefined ? {} : { childSessionId: part.childSessionId }),
63
+ ...(part.model === undefined ? {} : { model: flexModelLabel(part.model) }),
64
+ ...(timing?.startedAt === undefined ? {} : { startedAt: timing.startedAt }),
65
+ ...(timing?.endedAt === undefined ? {} : { finishedAt: timing.endedAt }),
66
+ };
67
+ }
68
+
69
+ /**
70
+ * The public projection keeps no attachment payload. An attachment the components can show needs
71
+ * its name, media type and size; one whose source did not state all three, such as a remote URL
72
+ * of unknown size, has no row.
73
+ */
74
+ export function flexAttachmentToHarness(
75
+ part: IFlexAttachmentMessagePart,
76
+ ): plugins.IHarnessAttachment | undefined {
77
+ if (part.name === undefined || part.mediaType === undefined || part.sizeBytes === undefined) {
78
+ return undefined;
79
+ }
80
+ return {
81
+ id: part.partId,
82
+ name: part.name,
83
+ mediaType: part.mediaType,
84
+ size: part.sizeBytes,
85
+ kind: part.attachmentType === 'image' ? 'image' : 'binary',
86
+ };
87
+ }
88
+
89
+ function attachmentsOf(message: IFlexMessage): plugins.IHarnessAttachment[] {
90
+ return message.parts.flatMap((part) => {
91
+ if (part.type !== 'attachment') return [];
92
+ const attachment = flexAttachmentToHarness(part);
93
+ return attachment ? [attachment] : [];
94
+ });
95
+ }
96
+
97
+ /**
98
+ * The transcript rows of one message. A user message is one row. An assistant message has one
99
+ * row per part, in part order: a text row, a reasoning row, or a tool row, so every streamed
100
+ * delta addresses exactly one row. The run's usage and error attach to its last text or reasoning
101
+ * row, or to a row of their own when the message has none.
102
+ */
103
+ export function flexMessageToHarnessRows(
104
+ message: IFlexMessage,
105
+ messageIndex: number,
106
+ timings: TFlexPartTimings = new Map(),
107
+ ): plugins.IHarnessMessage[] {
108
+ const createdAt = epoch(message.createdAt) ?? 0;
109
+ const completedAt = epoch(message.completedAt);
110
+ const identity = {
111
+ ...(message.model === undefined ? {} : { model: flexModelLabel(message.model) }),
112
+ ...(message.model?.variant === undefined ? {} : { effort: message.model.variant }),
113
+ };
114
+ if (message.role === 'user') {
115
+ const attachments = attachmentsOf(message);
116
+ return [{
117
+ id: message.messageId,
118
+ role: 'user',
119
+ text: message.parts.flatMap((part) => part.type === 'text' ? [part.text] : []).join('\n\n'),
120
+ createdAt,
121
+ order: { messageIndex, partIndex: 0 },
122
+ ...(attachments.length === 0 ? {} : { attachments }),
123
+ }];
124
+ }
125
+
126
+ const streaming = message.status === 'streaming';
127
+ const rows: plugins.IHarnessMessage[] = [];
128
+ let terminalRow: plugins.IHarnessMessage | undefined;
129
+ message.parts.forEach((part, partIndex) => {
130
+ const timing = timings.get(flexPartTimingKey(message.messageId, part.partId));
131
+ const base = {
132
+ id: flexPartRowId(message.messageId, part.partId),
133
+ createdAt: timing?.startedAt ?? createdAt,
134
+ order: { messageIndex, partIndex },
135
+ ...identity,
136
+ };
137
+ switch (part.type) {
138
+ case 'text': {
139
+ const row: plugins.IHarnessMessage = {
140
+ ...base,
141
+ role: 'assistant',
142
+ text: part.text,
143
+ ...(streaming && partIndex === message.parts.length - 1 ? { streaming: true } : {}),
144
+ };
145
+ rows.push(row);
146
+ terminalRow = row;
147
+ break;
148
+ }
149
+ case 'reasoning': {
150
+ const startedAt = timing?.startedAt ?? createdAt;
151
+ const endedAt = part.status === 'running'
152
+ ? undefined
153
+ : timing?.endedAt ?? completedAt ?? startedAt;
154
+ const row: plugins.IHarnessMessage = {
155
+ ...base,
156
+ role: 'assistant',
157
+ text: '',
158
+ reasoning: [{
159
+ id: part.partId,
160
+ text: part.text,
161
+ startedAt,
162
+ ...(endedAt === undefined ? {} : { endedAt }),
163
+ }],
164
+ };
165
+ rows.push(row);
166
+ terminalRow = row;
167
+ break;
168
+ }
169
+ case 'tool':
170
+ rows.push({ ...base, role: 'tool', text: '', toolCall: flexToolPartToHarness(part, timing) });
171
+ break;
172
+ case 'attachment': {
173
+ const attachment = flexAttachmentToHarness(part);
174
+ if (attachment) rows.push({ ...base, role: 'assistant', text: '', attachments: [attachment] });
175
+ break;
176
+ }
177
+ }
178
+ });
179
+
180
+ const terminal = {
181
+ ...(message.usage === undefined ? {} : { usage: flexUsageToHarness(message.usage) }),
182
+ ...(message.error === undefined ? {} : { error: message.error }),
183
+ };
184
+ if (Object.keys(terminal).length > 0) {
185
+ if (terminalRow) {
186
+ Object.assign(terminalRow, terminal);
187
+ } else {
188
+ rows.push({
189
+ id: message.messageId,
190
+ role: 'assistant',
191
+ text: '',
192
+ createdAt: completedAt ?? createdAt,
193
+ order: { messageIndex, partIndex: message.parts.length },
194
+ ...identity,
195
+ ...terminal,
196
+ });
197
+ }
198
+ }
199
+ return rows;
200
+ }
201
+
202
+ export function flexPermissionToHarness(
203
+ request: IFlexPermissionRequest,
204
+ ): plugins.IHarnessPermissionRequest {
205
+ const metadata = request.metadata;
206
+ return {
207
+ id: request.permissionId,
208
+ sessionId: request.sessionId,
209
+ ...(epoch(request.createdAt) === undefined ? {} : { createdAt: epoch(request.createdAt) }),
210
+ title: request.description,
211
+ type: request.kind,
212
+ ...(metadata !== null && typeof metadata === 'object' && !Array.isArray(metadata)
213
+ ? { metadata: { ...metadata } }
214
+ : {}),
215
+ };
216
+ }
217
+
218
+ export function flexSessionToHarnessStatus(session: IFlexSession): plugins.IHarnessStatus {
219
+ switch (session.activity.status) {
220
+ case 'running':
221
+ case 'scheduled':
222
+ case 'waiting_permission':
223
+ return { type: 'busy' };
224
+ case 'failed':
225
+ return {
226
+ type: 'error',
227
+ ...(session.activity.error === undefined ? {} : { message: session.activity.error }),
228
+ };
229
+ default:
230
+ return { type: 'idle' };
231
+ }
232
+ }
233
+
234
+ const sessionCardStates: Record<IFlexSession['activity']['status'], plugins.THarnessSessionCardState> = {
235
+ idle: 'normal',
236
+ scheduled: 'working',
237
+ running: 'working',
238
+ waiting_permission: 'attention',
239
+ failed: 'error',
240
+ cancelled: 'normal',
241
+ };
242
+
243
+ /** A session-list row. `title` falls back to the session id, which is what the session is named by. */
244
+ export function flexSessionToHarnessMeta(session: IFlexSession): plugins.IHarnessSessionMeta {
245
+ return {
246
+ id: session.sessionId,
247
+ title: session.title ?? session.sessionId,
248
+ ...(epoch(session.createdAt) === undefined ? {} : { createdAt: epoch(session.createdAt) }),
249
+ ...(epoch(session.updatedAt) === undefined ? {} : { updatedAt: epoch(session.updatedAt) }),
250
+ state: sessionCardStates[session.activity.status],
251
+ };
252
+ }
253
+
254
+ export function flexProjectToHarnessTodos(project: IFlexProjectStateResult): plugins.IHarnessTodoItem[] {
255
+ return project.state.tasks.map((task) => ({
256
+ id: task.id,
257
+ content: task.content,
258
+ status: task.status,
259
+ }));
260
+ }
261
+
262
+ export function flexProjectToHarnessScratchpad(
263
+ sessionId: string,
264
+ project: IFlexProjectStateResult,
265
+ ): plugins.IHarnessScratchpad {
266
+ return { id: sessionId, text: project.state.scratchpad, revision: project.revision };
267
+ }
@@ -0,0 +1,33 @@
1
+ export {
2
+ FlexHarnessTranscript,
3
+ type IFlexHarnessEventDelivery,
4
+ type IFlexHarnessTranscriptChange,
5
+ type IFlexHarnessTranscriptOptions,
6
+ type IFlexHarnessTranscriptSnapshot,
7
+ type IFlexHarnessTranscriptView,
8
+ } from './classes.transcript.js';
9
+ export {
10
+ flexAttachmentToHarness,
11
+ flexMessageToHarnessRows,
12
+ flexModelLabel,
13
+ flexPartRowId,
14
+ flexPermissionToHarness,
15
+ flexProjectToHarnessScratchpad,
16
+ flexProjectToHarnessTodos,
17
+ flexSessionToHarnessMeta,
18
+ flexSessionToHarnessStatus,
19
+ flexToolPartToHarness,
20
+ flexUsageToHarness,
21
+ type IFlexPartTiming,
22
+ type TFlexPartTimings,
23
+ } from './functions.mapping.js';
24
+ export {
25
+ flexHarnessAbortIntent,
26
+ flexHarnessPermissionIntent,
27
+ flexHarnessSendIntent,
28
+ runFlexHarnessChatIntent,
29
+ type IFlexHarnessChatTarget,
30
+ type IFlexHarnessSendIntentOptions,
31
+ type TFlexHarnessChatHost,
32
+ type TFlexHarnessChatIntent,
33
+ } from './functions.intents.js';
@@ -0,0 +1,18 @@
1
+ // @design.estate scope: the harness components' plain-data contract, types only
2
+ export type {
3
+ IHarnessAttachment,
4
+ IHarnessComposerSendDetail,
5
+ IHarnessMessage,
6
+ IHarnessPermissionRequest,
7
+ IHarnessPermissionResponseDetail,
8
+ IHarnessQuestionRequest,
9
+ IHarnessScratchpad,
10
+ IHarnessSessionMeta,
11
+ IHarnessStatus,
12
+ IHarnessTodoItem,
13
+ IHarnessToolCall,
14
+ IHarnessUsage,
15
+ THarnessSessionCardState,
16
+ THarnessStreamDelta,
17
+ THarnessToolStatus,
18
+ } from '@design.estate/dees-catalog';
@@ -0,0 +1,117 @@
1
+ A FlexHarness session as the inputs of the `@design.estate/dees-catalog` harness components (`dees-harness-chat` and its parts), and their events as FlexHarness calls.
2
+
3
+ ```sh
4
+ pnpm add @modelprofile.com/flexharness @design.estate/dees-catalog
5
+ ```
6
+
7
+ The subpath imports only types from `@design.estate/dees-catalog` and no runtime code at all, so it runs in the browser beside the components as well as on the server beside the harness.
8
+
9
+ ```typescript
10
+ import type { TFlexHarnessEvent } from '@modelprofile.com/flexharness';
11
+ import {
12
+ FlexHarnessTranscript,
13
+ flexHarnessAbortIntent,
14
+ flexHarnessPermissionIntent,
15
+ flexHarnessSendIntent,
16
+ } from '@modelprofile.com/flexharness/chat/harness';
17
+
18
+ const target = { scopeId, sessionId };
19
+ const transcript = new FlexHarnessTranscript(target);
20
+ let precedingSequence: number | undefined;
21
+ // Events that arrive while the session is being read wait here.
22
+ let buffered: TFlexHarnessEvent[] | undefined;
23
+
24
+ function apply(event: TFlexHarnessEvent): void {
25
+ const change = transcript.applyEvent(event, { precedingSequence });
26
+ precedingSequence = event.sequence;
27
+ if (change.reload) return void reloadSession();
28
+ const view = transcript.view();
29
+ if (change.messages) chat.messages = view.messages;
30
+ else for (const delta of change.deltas) chat.applyDelta(delta);
31
+ if (change.permissions) chat.permissions = view.permissions;
32
+ if (change.status) chat.status = view.status;
33
+ if (change.usage) chat.usage = view.usage;
34
+ }
35
+
36
+ async function reloadSession(): Promise<void> {
37
+ buffered = [];
38
+ const view = await harness.getSessionView(scopeId, sessionId);
39
+ transcript.load({
40
+ messages: view.messages,
41
+ permissions: view.pendingPermissions,
42
+ session: view.session,
43
+ sequence: view.sequence,
44
+ });
45
+ precedingSequence = undefined;
46
+ Object.assign(chat, transcript.view());
47
+ const pending = buffered;
48
+ buffered = undefined;
49
+ // Buffered events the view already includes change nothing.
50
+ for (const event of pending) {
51
+ if (buffered) break; // a reported gap started another reload
52
+ apply(event);
53
+ }
54
+ }
55
+
56
+ // Subscribe before the first read, so no event is lost.
57
+ harness.subscribe((event) => (buffered ? buffered.push(event) : apply(event)));
58
+ await reloadSession();
59
+
60
+ chat.addEventListener('harness-send', (event) =>
61
+ send(flexHarnessSendIntent(target, event.detail, { activeQueueId: transcript.activeQueueId })));
62
+ chat.addEventListener('harness-permission-response', (event) =>
63
+ send(flexHarnessPermissionIntent(target, event.detail)));
64
+ chat.addEventListener('harness-abort', () => send(flexHarnessAbortIntent(target)));
65
+ ```
66
+
67
+ ## Transcript
68
+
69
+ `FlexHarnessTranscript` follows one session. `load()` takes its authoritative state from `getSessionView()` and, with project management, `getProjectState()`. `applyEvent()` takes every harness event, ignores those of other sessions, and returns what changed.
70
+
71
+ The host forwards every event of the harness instance, of every session, in order, and passes the `sequence` of the event it received before as `precedingSequence` (`undefined` for the first event after `load()`). Event sequences are global to the instance, so only this proves that no event of the session was lost: an event that does not directly follow its predecessor asks for a reload. After a gap or a reconnect, the host calls `load()` and starts again with `precedingSequence: undefined`. `getSessionView()` states the `sequence` of the last event its state includes; passed to `load()`, it lets the transcript skip the events the state already holds and prove that the first event after it follows. A snapshot from `getMessages()`, `listPendingPermissions()` and `getSession()` carries no sequence, so the first event after such a `load()` is taken unchecked. `view()` returns a copy of the component inputs: `messages`, `permissions`, `questions`, `status`, `usage`, `todos` and `scratchpad`. The transcript keeps its own objects, so the deltas the components apply to theirs never apply twice.
72
+
73
+ - A user message is one row. An assistant message has one row per part, in part order: a text row, a reasoning row, or a tool row, so every streamed delta addresses exactly one row. The run's usage and error attach to its last text or reasoning row. Rows carry `order` from the message and part positions.
74
+ - `change.deltas` stream text, reasoning, tool updates and part ends into the rows the components already hold. When `change.messages` is true, rows were added or replaced: assign `view().messages` instead.
75
+ - `change.reload` means the transcript can no longer follow incrementally: events were lost, the history moved (`session.history.changed` after undo, redo or a branch), or a delta does not continue the text the transcript holds, which the delta's UTF-8 byte counters prove. Read the session again and call `load()`.
76
+ - Permission rows stay in the transcript once answered, showing the decision. A request its run leaves unanswered, because the run was stopped or failed, is removed when the run finishes.
77
+ - FlexHarness emits no event for project state. After a run, or after a project tool's row completes, read `getProjectState()` and pass it to `applyProjectState()` for the task list and the scratchpad.
78
+ - `questions` is always empty: FlexHarness has no question requests.
79
+ - Messages the session has not taken in yet (a queued prompt, a steer) carry no text in FlexHarness events. The host that sent them shows them as pending rows.
80
+ - An attachment row needs a name, a media type and a size. The public projection keeps no payload and states these only when the prompt did, so an attachment without all three, such as a remote URL of unknown size, has no row.
81
+
82
+ `flexMessageToHarnessRows()`, `flexPermissionToHarness()`, `flexSessionToHarnessMeta()` for `dees-harness-session-list`, and the other `flex*ToHarness` functions are the mappings the transcript uses, for hosts that keep their own state.
83
+
84
+ ## Intents
85
+
86
+ The intent functions map component events to plain `TFlexHarnessChatIntent` values a host carries to the server that owns the harness. There, `runFlexHarnessChatIntent(harness, intent)` makes the call and returns the admission of a queued prompt, whose `completion` the caller owns, as with `enqueuePrompt()`.
87
+
88
+ Intents are untrusted client input: a client can name any `scopeId` and `sessionId`. Before calling `runFlexHarnessChatIntent`, the server authenticates the caller and checks, or replaces, the intent's target with a session the caller may act on. The harness itself checks only that a permission belongs to the session the intent names, not who is asking.
89
+
90
+ - `harness-send`: text and attachments become prompt parts. A text attachment is sent as a UTF-8 file part; a truncated attachment, or one without a payload, throws. A message sent while idle, queued behind the running turn, or delivered as `next-turn` becomes `enqueuePrompt`. A steer joins the running turn, `transcript.activeQueueId`, through `steerPrompt`, and answers on that turn's model; with no running turn, or when its turn ended before the harness took it, it is the next turn's input and is queued with the prompt options. A steer id comes from `crypto.randomUUID()`, which browsers offer only in secure contexts (HTTPS or localhost). The composer's `model` becomes the prompt's `modelHint`; a host that offers accounts or reasoning efforts passes its own `modelHint`.
91
+ - `harness-permission-response`: `reject` rejects; an approval with `remember` is `always`, otherwise `once`. A request with `requiresExplicitResponse` refuses `always`.
92
+ - `harness-abort`: `abort()` of the session.
93
+
94
+ ## Issue Reporting and Security
95
+
96
+ Report issues and security concerns through [community.foss.global](https://community.foss.global).
97
+
98
+ ## License and Legal Information
99
+
100
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
101
+
102
+ **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
103
+
104
+ ### Trademarks
105
+
106
+ This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
107
+
108
+ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
109
+
110
+ ### Company Information
111
+
112
+ Task Venture Capital GmbH<br>
113
+ Registered at District Court Bremen HRB 35230 HB, Germany
114
+
115
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
116
+
117
+ By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.